Guest User

Untitled

a guest
Nov 1st, 2026
22
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 52.17 KB | None | 0 0
  1.  
  2. // ==UserScript==
  3. // @name EyeWire II - Coordinate Bridges & Colour Lasso
  4. // @namespace eyewire-ii-community-tools
  5. // @version 0.9.1
  6. // @description XYZ bridges, persistent colour lassos, and side-by-side 2D slide screenshots
  7. // @match https://eyewire-ii-community-dot-brain-wire-dot-seung-lab.ue.r.appspot.com/*
  8. // @run-at document-idle
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. /* global viewer */
  13.  
  14. (() => {
  15. 'use strict';
  16.  
  17. if (window.__EWII_TOOLS_V09__) return;
  18. window.__EWII_TOOLS_V09__ = true;
  19.  
  20. const CONFIG = {
  21. maxJumps: 20,
  22. maxLassos: 40,
  23. storageKey: 'ewii-tools-v03',
  24. opacity: 0.4,
  25. xyTolerance: 35,
  26. pollMs: 50,
  27. gestureQuietMs: 650,
  28. navigationTimeoutMs: 5000
  29. };
  30.  
  31. const database = loadDatabase();
  32.  
  33. let activeDataset = '';
  34. let lastPosition = null;
  35. let lastRenderKey = '';
  36.  
  37. let bridgeState = 'ready';
  38. let navigationTarget = null;
  39. let navigationStarted = 0;
  40. let lastWheelTime = 0;
  41.  
  42. let lassoEnabled = false;
  43. let drawing = false;
  44. let drawingPoints = [];
  45. let selectedColour = '#ffcc00';
  46. let menuOpen = false;
  47.  
  48. // =====================================================
  49. // STORAGE AND STATE
  50. // =====================================================
  51.  
  52. function loadDatabase() {
  53. try {
  54. const data = JSON.parse(
  55. localStorage.getItem(CONFIG.storageKey) || '{}'
  56. );
  57. return data && typeof data === 'object' ? data : {};
  58. } catch {
  59. return {};
  60. }
  61. }
  62.  
  63. function saveDatabase() {
  64. try {
  65. localStorage.setItem(
  66. CONFIG.storageKey,
  67. JSON.stringify(database)
  68. );
  69. return true;
  70. } catch (error) {
  71. console.error('[EWII] Storage error:', error);
  72. alert('Could not save EyeWire data.');
  73. return false;
  74. }
  75. }
  76.  
  77. function readState() {
  78. try {
  79. const hash = decodeURIComponent(location.hash.slice(1));
  80. const start = hash.indexOf('{');
  81. const end = hash.lastIndexOf('}');
  82.  
  83. if (start < 0 || end < start) return null;
  84.  
  85. const state = JSON.parse(hash.slice(start, end + 1));
  86.  
  87. if (!Array.isArray(state.position) ||
  88. state.position.length < 3) return null;
  89.  
  90. return state;
  91. } catch {
  92. return null;
  93. }
  94. }
  95.  
  96. function datasetKey(state) {
  97. const sources = (state.layers || [])
  98. .filter(layer => layer.type === 'image')
  99. .map(layer => typeof layer.source === 'string'
  100. ? layer.source
  101. : layer.source?.url || '')
  102. .filter(Boolean);
  103.  
  104. if (sources.length) return sources.join('|');
  105.  
  106. return location.hostname + '|' +
  107. (state.layers || []).map(x => x.name || '').join('|');
  108. }
  109.  
  110. function datasetData() {
  111. if (!database[activeDataset]) {
  112. database[activeDataset] = { jumps: [], lassos: [] };
  113. }
  114.  
  115. const data = database[activeDataset];
  116.  
  117. if (!Array.isArray(data.jumps)) data.jumps = [];
  118. if (!Array.isArray(data.lassos)) data.lassos = [];
  119.  
  120. return data;
  121. }
  122.  
  123. function currentXYZ() {
  124. const state = readState();
  125. return state ? state.position.slice(0, 3) : null;
  126. }
  127.  
  128. function validXYZ(xyz) {
  129. return Array.isArray(xyz) &&
  130. xyz.length >= 3 &&
  131. xyz.slice(0, 3).every(Number.isFinite);
  132. }
  133.  
  134. function sliceNumber(z) {
  135. return Math.round(z - 0.5);
  136. }
  137.  
  138. function sameSlice(a, b) {
  139. return sliceNumber(a) === sliceNumber(b);
  140. }
  141.  
  142. function makeId() {
  143. return typeof crypto.randomUUID === 'function'
  144. ? crypto.randomUUID()
  145. : Date.now().toString(36) +
  146. Math.random().toString(36).slice(2);
  147. }
  148.  
  149. function navigateTo(xyz) {
  150. if (!validXYZ(xyz)) return false;
  151.  
  152. viewer.position.coordinates_ = new Float32Array([...xyz])
  153. viewer.position.changed.dispatch()
  154.  
  155. return true;
  156. }
  157.  
  158. // =====================================================
  159. // RELIABLE DIRECTION-AWARE BRIDGE (integrated v0.7.2)
  160. // =====================================================
  161.  
  162. function nearXY(position, endpoint) {
  163. return Math.hypot(
  164. position[0] - endpoint[0],
  165. position[1] - endpoint[1]
  166. ) <= CONFIG.xyTolerance;
  167. }
  168.  
  169. function findEndpoint(position) {
  170. if (!validXYZ(position) || !activeDataset) return null;
  171.  
  172. let closest = null;
  173. let bestDistance = Infinity;
  174.  
  175. for (const jump of datasetData().jumps) {
  176. // Preserve the main script's individual Auto checkboxes.
  177. if (!jump.enabled ||
  178. !validXYZ(jump.pre) ||
  179. !validXYZ(jump.post)) continue;
  180.  
  181. for (const side of ['pre', 'post']) {
  182. const from = jump[side];
  183. if (!sameSlice(position[2], from[2])) continue;
  184. if (!nearXY(position, from)) continue;
  185.  
  186. const distance = Math.hypot(
  187. position[0] - from[0],
  188. position[1] - from[1]
  189. );
  190.  
  191. if (distance < bestDistance) {
  192. bestDistance = distance;
  193. closest = {
  194. jumpId: jump.id,
  195. jumpName: jump.name,
  196. side,
  197. from: [...from],
  198. to: [...jump[side === 'pre' ? 'post' : 'pre']]
  199. };
  200. }
  201. }
  202. }
  203.  
  204. return closest;
  205. }
  206.  
  207. function resetBridge() {
  208. bridgeState = 'ready';
  209. navigationTarget = null;
  210. navigationStarted = 0;
  211. lastWheelTime = 0;
  212. }
  213.  
  214. function setBridgeStatus(message) {
  215. const el = document.querySelector('#ewii-bridge-status');
  216. if (el) el.textContent = message;
  217. }
  218.  
  219. function find2DPanel() {
  220. return [...document.querySelectorAll(
  221. '.neuroglancer-rendered-data-panel'
  222. )].find(panel =>
  223. !panel.classList.contains('nge-3d-panel') &&
  224. panel.getBoundingClientRect().width > 0 &&
  225. panel.getBoundingClientRect().height > 0
  226. ) || null;
  227. }
  228.  
  229. function over2D(event) {
  230. const panel = find2DPanel();
  231. if (!panel) return false;
  232. const r = panel.getBoundingClientRect();
  233. return event.clientX >= r.left && event.clientX <= r.right &&
  234. event.clientY >= r.top && event.clientY <= r.bottom;
  235. }
  236.  
  237. function interceptWheel(event) {
  238. if (event.cancelable) event.preventDefault();
  239. event.stopImmediatePropagation();
  240. }
  241.  
  242. function scrollingTowardDestination(event, endpoint) {
  243. const zDifference = endpoint.to[2] - endpoint.from[2];
  244. // In this EyeWire setup: positive deltaY decreases Z;
  245. // negative deltaY increases Z.
  246. return zDifference < 0
  247. ? event.deltaY > 0
  248. : event.deltaY < 0;
  249. }
  250.  
  251. function onBridgeWheel(event) {
  252. if (!over2D(event)) return;
  253. if (event.target.closest?.('#ewii-tools-root') ||
  254. event.target.closest?.('#ewii-colour-dialog')) return;
  255.  
  256. const now = performance.now();
  257. const quietBefore = lastWheelTime === 0
  258. ? Infinity : now - lastWheelTime;
  259. lastWheelTime = now;
  260.  
  261. if (bridgeState === 'navigating') {
  262. interceptWheel(event);
  263. return;
  264. }
  265.  
  266. if (bridgeState === 'arrival-lock') {
  267. if (quietBefore < CONFIG.gestureQuietMs) {
  268. interceptWheel(event);
  269. return;
  270. }
  271. // Do NOT return: this first fresh wheel event must be
  272. // eligible to trigger the opposite endpoint.
  273. bridgeState = 'ready';
  274. setBridgeStatus('Bridge ready — checking new gesture');
  275. }
  276.  
  277. if (event.ctrlKey || event.shiftKey || event.altKey) return;
  278.  
  279. const state = readState();
  280. if (!state) return;
  281. const key = datasetKey(state);
  282. if (key !== activeDataset) return;
  283.  
  284. const endpoint = findEndpoint(state.position);
  285. if (!endpoint) return;
  286.  
  287. // Allow normal scrolling AWAY from either endpoint.
  288. if (!scrollingTowardDestination(event, endpoint)) return;
  289.  
  290. // Do not jump while a continuous scroll passes the endpoint.
  291. if (quietBefore < CONFIG.gestureQuietMs) return;
  292.  
  293. interceptWheel(event);
  294. bridgeState = 'navigating';
  295. navigationStarted = now;
  296. navigationTarget = [...endpoint.to];
  297.  
  298. setBridgeStatus(
  299. 'Jumping ' + endpoint.side + ' → ' +
  300. (endpoint.side === 'pre' ? 'post' : 'pre')
  301. );
  302.  
  303. console.log('[EWII v0.8] Bridge jump', endpoint.jumpName,
  304. endpoint.side, endpoint.from, '→', endpoint.to);
  305.  
  306. if (!navigateTo(navigationTarget)) {
  307. resetBridge();
  308. setBridgeStatus('Bridge navigation failed');
  309. }
  310. }
  311.  
  312. // Capture before EyeWire handles the wheel event.
  313. window.addEventListener('wheel', onBridgeWheel,
  314. { capture: true, passive: false });
  315.  
  316. function updateBridge(position) {
  317. if (bridgeState !== 'navigating' || !navigationTarget) return;
  318.  
  319. const arrived = position.every((value, index) =>
  320. Math.abs(value - navigationTarget[index]) < 0.1
  321. );
  322.  
  323. if (arrived) {
  324. bridgeState = 'arrival-lock';
  325. setBridgeStatus(
  326. 'Arrived at Z ' + sliceNumber(position[2]) +
  327. ' — release wheel before next jump'
  328. );
  329. } else if (performance.now() - navigationStarted >
  330. CONFIG.navigationTimeoutMs) {
  331. resetBridge();
  332. setBridgeStatus('Bridge timeout — ready');
  333. }
  334. }
  335.  
  336. // =====================================================
  337. // JUMP MANAGEMENT
  338. // =====================================================
  339.  
  340. function saveJumpPair() {
  341. const data = datasetData();
  342.  
  343. if (data.jumps.length >= CONFIG.maxJumps) {
  344. alert('Maximum 20 jump pairs per dataset.');
  345. return;
  346. }
  347.  
  348. const pre = currentXYZ();
  349. if (!pre) {
  350. alert('Viewer coordinates unavailable.');
  351. return;
  352. }
  353.  
  354. const name = prompt(
  355. 'Name this coordinate bridge:',
  356. `Bridge ${data.jumps.length + 1}`
  357. );
  358.  
  359. if (name === null) return;
  360.  
  361. data.jumps.push({
  362. id: makeId(),
  363. name: name.trim() ||
  364. `Bridge ${data.jumps.length + 1}`,
  365. pre,
  366. post: null,
  367. enabled: false
  368. });
  369.  
  370. saveDatabase();
  371. rebuildMenu();
  372.  
  373. alert(
  374. 'Pre coordinates saved.\n' +
  375. 'Move to the continuation after the damaged slices ' +
  376. 'and click Set post here.'
  377. );
  378. }
  379.  
  380. function setPost(id) {
  381. const jump = datasetData().jumps.find(
  382. x => x.id === id
  383. );
  384.  
  385. const position = currentXYZ();
  386. if (!jump || !position) return;
  387.  
  388. jump.post = position;
  389. jump.enabled = false;
  390.  
  391. resetBridge();
  392. saveDatabase();
  393. rebuildMenu();
  394. }
  395.  
  396. function deleteJump(id) {
  397. if (!confirm('Delete this bridge?')) return;
  398.  
  399. const data = datasetData();
  400. data.jumps = data.jumps.filter(x => x.id !== id);
  401.  
  402. resetBridge();
  403. saveDatabase();
  404. rebuildMenu();
  405. }
  406.  
  407. // =====================================================
  408. // LASSO GEOMETRY — PRESERVED FROM v0.5
  409. // =====================================================
  410.  
  411. function geometry() {
  412. const state = readState();
  413. const panel = find2DPanel();
  414.  
  415. if (!state || !panel) return null;
  416.  
  417. const rect = panel.getBoundingClientRect();
  418. const scale = Number(state.crossSectionScale);
  419.  
  420. if (!Number.isFinite(scale) || scale <= 0) {
  421. return null;
  422. }
  423.  
  424. return {
  425. panel,
  426. rect,
  427. position: state.position,
  428. scale,
  429. z: state.position[2]
  430. };
  431. }
  432.  
  433. function physicalScaleXY(state) {
  434. const dims = state.dimensions || {};
  435.  
  436. const x = Array.isArray(dims.x)
  437. ? Number(dims.x[0]) : 1;
  438.  
  439. const y = Array.isArray(dims.y)
  440. ? Number(dims.y[0]) : 1;
  441.  
  442. return {
  443. x: Number.isFinite(x) && x > 0 ? x : 1,
  444. y: Number.isFinite(y) && y > 0 ? y : 1
  445. };
  446. }
  447.  
  448. function physicalPerPixel(g, dimensions) {
  449. const label = g.panel.querySelector(
  450. '.neuroglancer-display-dimensions-widget-scale'
  451. )?.textContent || '';
  452.  
  453. const match = label.match(
  454. /([\d.]+)\s*(pm|nm|µm|um|mm|m)\s*\/\s*px/i
  455. );
  456.  
  457. if (match) {
  458. const units = {
  459. pm: 1e-12,
  460. nm: 1e-9,
  461. 'µm': 1e-6,
  462. um: 1e-6,
  463. mm: 1e-3,
  464. m: 1
  465. };
  466.  
  467. return Number(match[1]) * units[match[2]];
  468. }
  469.  
  470. return dimensions.x * g.scale;
  471. }
  472.  
  473. function screenToData(clientX, clientY, g) {
  474. const state = readState();
  475. if (!state) return null;
  476.  
  477. const dimensions = physicalScaleXY(state);
  478. const perPixel = physicalPerPixel(g, dimensions);
  479.  
  480. const centerX = g.rect.left + g.rect.width / 2;
  481. const centerY = g.rect.top + g.rect.height / 2;
  482.  
  483. return {
  484. x: g.position[0] +
  485. (clientX - centerX) *
  486. perPixel / dimensions.x,
  487. y: g.position[1] +
  488. (clientY - centerY) *
  489. perPixel / dimensions.y
  490. };
  491. }
  492.  
  493. function dataToScreen(point, g) {
  494. const state = readState();
  495. if (!state) return null;
  496.  
  497. const dimensions = physicalScaleXY(state);
  498. const perPixel = physicalPerPixel(g, dimensions);
  499.  
  500. const centerX = g.rect.left + g.rect.width / 2;
  501. const centerY = g.rect.top + g.rect.height / 2;
  502.  
  503. return {
  504. x: centerX +
  505. (point.x - g.position[0]) *
  506. dimensions.x / perPixel,
  507. y: centerY +
  508. (point.y - g.position[1]) *
  509. dimensions.y / perPixel
  510. };
  511. }
  512.  
  513. // =====================================================
  514. // VISUAL COLOUR PICKER
  515. // =====================================================
  516.  
  517. function chooseLassoColour(initialColour) {
  518. return new Promise(resolve => {
  519. const backdrop = document.createElement('div');
  520. backdrop.id = 'ewii-colour-dialog';
  521.  
  522. backdrop.style.cssText = `
  523. position:fixed;
  524. inset:0;
  525. z-index:2147483647;
  526. display:flex;
  527. align-items:center;
  528. justify-content:center;
  529. background:#0008;
  530. font:13px Arial,sans-serif;
  531. `;
  532.  
  533. const dialog = document.createElement('div');
  534. dialog.style.cssText = `
  535. width:370px;
  536. max-width:calc(100vw - 30px);
  537. padding:15px;
  538. background:#f1f1f1;
  539. color:#111;
  540. border:1px solid #999;
  541. border-radius:6px;
  542. box-shadow:0 10px 35px #0008;
  543. box-sizing:border-box;
  544. `;
  545.  
  546. const heading = document.createElement('div');
  547. heading.textContent = 'Color';
  548. heading.style.cssText =
  549. 'font-size:16px;margin-bottom:12px';
  550.  
  551. const caption = document.createElement('div');
  552. caption.textContent = 'Basic colors:';
  553. caption.style.marginBottom = '7px';
  554.  
  555. const palette = document.createElement('div');
  556. palette.style.cssText = `
  557. display:grid;
  558. grid-template-columns:repeat(10,1fr);
  559. gap:5px;
  560. margin-bottom:15px;
  561. `;
  562.  
  563. const colours = [
  564. '#ff0000','#ffff00','#00ff00','#00ffff',
  565. '#0000ff','#ff00ff','#ffffff','#000000',
  566. '#ff8080','#ffff80','#80ff80','#80ffff',
  567. '#8080ff','#ff80ff','#808080','#800000',
  568. '#808000','#008000','#008080','#000080',
  569. '#800080','#ff8000','#80ff00','#00ff80',
  570. '#0080ff','#8000ff','#ff0080','#804000',
  571. '#408000','#008040','#004080','#400080',
  572. '#804080','#c0c0c0','#404040','#202020',
  573. '#ffcc00','#aaff00','#00aaff','#aa00ff'
  574. ];
  575.  
  576. let selected = initialColour || '#ffcc00';
  577.  
  578. const previewRow = document.createElement('div');
  579. previewRow.style.cssText = `
  580. display:flex;
  581. align-items:center;
  582. gap:12px;
  583. margin-bottom:14px;
  584. `;
  585.  
  586. const preview = document.createElement('div');
  587. preview.style.cssText = `
  588. width:75px;
  589. height:48px;
  590. border:1px solid #888;
  591. background:${selected};
  592. flex-shrink:0;
  593. `;
  594.  
  595. const picker = document.createElement('input');
  596. picker.type = 'color';
  597. picker.value = selected;
  598. picker.title = 'Open custom colour picker';
  599. picker.style.cssText = `
  600. width:80px;
  601. height:48px;
  602. cursor:pointer;
  603. `;
  604.  
  605. const label = document.createElement('span');
  606. label.textContent = 'Custom colors';
  607.  
  608. function updateColour(colour) {
  609. selected = colour;
  610. preview.style.background = colour;
  611. picker.value = colour;
  612. }
  613.  
  614. for (const colour of colours) {
  615. const swatch = document.createElement('button');
  616. swatch.type = 'button';
  617. swatch.title = colour;
  618. swatch.style.cssText = `
  619. height:23px;
  620. border:1px solid #888;
  621. border-radius:2px;
  622. background:${colour};
  623. cursor:pointer;
  624. `;
  625.  
  626. swatch.addEventListener('click', () => {
  627. updateColour(colour);
  628. });
  629.  
  630. palette.appendChild(swatch);
  631. }
  632.  
  633. picker.addEventListener('input', () => {
  634. updateColour(picker.value);
  635. });
  636.  
  637. previewRow.append(preview, picker, label);
  638.  
  639. const actions = document.createElement('div');
  640. actions.style.cssText = `
  641. display:flex;
  642. justify-content:flex-end;
  643. gap:8px;
  644. `;
  645.  
  646. let closed = false;
  647.  
  648. function close(value) {
  649. if (closed) return;
  650. closed = true;
  651.  
  652. document.removeEventListener(
  653. 'keydown',
  654. onKeyDown,
  655. true
  656. );
  657.  
  658. backdrop.remove();
  659. resolve(value);
  660. }
  661.  
  662. function onKeyDown(event) {
  663. if (event.key === 'Escape') {
  664. event.preventDefault();
  665. event.stopImmediatePropagation();
  666. close(null);
  667. }
  668. }
  669.  
  670. document.addEventListener(
  671. 'keydown',
  672. onKeyDown,
  673. true
  674. );
  675.  
  676. const ok = document.createElement('button');
  677. ok.textContent = 'OK';
  678. ok.addEventListener('click', () => close(selected));
  679.  
  680. const cancel = document.createElement('button');
  681. cancel.textContent = 'Cancel';
  682. cancel.addEventListener('click', () => close(null));
  683.  
  684. for (const button of [ok, cancel]) {
  685. button.style.cssText = `
  686. padding:7px 22px;
  687. border:1px solid #999;
  688. border-radius:4px;
  689. background:white;
  690. cursor:pointer;
  691. `;
  692. }
  693.  
  694. actions.append(ok, cancel);
  695.  
  696. dialog.append(
  697. heading,
  698. caption,
  699. palette,
  700. previewRow,
  701. actions
  702. );
  703.  
  704. backdrop.appendChild(dialog);
  705. document.body.appendChild(backdrop);
  706. });
  707. }
  708.  
  709. // =====================================================
  710. // COLOUR LASSO
  711. // =====================================================
  712.  
  713. const overlay = document.createElement('canvas');
  714. overlay.id = 'ewii-lasso-overlay';
  715. overlay.style.cssText = `
  716. position:fixed;
  717. z-index:2147482000;
  718. display:none;
  719. pointer-events:none;
  720. touch-action:none;
  721. `;
  722.  
  723. document.body.appendChild(overlay);
  724.  
  725. function setLassoEnabled(value) {
  726. lassoEnabled = Boolean(value);
  727. drawing = false;
  728. drawingPoints = [];
  729.  
  730. overlay.style.pointerEvents =
  731. lassoEnabled ? 'auto' : 'none';
  732.  
  733. overlay.style.cursor =
  734. lassoEnabled ? 'crosshair' : 'default';
  735.  
  736. rebuildMenu();
  737. renderLassos();
  738. }
  739.  
  740. function beginDrawing(event) {
  741. if (!lassoEnabled || event.button !== 0) return;
  742.  
  743. const g = geometry();
  744. if (!g) return;
  745.  
  746. if (
  747. datasetData().lassos.length >=
  748. CONFIG.maxLassos
  749. ) {
  750. alert('Maximum 40 lassos per dataset.');
  751. return;
  752. }
  753.  
  754. const point = screenToData(
  755. event.clientX,
  756. event.clientY,
  757. g
  758. );
  759.  
  760. if (!point) return;
  761.  
  762. event.preventDefault();
  763. event.stopPropagation();
  764.  
  765. drawing = true;
  766. drawingPoints = [point];
  767.  
  768. try {
  769. overlay.setPointerCapture(event.pointerId);
  770. } catch {}
  771.  
  772. renderLassos();
  773. }
  774.  
  775. function moveDrawing(event) {
  776. if (!drawing) return;
  777.  
  778. const g = geometry();
  779. if (!g) return;
  780.  
  781. const point = screenToData(
  782. event.clientX,
  783. event.clientY,
  784. g
  785. );
  786.  
  787. if (!point) return;
  788.  
  789. const previous = drawingPoints.at(-1);
  790.  
  791. if (
  792. !previous ||
  793. Math.hypot(
  794. point.x - previous.x,
  795. point.y - previous.y
  796. ) > 0.3
  797. ) {
  798. drawingPoints.push(point);
  799. }
  800.  
  801. renderLassos();
  802. }
  803.  
  804. async function finishDrawing(event) {
  805. if (!drawing) return;
  806.  
  807. drawing = false;
  808.  
  809. try {
  810. overlay.releasePointerCapture(event.pointerId);
  811. } catch {}
  812.  
  813. if (drawingPoints.length < 3) {
  814. drawingPoints = [];
  815. renderLassos();
  816. return;
  817. }
  818.  
  819. const completedPoints = drawingPoints.map(
  820. point => ({ x: point.x, y: point.y })
  821. );
  822.  
  823. const g = geometry();
  824.  
  825. if (!g) {
  826. drawingPoints = [];
  827. renderLassos();
  828. return;
  829. }
  830.  
  831. const drawingZ = g.z;
  832. const drawingDataset = activeDataset;
  833.  
  834. const colour = await chooseLassoColour(
  835. selectedColour
  836. );
  837.  
  838. if (colour === null) {
  839. drawingPoints = [];
  840. renderLassos();
  841. return;
  842. }
  843.  
  844. selectedColour = colour;
  845.  
  846. if (!database[drawingDataset]) {
  847. database[drawingDataset] = {
  848. jumps: [],
  849. lassos: []
  850. };
  851. }
  852.  
  853. const data = database[drawingDataset];
  854.  
  855. if (!Array.isArray(data.lassos)) {
  856. data.lassos = [];
  857. }
  858.  
  859. if (data.lassos.length >= CONFIG.maxLassos) {
  860. alert('Maximum 40 lassos per dataset.');
  861. drawingPoints = [];
  862. return;
  863. }
  864.  
  865. data.lassos.push({
  866. id: makeId(),
  867. z: drawingZ,
  868. colour: selectedColour,
  869. points: completedPoints,
  870. created: new Date().toISOString()
  871. });
  872.  
  873. drawingPoints = [];
  874.  
  875. saveDatabase();
  876. rebuildMenu();
  877. renderLassos();
  878. }
  879.  
  880. overlay.addEventListener('pointerdown', beginDrawing);
  881. overlay.addEventListener('pointermove', moveDrawing);
  882. overlay.addEventListener('pointerup', finishDrawing);
  883.  
  884. overlay.addEventListener('pointercancel', () => {
  885. drawing = false;
  886. drawingPoints = [];
  887. renderLassos();
  888. });
  889.  
  890. function drawPolygon(ctx, points, colour, g, fill) {
  891. if (points.length < 2) return;
  892.  
  893. ctx.beginPath();
  894.  
  895. let started = false;
  896.  
  897. for (const point of points) {
  898. const p = dataToScreen(point, g);
  899. if (!p) continue;
  900.  
  901. const x = p.x - g.rect.left;
  902. const y = p.y - g.rect.top;
  903.  
  904. if (!started) {
  905. ctx.moveTo(x, y);
  906. started = true;
  907. } else {
  908. ctx.lineTo(x, y);
  909. }
  910. }
  911.  
  912. if (!started) return;
  913.  
  914. if (fill) ctx.closePath();
  915.  
  916. ctx.strokeStyle = colour;
  917. ctx.lineWidth = 2;
  918. ctx.stroke();
  919.  
  920. if (fill) {
  921. ctx.fillStyle = colour;
  922. ctx.globalAlpha = CONFIG.opacity;
  923. ctx.fill();
  924. ctx.globalAlpha = 1;
  925. }
  926. }
  927.  
  928. function renderLassos() {
  929. const g = geometry();
  930.  
  931. if (!g) {
  932. overlay.style.display = 'none';
  933. return;
  934. }
  935.  
  936. overlay.style.display = 'block';
  937. overlay.style.left = g.rect.left + 'px';
  938. overlay.style.top = g.rect.top + 'px';
  939. overlay.style.width = g.rect.width + 'px';
  940. overlay.style.height = g.rect.height + 'px';
  941.  
  942. const dpr = window.devicePixelRatio || 1;
  943.  
  944. const width = Math.max(
  945. 1, Math.round(g.rect.width * dpr)
  946. );
  947.  
  948. const height = Math.max(
  949. 1, Math.round(g.rect.height * dpr)
  950. );
  951.  
  952. if (overlay.width !== width) overlay.width = width;
  953. if (overlay.height !== height) overlay.height = height;
  954.  
  955. const ctx = overlay.getContext('2d');
  956. ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  957. ctx.clearRect(0, 0, g.rect.width, g.rect.height);
  958.  
  959. for (const lasso of datasetData().lassos) {
  960. if (!sameSlice(lasso.z, g.z)) continue;
  961.  
  962. drawPolygon(
  963. ctx, lasso.points, lasso.colour, g, true
  964. );
  965. }
  966.  
  967. if (drawing && drawingPoints.length) {
  968. drawPolygon(
  969. ctx, drawingPoints, selectedColour, g, false
  970. );
  971. }
  972. }
  973.  
  974. function deleteLasso(id) {
  975. if (!confirm('Delete this colour lasso?')) return;
  976.  
  977. const data = datasetData();
  978. data.lassos = data.lassos.filter(x => x.id !== id);
  979.  
  980. saveDatabase();
  981. rebuildMenu();
  982. renderLassos();
  983. }
  984.  
  985. // =====================================================
  986. // MENU
  987. // =====================================================
  988.  
  989. const root = document.createElement('div');
  990. root.id = 'ewii-tools-root';
  991. root.style.cssText = `
  992. position:fixed;
  993. z-index:2147483000;
  994. display:none;
  995. font:12px Arial,sans-serif;
  996. color:#e9f5ff;
  997. `;
  998.  
  999. const menu = document.createElement('div');
  1000. menu.style.cssText = `
  1001. position:absolute;
  1002. top:29px;
  1003. right:0;
  1004. width:315px;
  1005. max-height:75vh;
  1006. overflow:auto;
  1007. background:#102332;
  1008. border:1px solid #4b9fc0;
  1009. border-radius:8px;
  1010. padding:12px;
  1011. box-shadow:0 8px 24px #0009;
  1012. display:none;
  1013. box-sizing:border-box;
  1014. `;
  1015.  
  1016. const trigger = document.createElement('button');
  1017. trigger.textContent = 'Tools ▾';
  1018. trigger.title = 'EyeWire coordinate bridges and lasso';
  1019. trigger.style.cssText = `
  1020. background:#124d70;
  1021. color:white;
  1022. border:1px solid #54a9cb;
  1023. border-radius:5px;
  1024. padding:5px 8px;
  1025. cursor:pointer;
  1026. white-space:nowrap;
  1027. font:12px Arial,sans-serif;
  1028. `;
  1029.  
  1030. document.body.appendChild(root);
  1031. root.append(trigger, menu);
  1032.  
  1033. function makeButton(text, callback, colour = '#195777') {
  1034. const button = document.createElement('button');
  1035. button.textContent = text;
  1036. button.style.cssText = `
  1037. border:1px solid #4e91ab;
  1038. background:${colour};
  1039. color:white;
  1040. padding:5px 7px;
  1041. border-radius:4px;
  1042. cursor:pointer;
  1043. font:12px Arial,sans-serif;
  1044. `;
  1045.  
  1046. button.addEventListener('click', callback);
  1047. return button;
  1048. }
  1049.  
  1050. function makeSection(title) {
  1051. const section = document.createElement('div');
  1052. section.style.cssText = `
  1053. border-top:1px solid #385568;
  1054. margin-top:11px;
  1055. padding-top:10px;
  1056. `;
  1057.  
  1058. const heading = document.createElement('div');
  1059. heading.textContent = title;
  1060. heading.style.cssText =
  1061. 'font-weight:bold;margin-bottom:8px;font-size:13px';
  1062.  
  1063. section.appendChild(heading);
  1064. return section;
  1065. }
  1066.  
  1067. function makeRow() {
  1068. const row = document.createElement('div');
  1069. row.style.cssText = `
  1070. display:flex;
  1071. flex-wrap:wrap;
  1072. gap:5px;
  1073. align-items:center;
  1074. margin-bottom:7px;
  1075. `;
  1076. return row;
  1077. }
  1078.  
  1079. // =====================================================
  1080. // 2D SLIDE SCREENSHOTS — v0.9
  1081. // =====================================================
  1082. // Isolated from the existing bridge and lasso logic.
  1083.  
  1084. const slideTools = (() => {
  1085. const shots = [null, null];
  1086. let busy = false;
  1087. let status = 'Ready — capture Slide 1 or Slide 2.';
  1088. let statusEl = null;
  1089. let previewDialog = null;
  1090.  
  1091. const id = 'ewii-slide-screenshots-section';
  1092. const sleep = ms =>
  1093. new Promise(resolve => setTimeout(resolve, ms));
  1094.  
  1095. function panel2D() {
  1096. return [...document.querySelectorAll(
  1097. '.neuroglancer-rendered-data-panel'
  1098. )].find(el =>
  1099. !el.classList.contains('nge-3d-panel') &&
  1100. el.getBoundingClientRect().width > 0 &&
  1101. el.getBoundingClientRect().height > 0
  1102. );
  1103. }
  1104.  
  1105. function setStatus(message) {
  1106. status = message;
  1107.  
  1108. if (statusEl?.isConnected) {
  1109. statusEl.textContent = message;
  1110. }
  1111.  
  1112. console.log('[EWII 2D Slides]', message);
  1113. }
  1114.  
  1115. function screenshotButton() {
  1116. return document.querySelector(
  1117. 'button[title="Take a screenshot. Drag to reorder."]'
  1118. ) || [...document.querySelectorAll('button')].find(
  1119. button => /take a screenshot/i.test(
  1120. button.title ||
  1121. button.getAttribute('aria-label') ||
  1122. ''
  1123. )
  1124. );
  1125. }
  1126.  
  1127. function closeNativeDialog() {
  1128. const dialog = document.querySelector(
  1129. '.nge-shotdlg-stage'
  1130. )?.closest('[role="dialog"]') ||
  1131. document.querySelector(
  1132. '.nge-shotdlg-stage'
  1133. )?.parentElement?.parentElement;
  1134.  
  1135. if (!dialog) return;
  1136.  
  1137. const buttons = [
  1138. ...dialog.querySelectorAll('button')
  1139. ];
  1140.  
  1141. const cancel = buttons.find(
  1142. button => /^cancel$/i.test(
  1143. button.textContent.trim()
  1144. )
  1145. );
  1146.  
  1147. if (cancel) cancel.click();
  1148. }
  1149.  
  1150. function downloadCanvas(canvas, filename) {
  1151. canvas.toBlob(blob => {
  1152. if (!blob) {
  1153. setStatus('JPG export failed.');
  1154. return;
  1155. }
  1156.  
  1157. const url = URL.createObjectURL(blob);
  1158. const link = document.createElement('a');
  1159.  
  1160. link.href = url;
  1161. link.download = filename;
  1162.  
  1163. document.body.appendChild(link);
  1164. link.click();
  1165. link.remove();
  1166.  
  1167. setTimeout(() => URL.revokeObjectURL(url), 10000);
  1168. }, 'image/jpeg', 0.95);
  1169. }
  1170.  
  1171. function canvasButton(label, onClick) {
  1172. const button = document.createElement('button');
  1173.  
  1174. button.textContent = label;
  1175. button.type = 'button';
  1176.  
  1177. button.style.cssText =
  1178. 'background:#195777;color:#fff;' +
  1179. 'border:1px solid #4e91ab;' +
  1180. 'border-radius:4px;padding:6px 8px;' +
  1181. 'cursor:pointer;font:12px Arial,sans-serif';
  1182.  
  1183. button.addEventListener('click', onClick);
  1184. return button;
  1185. }
  1186.  
  1187. function compose(source, geometry, lassoCopy) {
  1188. const {
  1189. panelRect,
  1190. viewportHeight,
  1191. viewportWidth,
  1192. lassoRect
  1193. } = geometry;
  1194.  
  1195. // EyeWire's screenshot source is a full-resolution
  1196. // composite with its top toolbar excluded.
  1197. const sourceOffsetY = Math.max(
  1198. 0,
  1199. viewportHeight - source.height
  1200. );
  1201.  
  1202. const scaleX = source.width / viewportWidth;
  1203. const scaleY = scaleX;
  1204.  
  1205. const sx = Math.round(panelRect.x * scaleX);
  1206. const sy = Math.round(
  1207. (panelRect.y - sourceOffsetY) * scaleY
  1208. );
  1209.  
  1210. const sw = Math.round(
  1211. panelRect.width * scaleX
  1212. );
  1213.  
  1214. const sh = Math.round(
  1215. panelRect.height * scaleY
  1216. );
  1217.  
  1218. if (
  1219. sw < 10 ||
  1220. sh < 10 ||
  1221. sx < 0 ||
  1222. sy < 0 ||
  1223. sx + sw > source.width + 2 ||
  1224. sy + sh > source.height + 2
  1225. ) {
  1226. throw new Error(
  1227. 'Screenshot source/panel geometry mismatch: ' +
  1228. JSON.stringify({
  1229. source: [source.width, source.height],
  1230. sx, sy, sw, sh
  1231. })
  1232. );
  1233. }
  1234.  
  1235. const output = document.createElement('canvas');
  1236.  
  1237. output.width = Math.min(
  1238. sw,
  1239. source.width - sx
  1240. );
  1241.  
  1242. output.height = Math.min(
  1243. sh,
  1244. source.height - sy
  1245. );
  1246.  
  1247. const ctx = output.getContext('2d');
  1248.  
  1249. ctx.fillStyle = '#000';
  1250. ctx.fillRect(
  1251. 0, 0,
  1252. output.width,
  1253. output.height
  1254. );
  1255.  
  1256. ctx.drawImage(
  1257. source,
  1258. sx, sy,
  1259. output.width, output.height,
  1260. 0, 0,
  1261. output.width, output.height
  1262. );
  1263.  
  1264. // Composite the existing transparent lasso canvas.
  1265. // No changes are made to saved lasso coordinates.
  1266. if (
  1267. lassoCopy &&
  1268. lassoRect?.width &&
  1269. lassoRect?.height
  1270. ) {
  1271. const dx =
  1272. (lassoRect.x - panelRect.x) /
  1273. panelRect.width * output.width;
  1274.  
  1275. const dy =
  1276. (lassoRect.y - panelRect.y) /
  1277. panelRect.height * output.height;
  1278.  
  1279. const dw =
  1280. lassoRect.width /
  1281. panelRect.width * output.width;
  1282.  
  1283. const dh =
  1284. lassoRect.height /
  1285. panelRect.height * output.height;
  1286.  
  1287. ctx.drawImage(
  1288. lassoCopy,
  1289. dx, dy, dw, dh
  1290. );
  1291. }
  1292.  
  1293. return output;
  1294. }
  1295.  
  1296. async function capture(slot) {
  1297. if (busy) {
  1298. setStatus('A capture is already in progress.');
  1299. return;
  1300. }
  1301.  
  1302. if (document.querySelector(
  1303. '.nge-shotdlg-preview'
  1304. )) {
  1305. setStatus(
  1306. 'Close the native EyeWire screenshot popup first.'
  1307. );
  1308. return;
  1309. }
  1310.  
  1311. const panel = panel2D();
  1312. const button = screenshotButton();
  1313.  
  1314. if (!panel || !button) {
  1315. setStatus(
  1316. '2D panel or native screenshot button not found.'
  1317. );
  1318. return;
  1319. }
  1320.  
  1321. const lasso = document.querySelector(
  1322. '#ewii-lasso-overlay'
  1323. );
  1324.  
  1325. const panelRect = panel.getBoundingClientRect();
  1326. const lassoRect = lasso?.getBoundingClientRect();
  1327.  
  1328. const geometry = {
  1329. panelRect: {
  1330. x: panelRect.x,
  1331. y: panelRect.y,
  1332. width: panelRect.width,
  1333. height: panelRect.height
  1334. },
  1335. lassoRect: lassoRect ? {
  1336. x: lassoRect.x,
  1337. y: lassoRect.y,
  1338. width: lassoRect.width,
  1339. height: lassoRect.height
  1340. } : null,
  1341. viewportWidth: innerWidth,
  1342. viewportHeight: innerHeight
  1343. };
  1344.  
  1345. // Copy the lasso before the native dialog opens.
  1346. let lassoCopy = null;
  1347.  
  1348. if (lasso?.width && lasso?.height) {
  1349. lassoCopy = document.createElement('canvas');
  1350. lassoCopy.width = lasso.width;
  1351. lassoCopy.height = lasso.height;
  1352.  
  1353. lassoCopy.getContext('2d').drawImage(
  1354. lasso, 0, 0
  1355. );
  1356. }
  1357.  
  1358. busy = true;
  1359. setStatus(
  1360. 'Capturing Slide ' + (slot + 1) + '…'
  1361. );
  1362.  
  1363. const proto = CanvasRenderingContext2D.prototype;
  1364. const original = proto.drawImage;
  1365.  
  1366. let finished = false;
  1367. let timer;
  1368. let resolveCapture;
  1369.  
  1370. const result = new Promise(resolve => {
  1371. resolveCapture = resolve;
  1372. });
  1373.  
  1374. function finish(error, canvas) {
  1375. if (finished) return;
  1376.  
  1377. finished = true;
  1378. clearTimeout(timer);
  1379.  
  1380. if (proto.drawImage === wrapped) {
  1381. proto.drawImage = original;
  1382. }
  1383.  
  1384. resolveCapture({ error, canvas });
  1385. }
  1386.  
  1387. function wrapped(...args) {
  1388. // Let EyeWire draw normally first.
  1389. const result = original.apply(this, args);
  1390.  
  1391. if (
  1392. !finished &&
  1393. this.canvas?.classList?.contains(
  1394. 'nge-shotdlg-preview'
  1395. ) &&
  1396. args[0] instanceof HTMLCanvasElement
  1397. ) {
  1398. try {
  1399. // This is the full-resolution source canvas
  1400. // confirmed by the Firefox diagnostic.
  1401. const image = compose(
  1402. args[0],
  1403. geometry,
  1404. lassoCopy
  1405. );
  1406.  
  1407. finish(null, image);
  1408. } catch (error) {
  1409. finish(error, null);
  1410. }
  1411. }
  1412.  
  1413. return result;
  1414. }
  1415.  
  1416. proto.drawImage = wrapped;
  1417.  
  1418. timer = setTimeout(
  1419. () => finish(
  1420. new Error(
  1421. 'Timed out waiting for native capture.'
  1422. ),
  1423. null
  1424. ),
  1425. 8000
  1426. );
  1427.  
  1428. try {
  1429. button.click();
  1430. } catch (error) {
  1431. finish(error, null);
  1432. }
  1433.  
  1434. const { error, canvas } = await result;
  1435. busy = false;
  1436.  
  1437. if (error) {
  1438. setStatus(
  1439. 'Capture failed: ' + error.message
  1440. );
  1441.  
  1442. console.error(
  1443. '[EWII 2D Slides]',
  1444. error
  1445. );
  1446.  
  1447. return;
  1448. }
  1449.  
  1450. shots[slot] = {
  1451. canvas,
  1452. time: new Date(),
  1453. z: currentXYZ()?.[2] ?? null
  1454. };
  1455.  
  1456. setStatus(
  1457. 'Slide ' + (slot + 1) +
  1458. ' captured (' +
  1459. canvas.width + ' × ' +
  1460. canvas.height + ').'
  1461. );
  1462.  
  1463. await sleep(120);
  1464. closeNativeDialog();
  1465.  
  1466. if (previewDialog?.isConnected) {
  1467. refreshPreview();
  1468. }
  1469. }
  1470.  
  1471. function combinedCanvas() {
  1472. const available = shots.filter(Boolean);
  1473.  
  1474. if (!available.length) return null;
  1475.  
  1476. const gap = 20;
  1477.  
  1478. const height = Math.max(
  1479. ...available.map(
  1480. shot => shot.canvas.height
  1481. )
  1482. );
  1483.  
  1484. const width = available.reduce(
  1485. (sum, shot) => sum + shot.canvas.width,
  1486. 0
  1487. ) + gap * (available.length - 1);
  1488.  
  1489. const output = document.createElement('canvas');
  1490.  
  1491. output.width = width;
  1492. output.height = height;
  1493.  
  1494. const ctx = output.getContext('2d');
  1495.  
  1496. ctx.fillStyle = '#ffffff';
  1497. ctx.fillRect(
  1498. 0, 0,
  1499. width, height
  1500. );
  1501.  
  1502. let x = 0;
  1503.  
  1504. for (const shot of available) {
  1505. ctx.drawImage(
  1506. shot.canvas,
  1507. x, 0
  1508. );
  1509.  
  1510. x += shot.canvas.width + gap;
  1511. }
  1512.  
  1513. return output;
  1514. }
  1515.  
  1516. function refreshPreview() {
  1517. if (!previewDialog) return;
  1518.  
  1519. const content = previewDialog.querySelector(
  1520. '[data-ewii-slide-content]'
  1521. );
  1522.  
  1523. if (!content) return;
  1524.  
  1525. content.replaceChildren();
  1526.  
  1527. for (let i = 0; i < 2; i++) {
  1528. const col = document.createElement('div');
  1529.  
  1530. col.style.cssText =
  1531. 'flex:1;min-width:0;text-align:center';
  1532.  
  1533. const heading = document.createElement('div');
  1534.  
  1535. heading.textContent =
  1536. 'Slide ' + (i + 1) +
  1537. (shots[i]?.z != null
  1538. ? ' — Z ' + shots[i].z
  1539. : '');
  1540.  
  1541. heading.style.cssText =
  1542. 'margin-bottom:8px;font-weight:bold';
  1543.  
  1544. col.append(heading);
  1545.  
  1546. if (shots[i]) {
  1547. const img = document.createElement('img');
  1548.  
  1549. img.src = shots[i].canvas.toDataURL(
  1550. 'image/png'
  1551. );
  1552.  
  1553. img.style.cssText =
  1554. 'display:block;width:100%;height:auto;' +
  1555. 'object-fit:contain;' +
  1556. 'border:1px solid #42657d;' +
  1557. 'box-sizing:border-box';
  1558.  
  1559. col.append(img);
  1560. } else {
  1561. const placeholder =
  1562. document.createElement('div');
  1563.  
  1564. placeholder.textContent =
  1565. 'Not captured yet';
  1566.  
  1567. placeholder.style.cssText =
  1568. 'padding:70px 5px;' +
  1569. 'border:1px dashed #42657d';
  1570.  
  1571. col.append(placeholder);
  1572. }
  1573.  
  1574. content.append(col);
  1575. }
  1576. }
  1577.  
  1578. function showPreview() {
  1579. if (previewDialog?.isConnected) {
  1580. refreshPreview();
  1581. return;
  1582. }
  1583.  
  1584. const modal = document.createElement('div');
  1585. modal.id = 'ewii-slide-preview';
  1586.  
  1587. modal.style.cssText =
  1588. 'position:fixed;inset:0;' +
  1589. 'z-index:2147483646;' +
  1590. 'background:#000b;' +
  1591. 'display:flex;' +
  1592. 'align-items:center;' +
  1593. 'justify-content:center;' +
  1594. 'font:13px Arial,sans-serif;' +
  1595. 'color:#e9f5ff';
  1596.  
  1597. const card = document.createElement('div');
  1598.  
  1599. card.style.cssText =
  1600. 'background:#102332;' +
  1601. 'border:1px solid #4b9fc0;' +
  1602. 'border-radius:9px;' +
  1603. 'padding:16px;' +
  1604. 'width:min(94vw,1500px);' +
  1605. 'max-height:90vh;' +
  1606. 'overflow:auto;' +
  1607. 'box-shadow:0 12px 40px #000a';
  1608.  
  1609. const title = document.createElement('h3');
  1610.  
  1611. title.textContent =
  1612. '2D Slide Screenshots';
  1613.  
  1614. title.style.cssText =
  1615. 'margin:0 0 12px';
  1616.  
  1617. const content = document.createElement('div');
  1618.  
  1619. content.dataset.ewiiSlideContent = '1';
  1620.  
  1621. content.style.cssText =
  1622. 'display:flex;' +
  1623. 'gap:12px;' +
  1624. 'align-items:flex-start';
  1625.  
  1626. const actions = document.createElement('div');
  1627.  
  1628. actions.style.cssText =
  1629. 'display:flex;' +
  1630. 'flex-wrap:wrap;' +
  1631. 'gap:8px;' +
  1632. 'margin-top:12px';
  1633.  
  1634. actions.append(
  1635. canvasButton(
  1636. 'Capture Slide 1',
  1637. () => {
  1638. modal.remove();
  1639. previewDialog = null;
  1640. capture(0);
  1641. }
  1642. ),
  1643. canvasButton(
  1644. 'Capture Slide 2',
  1645. () => {
  1646. modal.remove();
  1647. previewDialog = null;
  1648. capture(1);
  1649. }
  1650. ),
  1651. canvasButton(
  1652. 'Download combined JPG',
  1653. () => {
  1654. const canvas = combinedCanvas();
  1655.  
  1656. if (!canvas) {
  1657. alert(
  1658. 'Capture at least one slide first.'
  1659. );
  1660. return;
  1661. }
  1662.  
  1663. downloadCanvas(
  1664. canvas,
  1665. 'eyewire-2d-slides.jpg'
  1666. );
  1667. }
  1668. ),
  1669. canvasButton(
  1670. 'Close',
  1671. () => {
  1672. modal.remove();
  1673. previewDialog = null;
  1674. }
  1675. )
  1676. );
  1677.  
  1678. card.append(
  1679. title,
  1680. content,
  1681. actions
  1682. );
  1683.  
  1684. modal.append(card);
  1685.  
  1686. modal.addEventListener('click', event => {
  1687. if (event.target === modal) {
  1688. modal.remove();
  1689. previewDialog = null;
  1690. }
  1691. });
  1692.  
  1693. document.body.append(modal);
  1694.  
  1695. previewDialog = modal;
  1696. refreshPreview();
  1697. }
  1698.  
  1699. function makeSlideSection() {
  1700. const section = document.createElement('div');
  1701.  
  1702. section.id = id;
  1703.  
  1704. section.style.cssText =
  1705. 'border-top:1px solid #385568;' +
  1706. 'margin-top:11px;' +
  1707. 'padding-top:10px';
  1708.  
  1709. const heading = document.createElement('div');
  1710.  
  1711. heading.textContent =
  1712. '2D Slide Screenshots';
  1713.  
  1714. heading.style.cssText =
  1715. 'font-weight:bold;' +
  1716. 'margin-bottom:8px;' +
  1717. 'font-size:13px';
  1718.  
  1719. const row = document.createElement('div');
  1720.  
  1721. row.style.cssText =
  1722. 'display:flex;' +
  1723. 'flex-wrap:wrap;' +
  1724. 'gap:5px;' +
  1725. 'margin-bottom:7px';
  1726.  
  1727. row.append(
  1728. canvasButton(
  1729. 'Capture Slide 1',
  1730. () => capture(0)
  1731. ),
  1732. canvasButton(
  1733. 'Capture Slide 2',
  1734. () => capture(1)
  1735. ),
  1736. canvasButton(
  1737. 'Preview',
  1738. showPreview
  1739. ),
  1740. canvasButton(
  1741. 'Download JPG',
  1742. () => {
  1743. const canvas = combinedCanvas();
  1744.  
  1745. if (!canvas) {
  1746. setStatus(
  1747. 'Capture a slide first.'
  1748. );
  1749. return;
  1750. }
  1751.  
  1752. downloadCanvas(
  1753. canvas,
  1754. 'eyewire-2d-slides.jpg'
  1755. );
  1756. }
  1757. )
  1758. );
  1759.  
  1760. const info = document.createElement('div');
  1761.  
  1762. info.style.cssText =
  1763. 'font-size:11px;' +
  1764. 'color:#b5d6e7;' +
  1765. 'margin-top:5px';
  1766.  
  1767. info.textContent = status;
  1768. statusEl = info;
  1769.  
  1770. section.append(
  1771. heading,
  1772. row,
  1773. info
  1774. );
  1775.  
  1776. return section;
  1777. }
  1778.  
  1779. return {
  1780. makeSection: makeSlideSection
  1781. };
  1782. })();
  1783.  
  1784. // =====================================================
  1785. // MENU CONTENT
  1786. // =====================================================
  1787.  
  1788. function rebuildMenu() {
  1789. if (!activeDataset) return;
  1790.  
  1791. menu.replaceChildren();
  1792.  
  1793. const data = datasetData();
  1794.  
  1795. const title = document.createElement('div');
  1796. title.textContent = 'EyeWire II Tools';
  1797. title.style.cssText =
  1798. 'font-size:16px;font-weight:bold;margin-bottom:6px';
  1799.  
  1800. menu.append(title);
  1801.  
  1802. const position = currentXYZ();
  1803. const coordinates = document.createElement('div');
  1804.  
  1805. coordinates.textContent = position
  1806. ? 'XYZ: ' + position.map(x => x.toFixed(2)).join(', ')
  1807. : 'Coordinates unavailable';
  1808.  
  1809. coordinates.style.cssText =
  1810. 'font-size:11px;color:#b5d6e7;overflow-wrap:anywhere';
  1811.  
  1812. menu.append(coordinates);
  1813.  
  1814. const status = document.createElement('div');
  1815. status.id = 'ewii-bridge-status';
  1816. status.textContent = 'Bridge: ' + bridgeState;
  1817. status.style.cssText =
  1818. 'font-size:11px;color:#88e3c0;margin-top:6px';
  1819.  
  1820. menu.append(status);
  1821.  
  1822. const jumpSection = makeSection(
  1823. `Coordinate bridges (${data.jumps.length}/${CONFIG.maxJumps})`
  1824. );
  1825.  
  1826. jumpSection.append(
  1827. makeButton('+ Save pre-jump here', saveJumpPair)
  1828. );
  1829.  
  1830. data.jumps.forEach(jump => {
  1831. const item = document.createElement('div');
  1832. item.style.cssText = `
  1833. padding:7px;
  1834. margin-top:7px;
  1835. background:#183747;
  1836. border-radius:5px;
  1837. `;
  1838.  
  1839. const name = document.createElement('div');
  1840. name.textContent = jump.name;
  1841. name.style.fontWeight = 'bold';
  1842.  
  1843. const info = document.createElement('div');
  1844. info.style.cssText =
  1845. 'font-size:11px;color:#bcd6e5;margin:5px 0';
  1846.  
  1847. info.textContent =
  1848. `Pre Z: ${jump.pre?.[2] ?? '?'} | ` +
  1849. `Post Z: ${jump.post?.[2] ?? 'Not set'}`;
  1850.  
  1851. const actions = makeRow();
  1852.  
  1853. if (!validXYZ(jump.post)) {
  1854. actions.append(
  1855. makeButton(
  1856. 'Set post here',
  1857. () => setPost(jump.id)
  1858. )
  1859. );
  1860. } else {
  1861. actions.append(
  1862. makeButton(
  1863. 'Go pre',
  1864. () => {
  1865. resetBridge();
  1866. navigateTo(jump.pre);
  1867. }
  1868. ),
  1869. makeButton(
  1870. 'Go post',
  1871. () => {
  1872. resetBridge();
  1873. navigateTo(jump.post);
  1874. }
  1875. )
  1876. );
  1877. }
  1878.  
  1879. const enabled = document.createElement('label');
  1880. enabled.style.cssText =
  1881. 'display:flex;align-items:center;gap:3px';
  1882.  
  1883. const checkbox = document.createElement('input');
  1884. checkbox.type = 'checkbox';
  1885. checkbox.checked = Boolean(jump.enabled);
  1886. checkbox.disabled = !validXYZ(jump.post);
  1887.  
  1888. checkbox.addEventListener('change', () => {
  1889. jump.enabled = checkbox.checked;
  1890. resetBridge();
  1891. saveDatabase();
  1892. });
  1893.  
  1894. enabled.append(checkbox, 'Auto');
  1895. actions.append(enabled);
  1896.  
  1897. actions.append(
  1898. makeButton(
  1899. 'Delete',
  1900. () => deleteJump(jump.id),
  1901. '#823d49'
  1902. )
  1903. );
  1904.  
  1905. item.append(name, info, actions);
  1906. jumpSection.append(item);
  1907. });
  1908.  
  1909. menu.append(jumpSection);
  1910.  
  1911. const lassoSection = makeSection(
  1912. `Colour lassos (${data.lassos.length}/${CONFIG.maxLassos})`
  1913. );
  1914.  
  1915. const toggleRow = makeRow();
  1916.  
  1917. toggleRow.append(
  1918. makeButton(
  1919. lassoEnabled
  1920. ? 'Lasso ON — disable'
  1921. : 'Enable lasso',
  1922. () => setLassoEnabled(!lassoEnabled),
  1923. lassoEnabled ? '#177b55' : '#195777'
  1924. )
  1925. );
  1926.  
  1927. lassoSection.append(toggleRow);
  1928.  
  1929. const note = document.createElement('div');
  1930. note.textContent =
  1931. 'Enable lasso, left-drag on the 2D image, ' +
  1932. 'then choose a colour from the visual picker.';
  1933. note.style.cssText =
  1934. 'font-size:11px;color:#b5d6e7;margin-bottom:8px';
  1935.  
  1936. lassoSection.append(note);
  1937.  
  1938. data.lassos.forEach((lasso, index) => {
  1939. const row = makeRow();
  1940.  
  1941. const swatch = document.createElement('span');
  1942. swatch.style.cssText = `
  1943. width:13px;
  1944. height:13px;
  1945. background:${lasso.colour};
  1946. border:1px solid white;
  1947. display:inline-block;
  1948. `;
  1949.  
  1950. const label = document.createElement('span');
  1951. label.textContent = `#${index + 1} — Z ${lasso.z}`;
  1952.  
  1953. row.append(
  1954. swatch,
  1955. label,
  1956. makeButton(
  1957. 'Delete',
  1958. () => deleteLasso(lasso.id),
  1959. '#823d49'
  1960. )
  1961. );
  1962.  
  1963. lassoSection.append(row);
  1964. });
  1965.  
  1966. lassoSection.append(
  1967. makeButton(
  1968. 'Reset all lassos for this dataset',
  1969. () => {
  1970. if (!confirm(
  1971. 'Delete all saved colour lassos for this dataset?'
  1972. )) return;
  1973.  
  1974. data.lassos = [];
  1975. saveDatabase();
  1976. rebuildMenu();
  1977. renderLassos();
  1978. },
  1979. '#823d49'
  1980. )
  1981. );
  1982.  
  1983. menu.append(lassoSection);
  1984.  
  1985. // Third section: 2D Slide Screenshots.
  1986. menu.append(slideTools.makeSection());
  1987.  
  1988. const footer = makeSection('Dataset data');
  1989.  
  1990. const datasetLabel = document.createElement('div');
  1991. datasetLabel.textContent = activeDataset;
  1992. datasetLabel.style.cssText = `
  1993. font-size:10px;
  1994. color:#a8cadb;
  1995. overflow-wrap:anywhere;
  1996. margin-bottom:8px;
  1997. `;
  1998.  
  1999. footer.append(datasetLabel);
  2000.  
  2001. footer.append(
  2002. makeButton(
  2003. 'Reset all jumps for this dataset',
  2004. () => {
  2005. if (!confirm(
  2006. 'Delete all coordinate bridges for this dataset?'
  2007. )) return;
  2008.  
  2009. data.jumps = [];
  2010. resetBridge();
  2011. saveDatabase();
  2012. rebuildMenu();
  2013. },
  2014. '#823d49'
  2015. )
  2016. );
  2017.  
  2018. menu.append(footer);
  2019. }
  2020.  
  2021. trigger.addEventListener('click', () => {
  2022. menuOpen = !menuOpen;
  2023. menu.style.display = menuOpen ? 'block' : 'none';
  2024. if (menuOpen) rebuildMenu();
  2025. });
  2026.  
  2027. root.addEventListener('mouseenter', () => {
  2028. menuOpen = true;
  2029. menu.style.display = 'block';
  2030. rebuildMenu();
  2031. });
  2032.  
  2033. root.addEventListener('mouseleave', () => {
  2034. menuOpen = false;
  2035. menu.style.display = 'none';
  2036. });
  2037.  
  2038. function visibleButton(selector) {
  2039. return [...document.querySelectorAll(selector)]
  2040. .find(button => {
  2041. const rect = button.getBoundingClientRect();
  2042. return rect.width > 0 && rect.height > 0;
  2043. });
  2044. }
  2045.  
  2046. function positionMenu() {
  2047. const layout = document.body.dataset.ngeLayout;
  2048.  
  2049. if (layout === '3d') {
  2050. root.style.display = 'none';
  2051. return;
  2052. }
  2053.  
  2054. const button = layout === 'xy'
  2055. ? visibleButton('button[data-nge-btn="split"]') ||
  2056. visibleButton('button[data-nge-btn="xy"]')
  2057. : visibleButton('button[data-nge-btn="xy"]');
  2058.  
  2059. if (!button) {
  2060. root.style.display = 'none';
  2061. return;
  2062. }
  2063.  
  2064. const rect = button.getBoundingClientRect();
  2065.  
  2066. root.style.display = 'block';
  2067. root.style.left = Math.max(0, rect.right - 65) + 'px';
  2068. root.style.top = rect.bottom + 5 + 'px';
  2069. }
  2070.  
  2071. // =====================================================
  2072. // MAIN UPDATE LOOP
  2073. // =====================================================
  2074.  
  2075. function update() {
  2076. const state = readState();
  2077.  
  2078. if (state) {
  2079. const key = datasetKey(state);
  2080.  
  2081. if (key !== activeDataset) {
  2082. activeDataset = key;
  2083. lastPosition = null;
  2084. lastRenderKey = '';
  2085. resetBridge();
  2086. drawing = false;
  2087. drawingPoints = [];
  2088. rebuildMenu();
  2089. }
  2090.  
  2091. const current = state.position.slice(0, 3);
  2092.  
  2093. updateBridge(current);
  2094. lastPosition = current;
  2095.  
  2096. const g = geometry();
  2097.  
  2098. const renderKey = JSON.stringify({
  2099. dataset: activeDataset,
  2100. position: current,
  2101. scale: state.crossSectionScale,
  2102. rect: g ? [
  2103. g.rect.x, g.rect.y,
  2104. g.rect.width, g.rect.height
  2105. ] : null,
  2106. drawing: drawingPoints.length
  2107. });
  2108.  
  2109. if (renderKey !== lastRenderKey || drawing) {
  2110. lastRenderKey = renderKey;
  2111. renderLassos();
  2112. }
  2113. }
  2114.  
  2115. positionMenu();
  2116. }
  2117.  
  2118. setInterval(update, CONFIG.pollMs);
  2119. update();
  2120.  
  2121. console.log('[EyeWire II Tools v0.9] Loaded');
  2122. })();
Add Comment
Please, Sign In to add comment