Guest User

Untitled

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