Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name EyeWire II - Coordinate Bridges & Colour Lasso
- // @namespace eyewire-ii-community-tools
- // @version 0.9.0
- // @description XYZ bridges, persistent colour lassos, and side-by-side 2D slide screenshots
- // @match https://eyewire-ii-community-dot-brain-wire-dot-seung-lab.ue.r.appspot.com/*
- // @run-at document-idle
- // @grant none
- // ==/UserScript==
- (() => {
- 'use strict';
- if (window.__EWII_TOOLS_V09__) return;
- window.__EWII_TOOLS_V09__ = true;
- const CONFIG = {
- maxJumps: 20,
- maxLassos: 40,
- storageKey: 'ewii-tools-v03',
- opacity: 0.4,
- xyTolerance: 35,
- pollMs: 50,
- gestureQuietMs: 650,
- navigationTimeoutMs: 5000
- };
- const database = loadDatabase();
- let activeDataset = '';
- let lastPosition = null;
- let lastRenderKey = '';
- let bridgeState = 'ready';
- let navigationTarget = null;
- let navigationStarted = 0;
- let lastWheelTime = 0;
- let lassoEnabled = false;
- let drawing = false;
- let drawingPoints = [];
- let selectedColour = '#ffcc00';
- let menuOpen = false;
- // =====================================================
- // STORAGE AND STATE
- // =====================================================
- function loadDatabase() {
- try {
- const data = JSON.parse(
- localStorage.getItem(CONFIG.storageKey) || '{}'
- );
- return data && typeof data === 'object' ? data : {};
- } catch {
- return {};
- }
- }
- function saveDatabase() {
- try {
- localStorage.setItem(
- CONFIG.storageKey,
- JSON.stringify(database)
- );
- return true;
- } catch (error) {
- console.error('[EWII] Storage error:', error);
- alert('Could not save EyeWire data.');
- return false;
- }
- }
- function readState() {
- try {
- const hash = decodeURIComponent(location.hash.slice(1));
- const start = hash.indexOf('{');
- const end = hash.lastIndexOf('}');
- if (start < 0 || end < start) return null;
- const state = JSON.parse(hash.slice(start, end + 1));
- if (!Array.isArray(state.position) ||
- state.position.length < 3) return null;
- return state;
- } catch {
- return null;
- }
- }
- function datasetKey(state) {
- const sources = (state.layers || [])
- .filter(layer => layer.type === 'image')
- .map(layer => typeof layer.source === 'string'
- ? layer.source
- : layer.source?.url || '')
- .filter(Boolean);
- if (sources.length) return sources.join('|');
- return location.hostname + '|' +
- (state.layers || []).map(x => x.name || '').join('|');
- }
- function datasetData() {
- if (!database[activeDataset]) {
- database[activeDataset] = { jumps: [], lassos: [] };
- }
- const data = database[activeDataset];
- if (!Array.isArray(data.jumps)) data.jumps = [];
- if (!Array.isArray(data.lassos)) data.lassos = [];
- return data;
- }
- function currentXYZ() {
- const state = readState();
- return state ? state.position.slice(0, 3) : null;
- }
- function validXYZ(xyz) {
- return Array.isArray(xyz) &&
- xyz.length >= 3 &&
- xyz.slice(0, 3).every(Number.isFinite);
- }
- function sliceNumber(z) {
- return Math.round(z - 0.5);
- }
- function sameSlice(a, b) {
- return sliceNumber(a) === sliceNumber(b);
- }
- function makeId() {
- return typeof crypto.randomUUID === 'function'
- ? crypto.randomUUID()
- : Date.now().toString(36) +
- Math.random().toString(36).slice(2);
- }
- function navigateTo(xyz) {
- if (!validXYZ(xyz)) return false;
- const state = readState();
- if (!state) return false;
- location.hash = '#!' + encodeURIComponent(
- JSON.stringify({ ...state, position: [...xyz] })
- );
- return true;
- }
- // =====================================================
- // RELIABLE DIRECTION-AWARE BRIDGE (integrated v0.7.2)
- // =====================================================
- function nearXY(position, endpoint) {
- return Math.hypot(
- position[0] - endpoint[0],
- position[1] - endpoint[1]
- ) <= CONFIG.xyTolerance;
- }
- function findEndpoint(position) {
- if (!validXYZ(position) || !activeDataset) return null;
- let closest = null;
- let bestDistance = Infinity;
- for (const jump of datasetData().jumps) {
- // Preserve the main script's individual Auto checkboxes.
- if (!jump.enabled ||
- !validXYZ(jump.pre) ||
- !validXYZ(jump.post)) continue;
- for (const side of ['pre', 'post']) {
- const from = jump[side];
- if (!sameSlice(position[2], from[2])) continue;
- if (!nearXY(position, from)) continue;
- const distance = Math.hypot(
- position[0] - from[0],
- position[1] - from[1]
- );
- if (distance < bestDistance) {
- bestDistance = distance;
- closest = {
- jumpId: jump.id,
- jumpName: jump.name,
- side,
- from: [...from],
- to: [...jump[side === 'pre' ? 'post' : 'pre']]
- };
- }
- }
- }
- return closest;
- }
- function resetBridge() {
- bridgeState = 'ready';
- navigationTarget = null;
- navigationStarted = 0;
- lastWheelTime = 0;
- }
- function setBridgeStatus(message) {
- const el = document.querySelector('#ewii-bridge-status');
- if (el) el.textContent = message;
- }
- function find2DPanel() {
- return [...document.querySelectorAll(
- '.neuroglancer-rendered-data-panel'
- )].find(panel =>
- !panel.classList.contains('nge-3d-panel') &&
- panel.getBoundingClientRect().width > 0 &&
- panel.getBoundingClientRect().height > 0
- ) || null;
- }
- function over2D(event) {
- const panel = find2DPanel();
- if (!panel) return false;
- const r = panel.getBoundingClientRect();
- return event.clientX >= r.left && event.clientX <= r.right &&
- event.clientY >= r.top && event.clientY <= r.bottom;
- }
- function interceptWheel(event) {
- if (event.cancelable) event.preventDefault();
- event.stopImmediatePropagation();
- }
- function scrollingTowardDestination(event, endpoint) {
- const zDifference = endpoint.to[2] - endpoint.from[2];
- // In this EyeWire setup: positive deltaY decreases Z;
- // negative deltaY increases Z.
- return zDifference < 0
- ? event.deltaY > 0
- : event.deltaY < 0;
- }
- function onBridgeWheel(event) {
- if (!over2D(event)) return;
- if (event.target.closest?.('#ewii-tools-root') ||
- event.target.closest?.('#ewii-colour-dialog')) return;
- const now = performance.now();
- const quietBefore = lastWheelTime === 0
- ? Infinity : now - lastWheelTime;
- lastWheelTime = now;
- if (bridgeState === 'navigating') {
- interceptWheel(event);
- return;
- }
- if (bridgeState === 'arrival-lock') {
- if (quietBefore < CONFIG.gestureQuietMs) {
- interceptWheel(event);
- return;
- }
- // Do NOT return: this first fresh wheel event must be
- // eligible to trigger the opposite endpoint.
- bridgeState = 'ready';
- setBridgeStatus('Bridge ready — checking new gesture');
- }
- if (event.ctrlKey || event.shiftKey || event.altKey) return;
- const state = readState();
- if (!state) return;
- const key = datasetKey(state);
- if (key !== activeDataset) return;
- const endpoint = findEndpoint(state.position);
- if (!endpoint) return;
- // Allow normal scrolling AWAY from either endpoint.
- if (!scrollingTowardDestination(event, endpoint)) return;
- // Do not jump while a continuous scroll passes the endpoint.
- if (quietBefore < CONFIG.gestureQuietMs) return;
- interceptWheel(event);
- bridgeState = 'navigating';
- navigationStarted = now;
- navigationTarget = [...endpoint.to];
- setBridgeStatus(
- 'Jumping ' + endpoint.side + ' → ' +
- (endpoint.side === 'pre' ? 'post' : 'pre')
- );
- console.log('[EWII v0.8] Bridge jump', endpoint.jumpName,
- endpoint.side, endpoint.from, '→', endpoint.to);
- if (!navigateTo(navigationTarget)) {
- resetBridge();
- setBridgeStatus('Bridge navigation failed');
- }
- }
- // Capture before EyeWire handles the wheel event.
- window.addEventListener('wheel', onBridgeWheel,
- { capture: true, passive: false });
- function updateBridge(position) {
- if (bridgeState !== 'navigating' || !navigationTarget) return;
- const arrived = position.every((value, index) =>
- Math.abs(value - navigationTarget[index]) < 0.1
- );
- if (arrived) {
- bridgeState = 'arrival-lock';
- setBridgeStatus(
- 'Arrived at Z ' + sliceNumber(position[2]) +
- ' — release wheel before next jump'
- );
- } else if (performance.now() - navigationStarted >
- CONFIG.navigationTimeoutMs) {
- resetBridge();
- setBridgeStatus('Bridge timeout — ready');
- }
- }
- // =====================================================
- // JUMP MANAGEMENT
- // =====================================================
- function saveJumpPair() {
- const data = datasetData();
- if (data.jumps.length >= CONFIG.maxJumps) {
- alert('Maximum 20 jump pairs per dataset.');
- return;
- }
- const pre = currentXYZ();
- if (!pre) {
- alert('Viewer coordinates unavailable.');
- return;
- }
- const name = prompt(
- 'Name this coordinate bridge:',
- `Bridge ${data.jumps.length + 1}`
- );
- if (name === null) return;
- data.jumps.push({
- id: makeId(),
- name: name.trim() ||
- `Bridge ${data.jumps.length + 1}`,
- pre,
- post: null,
- enabled: false
- });
- saveDatabase();
- rebuildMenu();
- alert(
- 'Pre coordinates saved.\n' +
- 'Move to the continuation after the damaged slices ' +
- 'and click Set post here.'
- );
- }
- function setPost(id) {
- const jump = datasetData().jumps.find(
- x => x.id === id
- );
- const position = currentXYZ();
- if (!jump || !position) return;
- jump.post = position;
- jump.enabled = false;
- resetBridge();
- saveDatabase();
- rebuildMenu();
- }
- function deleteJump(id) {
- if (!confirm('Delete this bridge?')) return;
- const data = datasetData();
- data.jumps = data.jumps.filter(x => x.id !== id);
- resetBridge();
- saveDatabase();
- rebuildMenu();
- }
- // =====================================================
- // LASSO GEOMETRY — PRESERVED FROM v0.5
- // =====================================================
- function geometry() {
- const state = readState();
- const panel = find2DPanel();
- if (!state || !panel) return null;
- const rect = panel.getBoundingClientRect();
- const scale = Number(state.crossSectionScale);
- if (!Number.isFinite(scale) || scale <= 0) {
- return null;
- }
- return {
- panel,
- rect,
- position: state.position,
- scale,
- z: state.position[2]
- };
- }
- function physicalScaleXY(state) {
- const dims = state.dimensions || {};
- const x = Array.isArray(dims.x)
- ? Number(dims.x[0]) : 1;
- const y = Array.isArray(dims.y)
- ? Number(dims.y[0]) : 1;
- return {
- x: Number.isFinite(x) && x > 0 ? x : 1,
- y: Number.isFinite(y) && y > 0 ? y : 1
- };
- }
- function physicalPerPixel(g, dimensions) {
- const label = g.panel.querySelector(
- '.neuroglancer-display-dimensions-widget-scale'
- )?.textContent || '';
- const match = label.match(
- /([\d.]+)\s*(pm|nm|µm|um|mm|m)\s*\/\s*px/i
- );
- if (match) {
- const units = {
- pm: 1e-12,
- nm: 1e-9,
- 'µm': 1e-6,
- um: 1e-6,
- mm: 1e-3,
- m: 1
- };
- return Number(match[1]) * units[match[2]];
- }
- return dimensions.x * g.scale;
- }
- function screenToData(clientX, clientY, g) {
- const state = readState();
- if (!state) return null;
- const dimensions = physicalScaleXY(state);
- const perPixel = physicalPerPixel(g, dimensions);
- const centerX = g.rect.left + g.rect.width / 2;
- const centerY = g.rect.top + g.rect.height / 2;
- return {
- x: g.position[0] +
- (clientX - centerX) *
- perPixel / dimensions.x,
- y: g.position[1] +
- (clientY - centerY) *
- perPixel / dimensions.y
- };
- }
- function dataToScreen(point, g) {
- const state = readState();
- if (!state) return null;
- const dimensions = physicalScaleXY(state);
- const perPixel = physicalPerPixel(g, dimensions);
- const centerX = g.rect.left + g.rect.width / 2;
- const centerY = g.rect.top + g.rect.height / 2;
- return {
- x: centerX +
- (point.x - g.position[0]) *
- dimensions.x / perPixel,
- y: centerY +
- (point.y - g.position[1]) *
- dimensions.y / perPixel
- };
- }
- // =====================================================
- // VISUAL COLOUR PICKER
- // =====================================================
- function chooseLassoColour(initialColour) {
- return new Promise(resolve => {
- const backdrop = document.createElement('div');
- backdrop.id = 'ewii-colour-dialog';
- backdrop.style.cssText = `
- position:fixed;
- inset:0;
- z-index:2147483647;
- display:flex;
- align-items:center;
- justify-content:center;
- background:#0008;
- font:13px Arial,sans-serif;
- `;
- const dialog = document.createElement('div');
- dialog.style.cssText = `
- width:370px;
- max-width:calc(100vw - 30px);
- padding:15px;
- background:#f1f1f1;
- color:#111;
- border:1px solid #999;
- border-radius:6px;
- box-shadow:0 10px 35px #0008;
- box-sizing:border-box;
- `;
- const heading = document.createElement('div');
- heading.textContent = 'Color';
- heading.style.cssText =
- 'font-size:16px;margin-bottom:12px';
- const caption = document.createElement('div');
- caption.textContent = 'Basic colors:';
- caption.style.marginBottom = '7px';
- const palette = document.createElement('div');
- palette.style.cssText = `
- display:grid;
- grid-template-columns:repeat(10,1fr);
- gap:5px;
- margin-bottom:15px;
- `;
- const colours = [
- '#ff0000','#ffff00','#00ff00','#00ffff',
- '#0000ff','#ff00ff','#ffffff','#000000',
- '#ff8080','#ffff80','#80ff80','#80ffff',
- '#8080ff','#ff80ff','#808080','#800000',
- '#808000','#008000','#008080','#000080',
- '#800080','#ff8000','#80ff00','#00ff80',
- '#0080ff','#8000ff','#ff0080','#804000',
- '#408000','#008040','#004080','#400080',
- '#804080','#c0c0c0','#404040','#202020',
- '#ffcc00','#aaff00','#00aaff','#aa00ff'
- ];
- let selected = initialColour || '#ffcc00';
- const previewRow = document.createElement('div');
- previewRow.style.cssText = `
- display:flex;
- align-items:center;
- gap:12px;
- margin-bottom:14px;
- `;
- const preview = document.createElement('div');
- preview.style.cssText = `
- width:75px;
- height:48px;
- border:1px solid #888;
- background:${selected};
- flex-shrink:0;
- `;
- const picker = document.createElement('input');
- picker.type = 'color';
- picker.value = selected;
- picker.title = 'Open custom colour picker';
- picker.style.cssText = `
- width:80px;
- height:48px;
- cursor:pointer;
- `;
- const label = document.createElement('span');
- label.textContent = 'Custom colors';
- function updateColour(colour) {
- selected = colour;
- preview.style.background = colour;
- picker.value = colour;
- }
- for (const colour of colours) {
- const swatch = document.createElement('button');
- swatch.type = 'button';
- swatch.title = colour;
- swatch.style.cssText = `
- height:23px;
- border:1px solid #888;
- border-radius:2px;
- background:${colour};
- cursor:pointer;
- `;
- swatch.addEventListener('click', () => {
- updateColour(colour);
- });
- palette.appendChild(swatch);
- }
- picker.addEventListener('input', () => {
- updateColour(picker.value);
- });
- previewRow.append(preview, picker, label);
- const actions = document.createElement('div');
- actions.style.cssText = `
- display:flex;
- justify-content:flex-end;
- gap:8px;
- `;
- let closed = false;
- function close(value) {
- if (closed) return;
- closed = true;
- document.removeEventListener(
- 'keydown',
- onKeyDown,
- true
- );
- backdrop.remove();
- resolve(value);
- }
- function onKeyDown(event) {
- if (event.key === 'Escape') {
- event.preventDefault();
- event.stopImmediatePropagation();
- close(null);
- }
- }
- document.addEventListener(
- 'keydown',
- onKeyDown,
- true
- );
- const ok = document.createElement('button');
- ok.textContent = 'OK';
- ok.addEventListener('click', () => close(selected));
- const cancel = document.createElement('button');
- cancel.textContent = 'Cancel';
- cancel.addEventListener('click', () => close(null));
- for (const button of [ok, cancel]) {
- button.style.cssText = `
- padding:7px 22px;
- border:1px solid #999;
- border-radius:4px;
- background:white;
- cursor:pointer;
- `;
- }
- actions.append(ok, cancel);
- dialog.append(
- heading,
- caption,
- palette,
- previewRow,
- actions
- );
- backdrop.appendChild(dialog);
- document.body.appendChild(backdrop);
- });
- }
- // =====================================================
- // COLOUR LASSO
- // =====================================================
- const overlay = document.createElement('canvas');
- overlay.id = 'ewii-lasso-overlay';
- overlay.style.cssText = `
- position:fixed;
- z-index:2147482000;
- display:none;
- pointer-events:none;
- touch-action:none;
- `;
- document.body.appendChild(overlay);
- function setLassoEnabled(value) {
- lassoEnabled = Boolean(value);
- drawing = false;
- drawingPoints = [];
- overlay.style.pointerEvents =
- lassoEnabled ? 'auto' : 'none';
- overlay.style.cursor =
- lassoEnabled ? 'crosshair' : 'default';
- rebuildMenu();
- renderLassos();
- }
- function beginDrawing(event) {
- if (!lassoEnabled || event.button !== 0) return;
- const g = geometry();
- if (!g) return;
- if (
- datasetData().lassos.length >=
- CONFIG.maxLassos
- ) {
- alert('Maximum 40 lassos per dataset.');
- return;
- }
- const point = screenToData(
- event.clientX,
- event.clientY,
- g
- );
- if (!point) return;
- event.preventDefault();
- event.stopPropagation();
- drawing = true;
- drawingPoints = [point];
- try {
- overlay.setPointerCapture(event.pointerId);
- } catch {}
- renderLassos();
- }
- function moveDrawing(event) {
- if (!drawing) return;
- const g = geometry();
- if (!g) return;
- const point = screenToData(
- event.clientX,
- event.clientY,
- g
- );
- if (!point) return;
- const previous = drawingPoints.at(-1);
- if (
- !previous ||
- Math.hypot(
- point.x - previous.x,
- point.y - previous.y
- ) > 0.3
- ) {
- drawingPoints.push(point);
- }
- renderLassos();
- }
- async function finishDrawing(event) {
- if (!drawing) return;
- drawing = false;
- try {
- overlay.releasePointerCapture(event.pointerId);
- } catch {}
- if (drawingPoints.length < 3) {
- drawingPoints = [];
- renderLassos();
- return;
- }
- const completedPoints = drawingPoints.map(
- point => ({ x: point.x, y: point.y })
- );
- const g = geometry();
- if (!g) {
- drawingPoints = [];
- renderLassos();
- return;
- }
- const drawingZ = g.z;
- const drawingDataset = activeDataset;
- const colour = await chooseLassoColour(
- selectedColour
- );
- if (colour === null) {
- drawingPoints = [];
- renderLassos();
- return;
- }
- selectedColour = colour;
- if (!database[drawingDataset]) {
- database[drawingDataset] = {
- jumps: [],
- lassos: []
- };
- }
- const data = database[drawingDataset];
- if (!Array.isArray(data.lassos)) {
- data.lassos = [];
- }
- if (data.lassos.length >= CONFIG.maxLassos) {
- alert('Maximum 40 lassos per dataset.');
- drawingPoints = [];
- return;
- }
- data.lassos.push({
- id: makeId(),
- z: drawingZ,
- colour: selectedColour,
- points: completedPoints,
- created: new Date().toISOString()
- });
- drawingPoints = [];
- saveDatabase();
- rebuildMenu();
- renderLassos();
- }
- overlay.addEventListener('pointerdown', beginDrawing);
- overlay.addEventListener('pointermove', moveDrawing);
- overlay.addEventListener('pointerup', finishDrawing);
- overlay.addEventListener('pointercancel', () => {
- drawing = false;
- drawingPoints = [];
- renderLassos();
- });
- function drawPolygon(ctx, points, colour, g, fill) {
- if (points.length < 2) return;
- ctx.beginPath();
- let started = false;
- for (const point of points) {
- const p = dataToScreen(point, g);
- if (!p) continue;
- const x = p.x - g.rect.left;
- const y = p.y - g.rect.top;
- if (!started) {
- ctx.moveTo(x, y);
- started = true;
- } else {
- ctx.lineTo(x, y);
- }
- }
- if (!started) return;
- if (fill) ctx.closePath();
- ctx.strokeStyle = colour;
- ctx.lineWidth = 2;
- ctx.stroke();
- if (fill) {
- ctx.fillStyle = colour;
- ctx.globalAlpha = CONFIG.opacity;
- ctx.fill();
- ctx.globalAlpha = 1;
- }
- }
- function renderLassos() {
- const g = geometry();
- if (!g) {
- overlay.style.display = 'none';
- return;
- }
- overlay.style.display = 'block';
- overlay.style.left = g.rect.left + 'px';
- overlay.style.top = g.rect.top + 'px';
- overlay.style.width = g.rect.width + 'px';
- overlay.style.height = g.rect.height + 'px';
- const dpr = window.devicePixelRatio || 1;
- const width = Math.max(
- 1, Math.round(g.rect.width * dpr)
- );
- const height = Math.max(
- 1, Math.round(g.rect.height * dpr)
- );
- if (overlay.width !== width) overlay.width = width;
- if (overlay.height !== height) overlay.height = height;
- const ctx = overlay.getContext('2d');
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
- ctx.clearRect(0, 0, g.rect.width, g.rect.height);
- for (const lasso of datasetData().lassos) {
- if (!sameSlice(lasso.z, g.z)) continue;
- drawPolygon(
- ctx, lasso.points, lasso.colour, g, true
- );
- }
- if (drawing && drawingPoints.length) {
- drawPolygon(
- ctx, drawingPoints, selectedColour, g, false
- );
- }
- }
- function deleteLasso(id) {
- if (!confirm('Delete this colour lasso?')) return;
- const data = datasetData();
- data.lassos = data.lassos.filter(x => x.id !== id);
- saveDatabase();
- rebuildMenu();
- renderLassos();
- }
- // =====================================================
- // MENU
- // =====================================================
- const root = document.createElement('div');
- root.id = 'ewii-tools-root';
- root.style.cssText = `
- position:fixed;
- z-index:2147483000;
- display:none;
- font:12px Arial,sans-serif;
- color:#e9f5ff;
- `;
- const menu = document.createElement('div');
- menu.style.cssText = `
- position:absolute;
- top:29px;
- right:0;
- width:315px;
- max-height:75vh;
- overflow:auto;
- background:#102332;
- border:1px solid #4b9fc0;
- border-radius:8px;
- padding:12px;
- box-shadow:0 8px 24px #0009;
- display:none;
- box-sizing:border-box;
- `;
- const trigger = document.createElement('button');
- trigger.textContent = 'Tools ▾';
- trigger.title = 'EyeWire coordinate bridges and lasso';
- trigger.style.cssText = `
- background:#124d70;
- color:white;
- border:1px solid #54a9cb;
- border-radius:5px;
- padding:5px 8px;
- cursor:pointer;
- white-space:nowrap;
- font:12px Arial,sans-serif;
- `;
- document.body.appendChild(root);
- root.append(trigger, menu);
- function makeButton(text, callback, colour = '#195777') {
- const button = document.createElement('button');
- button.textContent = text;
- button.style.cssText = `
- border:1px solid #4e91ab;
- background:${colour};
- color:white;
- padding:5px 7px;
- border-radius:4px;
- cursor:pointer;
- font:12px Arial,sans-serif;
- `;
- button.addEventListener('click', callback);
- return button;
- }
- function makeSection(title) {
- const section = document.createElement('div');
- section.style.cssText = `
- border-top:1px solid #385568;
- margin-top:11px;
- padding-top:10px;
- `;
- const heading = document.createElement('div');
- heading.textContent = title;
- heading.style.cssText =
- 'font-weight:bold;margin-bottom:8px;font-size:13px';
- section.appendChild(heading);
- return section;
- }
- function makeRow() {
- const row = document.createElement('div');
- row.style.cssText = `
- display:flex;
- flex-wrap:wrap;
- gap:5px;
- align-items:center;
- margin-bottom:7px;
- `;
- return row;
- }
- // =====================================================
- // 2D SLIDE SCREENSHOTS — v0.9
- // =====================================================
- // Isolated from the existing bridge and lasso logic.
- const slideTools = (() => {
- const shots = [null, null];
- let busy = false;
- let status = 'Ready — capture Slide 1 or Slide 2.';
- let statusEl = null;
- let previewDialog = null;
- const id = 'ewii-slide-screenshots-section';
- const sleep = ms =>
- new Promise(resolve => setTimeout(resolve, ms));
- function panel2D() {
- return [...document.querySelectorAll(
- '.neuroglancer-rendered-data-panel'
- )].find(el =>
- !el.classList.contains('nge-3d-panel') &&
- el.getBoundingClientRect().width > 0 &&
- el.getBoundingClientRect().height > 0
- );
- }
- function setStatus(message) {
- status = message;
- if (statusEl?.isConnected) {
- statusEl.textContent = message;
- }
- console.log('[EWII 2D Slides]', message);
- }
- function screenshotButton() {
- return document.querySelector(
- 'button[title="Take a screenshot. Drag to reorder."]'
- ) || [...document.querySelectorAll('button')].find(
- button => /take a screenshot/i.test(
- button.title ||
- button.getAttribute('aria-label') ||
- ''
- )
- );
- }
- function closeNativeDialog() {
- const dialog = document.querySelector(
- '.nge-shotdlg-stage'
- )?.closest('[role="dialog"]') ||
- document.querySelector(
- '.nge-shotdlg-stage'
- )?.parentElement?.parentElement;
- if (!dialog) return;
- const buttons = [
- ...dialog.querySelectorAll('button')
- ];
- const cancel = buttons.find(
- button => /^cancel$/i.test(
- button.textContent.trim()
- )
- );
- if (cancel) cancel.click();
- }
- function downloadCanvas(canvas, filename) {
- canvas.toBlob(blob => {
- if (!blob) {
- setStatus('JPG export failed.');
- return;
- }
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = filename;
- document.body.appendChild(link);
- link.click();
- link.remove();
- setTimeout(() => URL.revokeObjectURL(url), 10000);
- }, 'image/jpeg', 0.95);
- }
- function canvasButton(label, onClick) {
- const button = document.createElement('button');
- button.textContent = label;
- button.type = 'button';
- button.style.cssText =
- 'background:#195777;color:#fff;' +
- 'border:1px solid #4e91ab;' +
- 'border-radius:4px;padding:6px 8px;' +
- 'cursor:pointer;font:12px Arial,sans-serif';
- button.addEventListener('click', onClick);
- return button;
- }
- function compose(source, geometry, lassoCopy) {
- const {
- panelRect,
- viewportHeight,
- viewportWidth,
- lassoRect
- } = geometry;
- // EyeWire's screenshot source is a full-resolution
- // composite with its top toolbar excluded.
- const sourceOffsetY = Math.max(
- 0,
- viewportHeight - source.height
- );
- const scaleX = source.width / viewportWidth;
- const scaleY = scaleX;
- const sx = Math.round(panelRect.x * scaleX);
- const sy = Math.round(
- (panelRect.y - sourceOffsetY) * scaleY
- );
- const sw = Math.round(
- panelRect.width * scaleX
- );
- const sh = Math.round(
- panelRect.height * scaleY
- );
- if (
- sw < 10 ||
- sh < 10 ||
- sx < 0 ||
- sy < 0 ||
- sx + sw > source.width + 2 ||
- sy + sh > source.height + 2
- ) {
- throw new Error(
- 'Screenshot source/panel geometry mismatch: ' +
- JSON.stringify({
- source: [source.width, source.height],
- sx, sy, sw, sh
- })
- );
- }
- const output = document.createElement('canvas');
- output.width = Math.min(
- sw,
- source.width - sx
- );
- output.height = Math.min(
- sh,
- source.height - sy
- );
- const ctx = output.getContext('2d');
- ctx.fillStyle = '#000';
- ctx.fillRect(
- 0, 0,
- output.width,
- output.height
- );
- ctx.drawImage(
- source,
- sx, sy,
- output.width, output.height,
- 0, 0,
- output.width, output.height
- );
- // Composite the existing transparent lasso canvas.
- // No changes are made to saved lasso coordinates.
- if (
- lassoCopy &&
- lassoRect?.width &&
- lassoRect?.height
- ) {
- const dx =
- (lassoRect.x - panelRect.x) /
- panelRect.width * output.width;
- const dy =
- (lassoRect.y - panelRect.y) /
- panelRect.height * output.height;
- const dw =
- lassoRect.width /
- panelRect.width * output.width;
- const dh =
- lassoRect.height /
- panelRect.height * output.height;
- ctx.drawImage(
- lassoCopy,
- dx, dy, dw, dh
- );
- }
- return output;
- }
- async function capture(slot) {
- if (busy) {
- setStatus('A capture is already in progress.');
- return;
- }
- if (document.querySelector(
- '.nge-shotdlg-preview'
- )) {
- setStatus(
- 'Close the native EyeWire screenshot popup first.'
- );
- return;
- }
- const panel = panel2D();
- const button = screenshotButton();
- if (!panel || !button) {
- setStatus(
- '2D panel or native screenshot button not found.'
- );
- return;
- }
- const lasso = document.querySelector(
- '#ewii-lasso-overlay'
- );
- const panelRect = panel.getBoundingClientRect();
- const lassoRect = lasso?.getBoundingClientRect();
- const geometry = {
- panelRect: {
- x: panelRect.x,
- y: panelRect.y,
- width: panelRect.width,
- height: panelRect.height
- },
- lassoRect: lassoRect ? {
- x: lassoRect.x,
- y: lassoRect.y,
- width: lassoRect.width,
- height: lassoRect.height
- } : null,
- viewportWidth: innerWidth,
- viewportHeight: innerHeight
- };
- // Copy the lasso before the native dialog opens.
- let lassoCopy = null;
- if (lasso?.width && lasso?.height) {
- lassoCopy = document.createElement('canvas');
- lassoCopy.width = lasso.width;
- lassoCopy.height = lasso.height;
- lassoCopy.getContext('2d').drawImage(
- lasso, 0, 0
- );
- }
- busy = true;
- setStatus(
- 'Capturing Slide ' + (slot + 1) + '…'
- );
- const proto = CanvasRenderingContext2D.prototype;
- const original = proto.drawImage;
- let finished = false;
- let timer;
- let resolveCapture;
- const result = new Promise(resolve => {
- resolveCapture = resolve;
- });
- function finish(error, canvas) {
- if (finished) return;
- finished = true;
- clearTimeout(timer);
- if (proto.drawImage === wrapped) {
- proto.drawImage = original;
- }
- resolveCapture({ error, canvas });
- }
- function wrapped(...args) {
- // Let EyeWire draw normally first.
- const result = original.apply(this, args);
- if (
- !finished &&
- this.canvas?.classList?.contains(
- 'nge-shotdlg-preview'
- ) &&
- args[0] instanceof HTMLCanvasElement
- ) {
- try {
- // This is the full-resolution source canvas
- // confirmed by the Firefox diagnostic.
- const image = compose(
- args[0],
- geometry,
- lassoCopy
- );
- finish(null, image);
- } catch (error) {
- finish(error, null);
- }
- }
- return result;
- }
- proto.drawImage = wrapped;
- timer = setTimeout(
- () => finish(
- new Error(
- 'Timed out waiting for native capture.'
- ),
- null
- ),
- 8000
- );
- try {
- button.click();
- } catch (error) {
- finish(error, null);
- }
- const { error, canvas } = await result;
- busy = false;
- if (error) {
- setStatus(
- 'Capture failed: ' + error.message
- );
- console.error(
- '[EWII 2D Slides]',
- error
- );
- return;
- }
- shots[slot] = {
- canvas,
- time: new Date(),
- z: currentXYZ()?.[2] ?? null
- };
- setStatus(
- 'Slide ' + (slot + 1) +
- ' captured (' +
- canvas.width + ' × ' +
- canvas.height + ').'
- );
- await sleep(120);
- closeNativeDialog();
- if (previewDialog?.isConnected) {
- refreshPreview();
- }
- }
- function combinedCanvas() {
- const available = shots.filter(Boolean);
- if (!available.length) return null;
- const gap = 20;
- const height = Math.max(
- ...available.map(
- shot => shot.canvas.height
- )
- );
- const width = available.reduce(
- (sum, shot) => sum + shot.canvas.width,
- 0
- ) + gap * (available.length - 1);
- const output = document.createElement('canvas');
- output.width = width;
- output.height = height;
- const ctx = output.getContext('2d');
- ctx.fillStyle = '#ffffff';
- ctx.fillRect(
- 0, 0,
- width, height
- );
- let x = 0;
- for (const shot of available) {
- ctx.drawImage(
- shot.canvas,
- x, 0
- );
- x += shot.canvas.width + gap;
- }
- return output;
- }
- function refreshPreview() {
- if (!previewDialog) return;
- const content = previewDialog.querySelector(
- '[data-ewii-slide-content]'
- );
- if (!content) return;
- content.replaceChildren();
- for (let i = 0; i < 2; i++) {
- const col = document.createElement('div');
- col.style.cssText =
- 'flex:1;min-width:0;text-align:center';
- const heading = document.createElement('div');
- heading.textContent =
- 'Slide ' + (i + 1) +
- (shots[i]?.z != null
- ? ' — Z ' + shots[i].z
- : '');
- heading.style.cssText =
- 'margin-bottom:8px;font-weight:bold';
- col.append(heading);
- if (shots[i]) {
- const img = document.createElement('img');
- img.src = shots[i].canvas.toDataURL(
- 'image/png'
- );
- img.style.cssText =
- 'display:block;width:100%;height:auto;' +
- 'object-fit:contain;' +
- 'border:1px solid #42657d;' +
- 'box-sizing:border-box';
- col.append(img);
- } else {
- const placeholder =
- document.createElement('div');
- placeholder.textContent =
- 'Not captured yet';
- placeholder.style.cssText =
- 'padding:70px 5px;' +
- 'border:1px dashed #42657d';
- col.append(placeholder);
- }
- content.append(col);
- }
- }
- function showPreview() {
- if (previewDialog?.isConnected) {
- refreshPreview();
- return;
- }
- const modal = document.createElement('div');
- modal.id = 'ewii-slide-preview';
- modal.style.cssText =
- 'position:fixed;inset:0;' +
- 'z-index:2147483646;' +
- 'background:#000b;' +
- 'display:flex;' +
- 'align-items:center;' +
- 'justify-content:center;' +
- 'font:13px Arial,sans-serif;' +
- 'color:#e9f5ff';
- const card = document.createElement('div');
- card.style.cssText =
- 'background:#102332;' +
- 'border:1px solid #4b9fc0;' +
- 'border-radius:9px;' +
- 'padding:16px;' +
- 'width:min(94vw,1500px);' +
- 'max-height:90vh;' +
- 'overflow:auto;' +
- 'box-shadow:0 12px 40px #000a';
- const title = document.createElement('h3');
- title.textContent =
- '2D Slide Screenshots';
- title.style.cssText =
- 'margin:0 0 12px';
- const content = document.createElement('div');
- content.dataset.ewiiSlideContent = '1';
- content.style.cssText =
- 'display:flex;' +
- 'gap:12px;' +
- 'align-items:flex-start';
- const actions = document.createElement('div');
- actions.style.cssText =
- 'display:flex;' +
- 'flex-wrap:wrap;' +
- 'gap:8px;' +
- 'margin-top:12px';
- actions.append(
- canvasButton(
- 'Capture Slide 1',
- () => {
- modal.remove();
- previewDialog = null;
- capture(0);
- }
- ),
- canvasButton(
- 'Capture Slide 2',
- () => {
- modal.remove();
- previewDialog = null;
- capture(1);
- }
- ),
- canvasButton(
- 'Download combined JPG',
- () => {
- const canvas = combinedCanvas();
- if (!canvas) {
- alert(
- 'Capture at least one slide first.'
- );
- return;
- }
- downloadCanvas(
- canvas,
- 'eyewire-2d-slides.jpg'
- );
- }
- ),
- canvasButton(
- 'Close',
- () => {
- modal.remove();
- previewDialog = null;
- }
- )
- );
- card.append(
- title,
- content,
- actions
- );
- modal.append(card);
- modal.addEventListener('click', event => {
- if (event.target === modal) {
- modal.remove();
- previewDialog = null;
- }
- });
- document.body.append(modal);
- previewDialog = modal;
- refreshPreview();
- }
- function makeSlideSection() {
- const section = document.createElement('div');
- section.id = id;
- section.style.cssText =
- 'border-top:1px solid #385568;' +
- 'margin-top:11px;' +
- 'padding-top:10px';
- const heading = document.createElement('div');
- heading.textContent =
- '2D Slide Screenshots';
- heading.style.cssText =
- 'font-weight:bold;' +
- 'margin-bottom:8px;' +
- 'font-size:13px';
- const row = document.createElement('div');
- row.style.cssText =
- 'display:flex;' +
- 'flex-wrap:wrap;' +
- 'gap:5px;' +
- 'margin-bottom:7px';
- row.append(
- canvasButton(
- 'Capture Slide 1',
- () => capture(0)
- ),
- canvasButton(
- 'Capture Slide 2',
- () => capture(1)
- ),
- canvasButton(
- 'Preview',
- showPreview
- ),
- canvasButton(
- 'Download JPG',
- () => {
- const canvas = combinedCanvas();
- if (!canvas) {
- setStatus(
- 'Capture a slide first.'
- );
- return;
- }
- downloadCanvas(
- canvas,
- 'eyewire-2d-slides.jpg'
- );
- }
- )
- );
- const info = document.createElement('div');
- info.style.cssText =
- 'font-size:11px;' +
- 'color:#b5d6e7;' +
- 'margin-top:5px';
- info.textContent = status;
- statusEl = info;
- section.append(
- heading,
- row,
- info
- );
- return section;
- }
- return {
- makeSection: makeSlideSection
- };
- })();
- // =====================================================
- // MENU CONTENT
- // =====================================================
- function rebuildMenu() {
- if (!activeDataset) return;
- menu.replaceChildren();
- const data = datasetData();
- const title = document.createElement('div');
- title.textContent = 'EyeWire II Tools';
- title.style.cssText =
- 'font-size:16px;font-weight:bold;margin-bottom:6px';
- menu.append(title);
- const position = currentXYZ();
- const coordinates = document.createElement('div');
- coordinates.textContent = position
- ? 'XYZ: ' + position.map(x => x.toFixed(2)).join(', ')
- : 'Coordinates unavailable';
- coordinates.style.cssText =
- 'font-size:11px;color:#b5d6e7;overflow-wrap:anywhere';
- menu.append(coordinates);
- const status = document.createElement('div');
- status.id = 'ewii-bridge-status';
- status.textContent = 'Bridge: ' + bridgeState;
- status.style.cssText =
- 'font-size:11px;color:#88e3c0;margin-top:6px';
- menu.append(status);
- const jumpSection = makeSection(
- `Coordinate bridges (${data.jumps.length}/${CONFIG.maxJumps})`
- );
- jumpSection.append(
- makeButton('+ Save pre-jump here', saveJumpPair)
- );
- data.jumps.forEach(jump => {
- const item = document.createElement('div');
- item.style.cssText = `
- padding:7px;
- margin-top:7px;
- background:#183747;
- border-radius:5px;
- `;
- const name = document.createElement('div');
- name.textContent = jump.name;
- name.style.fontWeight = 'bold';
- const info = document.createElement('div');
- info.style.cssText =
- 'font-size:11px;color:#bcd6e5;margin:5px 0';
- info.textContent =
- `Pre Z: ${jump.pre?.[2] ?? '?'} | ` +
- `Post Z: ${jump.post?.[2] ?? 'Not set'}`;
- const actions = makeRow();
- if (!validXYZ(jump.post)) {
- actions.append(
- makeButton(
- 'Set post here',
- () => setPost(jump.id)
- )
- );
- } else {
- actions.append(
- makeButton(
- 'Go pre',
- () => {
- resetBridge();
- navigateTo(jump.pre);
- }
- ),
- makeButton(
- 'Go post',
- () => {
- resetBridge();
- navigateTo(jump.post);
- }
- )
- );
- }
- const enabled = document.createElement('label');
- enabled.style.cssText =
- 'display:flex;align-items:center;gap:3px';
- const checkbox = document.createElement('input');
- checkbox.type = 'checkbox';
- checkbox.checked = Boolean(jump.enabled);
- checkbox.disabled = !validXYZ(jump.post);
- checkbox.addEventListener('change', () => {
- jump.enabled = checkbox.checked;
- resetBridge();
- saveDatabase();
- });
- enabled.append(checkbox, 'Auto');
- actions.append(enabled);
- actions.append(
- makeButton(
- 'Delete',
- () => deleteJump(jump.id),
- '#823d49'
- )
- );
- item.append(name, info, actions);
- jumpSection.append(item);
- });
- menu.append(jumpSection);
- const lassoSection = makeSection(
- `Colour lassos (${data.lassos.length}/${CONFIG.maxLassos})`
- );
- const toggleRow = makeRow();
- toggleRow.append(
- makeButton(
- lassoEnabled
- ? 'Lasso ON — disable'
- : 'Enable lasso',
- () => setLassoEnabled(!lassoEnabled),
- lassoEnabled ? '#177b55' : '#195777'
- )
- );
- lassoSection.append(toggleRow);
- const note = document.createElement('div');
- note.textContent =
- 'Enable lasso, left-drag on the 2D image, ' +
- 'then choose a colour from the visual picker.';
- note.style.cssText =
- 'font-size:11px;color:#b5d6e7;margin-bottom:8px';
- lassoSection.append(note);
- data.lassos.forEach((lasso, index) => {
- const row = makeRow();
- const swatch = document.createElement('span');
- swatch.style.cssText = `
- width:13px;
- height:13px;
- background:${lasso.colour};
- border:1px solid white;
- display:inline-block;
- `;
- const label = document.createElement('span');
- label.textContent = `#${index + 1} — Z ${lasso.z}`;
- row.append(
- swatch,
- label,
- makeButton(
- 'Delete',
- () => deleteLasso(lasso.id),
- '#823d49'
- )
- );
- lassoSection.append(row);
- });
- lassoSection.append(
- makeButton(
- 'Reset all lassos for this dataset',
- () => {
- if (!confirm(
- 'Delete all saved colour lassos for this dataset?'
- )) return;
- data.lassos = [];
- saveDatabase();
- rebuildMenu();
- renderLassos();
- },
- '#823d49'
- )
- );
- menu.append(lassoSection);
- // Third section: 2D Slide Screenshots.
- menu.append(slideTools.makeSection());
- const footer = makeSection('Dataset data');
- const datasetLabel = document.createElement('div');
- datasetLabel.textContent = activeDataset;
- datasetLabel.style.cssText = `
- font-size:10px;
- color:#a8cadb;
- overflow-wrap:anywhere;
- margin-bottom:8px;
- `;
- footer.append(datasetLabel);
- footer.append(
- makeButton(
- 'Reset all jumps for this dataset',
- () => {
- if (!confirm(
- 'Delete all coordinate bridges for this dataset?'
- )) return;
- data.jumps = [];
- resetBridge();
- saveDatabase();
- rebuildMenu();
- },
- '#823d49'
- )
- );
- menu.append(footer);
- }
- trigger.addEventListener('click', () => {
- menuOpen = !menuOpen;
- menu.style.display = menuOpen ? 'block' : 'none';
- if (menuOpen) rebuildMenu();
- });
- root.addEventListener('mouseenter', () => {
- menuOpen = true;
- menu.style.display = 'block';
- rebuildMenu();
- });
- root.addEventListener('mouseleave', () => {
- menuOpen = false;
- menu.style.display = 'none';
- });
- function visibleButton(selector) {
- return [...document.querySelectorAll(selector)]
- .find(button => {
- const rect = button.getBoundingClientRect();
- return rect.width > 0 && rect.height > 0;
- });
- }
- function positionMenu() {
- const layout = document.body.dataset.ngeLayout;
- if (layout === '3d') {
- root.style.display = 'none';
- return;
- }
- const button = layout === 'xy'
- ? visibleButton('button[data-nge-btn="split"]') ||
- visibleButton('button[data-nge-btn="xy"]')
- : visibleButton('button[data-nge-btn="xy"]');
- if (!button) {
- root.style.display = 'none';
- return;
- }
- const rect = button.getBoundingClientRect();
- root.style.display = 'block';
- root.style.left = Math.max(0, rect.right - 65) + 'px';
- root.style.top = rect.bottom + 5 + 'px';
- }
- // =====================================================
- // MAIN UPDATE LOOP
- // =====================================================
- function update() {
- const state = readState();
- if (state) {
- const key = datasetKey(state);
- if (key !== activeDataset) {
- activeDataset = key;
- lastPosition = null;
- lastRenderKey = '';
- resetBridge();
- drawing = false;
- drawingPoints = [];
- rebuildMenu();
- }
- const current = state.position.slice(0, 3);
- updateBridge(current);
- lastPosition = current;
- const g = geometry();
- const renderKey = JSON.stringify({
- dataset: activeDataset,
- position: current,
- scale: state.crossSectionScale,
- rect: g ? [
- g.rect.x, g.rect.y,
- g.rect.width, g.rect.height
- ] : null,
- drawing: drawingPoints.length
- });
- if (renderKey !== lastRenderKey || drawing) {
- lastRenderKey = renderKey;
- renderLassos();
- }
- }
- positionMenu();
- }
- setInterval(update, CONFIG.pollMs);
- update();
- console.log('[EyeWire II Tools v0.9] Loaded');
- })();
Advertisement
Add Comment
Please, Sign In to add comment