Guest User

Untitled

a guest
Nov 1st, 2026
24
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.43 KB | None | 0 0
  1.  
  2. // ==UserScript==
  3. // @name EyeWire II - UI Guard
  4. // @namespace local.eyewire.ui
  5. // @version 2.1.0
  6. // @description Stable legend, annotations, radio and Selection Details; experimental Cut/Merge layer-switch guard.
  7. // @match https://eyewire-ii-community-dot-brain-wire-dot-seung-lab.ue.r.appspot.com/*
  8. // @run-at document-start
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (() => {
  13. 'use strict';
  14.  
  15. const LOG = '[EW Guard]';
  16.  
  17. const LEGEND = '.nge-seg-legend';
  18.  
  19. const ANNOTATIONS_PANEL =
  20. '.neuroglancer-side-panel.nge-ann-panel';
  21.  
  22. const ANNOTATIONS_TAB =
  23. '.neuroglancer-layer-side-panel-tab-view';
  24.  
  25. const SELECTION_PANEL =
  26. '.neuroglancer-side-panel.neuroglancer-selection-details';
  27.  
  28. const CLOSE_BUTTON =
  29. '.neuroglancer-icon[title="Close panel"]';
  30.  
  31. const SETTINGS_BUTTON =
  32. 'button.nge-settings-advanced-btn';
  33.  
  34. const STORAGE_KEY =
  35. 'ew-ui-guard-selection-closed';
  36.  
  37. const EDITING = [
  38. '.nge-split-merge-overlay',
  39. '.graphene-merge-segments',
  40. '.graphene-multicut'
  41. ].join(', ');
  42.  
  43. function editingActive() {
  44. return !!document.querySelector(EDITING);
  45. }
  46.  
  47. function important(el, property, value) {
  48. if (
  49. el.style.getPropertyValue(property) !== value ||
  50. el.style.getPropertyPriority(property) !== 'important'
  51. ) {
  52. el.style.setProperty(property, value, 'important');
  53. }
  54. }
  55.  
  56. // ============================================
  57. // 1. STATIONARY LEGEND
  58. // ============================================
  59.  
  60. let legend = null;
  61. let legendPosition = null;
  62.  
  63. function updateLegend() {
  64. const found = document.querySelector(LEGEND);
  65.  
  66. if (!found) {
  67. legend = null;
  68. legendPosition = null;
  69. return;
  70. }
  71.  
  72. if (found !== legend) {
  73. legend = found;
  74. legendPosition = null;
  75. }
  76.  
  77. if (!legendPosition && !editingActive()) {
  78. const r = found.getBoundingClientRect();
  79.  
  80. if (r.width >= 10 && r.height >= 5) {
  81. legendPosition = {
  82. left: r.left,
  83. top: r.top,
  84. width: r.width
  85. };
  86. }
  87. }
  88.  
  89. if (!legendPosition) return;
  90.  
  91. important(found, 'position', 'fixed');
  92. important(found, 'left', `${legendPosition.left}px`);
  93. important(found, 'top', `${legendPosition.top}px`);
  94. important(found, 'right', 'auto');
  95. important(found, 'bottom', 'auto');
  96. important(found, 'width', `${legendPosition.width}px`);
  97. important(found, 'margin', '0');
  98. important(found, 'transform', 'none');
  99. important(found, 'transition', 'none');
  100. important(found, 'z-index', '1000');
  101. }
  102.  
  103. // ============================================
  104. // 2. ANNOTATIONS HEIGHT LOCK
  105. // ============================================
  106.  
  107. let lockedHeight = null;
  108.  
  109. function updateAnnotationsHeight() {
  110. if (lockedHeight !== null) return;
  111. if (editingActive()) return;
  112.  
  113. const tab = document.querySelector(
  114. `${ANNOTATIONS_PANEL} ${ANNOTATIONS_TAB}`
  115. );
  116.  
  117. if (!tab) return;
  118.  
  119. const height = tab.getBoundingClientRect().height;
  120. if (height < 50) return;
  121.  
  122. lockedHeight = height;
  123.  
  124. const style = document.createElement('style');
  125. style.id = 'ew-ui-guard-annotations-height';
  126.  
  127. style.textContent = `
  128. ${ANNOTATIONS_PANEL} ${ANNOTATIONS_TAB} {
  129. height: ${height}px !important;
  130. min-height: ${height}px !important;
  131. max-height: ${height}px !important;
  132. flex: 0 0 ${height}px !important;
  133. }
  134. `;
  135.  
  136. (document.head || document.documentElement)
  137. .appendChild(style);
  138.  
  139. console.info(
  140. LOG,
  141. 'Annotations height locked:',
  142. height
  143. );
  144. }
  145.  
  146. // ============================================
  147. // 3. STATIONARY, FUNCTIONAL EYEWIRE RADIO
  148. //
  149. // UNCHANGED FROM WORKING v2.0.
  150. // ============================================
  151.  
  152. function installRadioFix() {
  153. if (
  154. document.getElementById(
  155. 'ew-ui-guard-radio-position'
  156. )
  157. ) {
  158. return;
  159. }
  160.  
  161. const style = document.createElement('style');
  162.  
  163. style.id = 'ew-ui-guard-radio-position';
  164.  
  165. style.textContent = `
  166. .nge-radio {
  167. bottom: 6px !important;
  168. transition: none !important;
  169. }
  170. `;
  171.  
  172. (document.head || document.documentElement)
  173. .appendChild(style);
  174. }
  175.  
  176. // ============================================
  177. // 4. SELECTION DETAILS
  178. // ============================================
  179.  
  180. let selectionLocked = false;
  181. let selectionLocation = null;
  182. let manualOpenUntil = 0;
  183.  
  184. try {
  185. selectionLocked =
  186. localStorage.getItem(STORAGE_KEY) === 'true';
  187. } catch (e) {
  188. console.warn(LOG, e);
  189. }
  190.  
  191. function saveSelectionLock(value) {
  192. selectionLocked = value;
  193.  
  194. try {
  195. localStorage.setItem(
  196. STORAGE_KEY,
  197. String(value)
  198. );
  199. } catch (e) {
  200. console.warn(LOG, e);
  201. }
  202. }
  203.  
  204. function findSelectionLocation() {
  205. const manager = window.viewer?.sidePanelManager;
  206.  
  207. if (!manager?.registeredPanels) return null;
  208.  
  209. const entries = [...manager.registeredPanels];
  210.  
  211. if (
  212. selectionLocation &&
  213. entries.some(
  214. e => e.location === selectionLocation
  215. )
  216. ) {
  217. return selectionLocation;
  218. }
  219.  
  220. selectionLocation = null;
  221.  
  222. const open = entries.find(
  223. e => e.panel?.element?.matches?.(SELECTION_PANEL)
  224. );
  225.  
  226. if (open) {
  227. selectionLocation = open.location;
  228. return selectionLocation;
  229. }
  230.  
  231. const matches = entries.filter(e => {
  232. const v = e.location?.value;
  233.  
  234. return (
  235. v?.side === 'right' &&
  236. v?.row === 1 &&
  237. v?.minSize === 150
  238. );
  239. });
  240.  
  241. if (matches.length === 1) {
  242. selectionLocation = matches[0].location;
  243. }
  244.  
  245. return selectionLocation;
  246. }
  247.  
  248. function protectSelectionVisibility() {
  249. const location = findSelectionLocation();
  250.  
  251. if (!location || location.__ewGuardProtected) {
  252. return;
  253. }
  254.  
  255. const descriptor =
  256. Object.getOwnPropertyDescriptor(
  257. Object.getPrototypeOf(location),
  258. 'visible'
  259. );
  260.  
  261. if (
  262. typeof descriptor?.get !== 'function' ||
  263. typeof descriptor?.set !== 'function'
  264. ) {
  265. return;
  266. }
  267.  
  268. Object.defineProperty(location, 'visible', {
  269. configurable: true,
  270. enumerable: descriptor.enumerable,
  271.  
  272. get() {
  273. return descriptor.get.call(this);
  274. },
  275.  
  276. set(value) {
  277. if (
  278. value === true &&
  279. selectionLocked &&
  280. performance.now() >= manualOpenUntil
  281. ) {
  282. return;
  283. }
  284.  
  285. descriptor.set.call(this, value);
  286. }
  287. });
  288.  
  289. Object.defineProperty(
  290. location,
  291. '__ewGuardProtected',
  292. {
  293. value: true,
  294. configurable: true
  295. }
  296. );
  297. }
  298.  
  299. function selectionAction(event) {
  300. if (!event.isTrusted) return;
  301. if (!(event.target instanceof Element)) return;
  302.  
  303. const settings =
  304. event.target.closest(SETTINGS_BUTTON);
  305.  
  306. if (
  307. settings &&
  308. /selection\s*details/i.test(
  309. settings.textContent || ''
  310. )
  311. ) {
  312. saveSelectionLock(false);
  313. manualOpenUntil = performance.now() + 2000;
  314. return;
  315. }
  316.  
  317. if (
  318. event.target.closest(
  319. `${SELECTION_PANEL} ${CLOSE_BUTTON}`
  320. )
  321. ) {
  322. saveSelectionLock(true);
  323. manualOpenUntil = 0;
  324. }
  325. }
  326.  
  327. document.addEventListener(
  328. 'pointerdown',
  329. selectionAction,
  330. true
  331. );
  332.  
  333. document.addEventListener(
  334. 'click',
  335. selectionAction,
  336. true
  337. );
  338.  
  339. document.addEventListener(
  340. 'keydown',
  341. event => {
  342. if (
  343. event.key === 'Enter' ||
  344. event.key === ' '
  345. ) {
  346. selectionAction(event);
  347. }
  348. },
  349. true
  350. );
  351.  
  352. // ============================================
  353. // 5. EXPERIMENTAL CUT/MERGE LAYER GUARD
  354. //
  355. // The uploaded trace identified two distinct
  356. // selection paths:
  357. //
  358. // Automatic editing activation:
  359. // activate -> selectedLayer.layer = pni_mec
  360. //
  361. // Automatic editing cleanup:
  362. // activation callback -> previous layer
  363. //
  364. // Manual layer selection uses a different
  365. // call stack.
  366. //
  367. // We intercept only the activation assignment
  368. // to the segmentation layer.
  369. //
  370. // No layer clicking or restoration is done.
  371. // ============================================
  372.  
  373. let protectedSelectedLayer = null;
  374.  
  375. const SEGMENTATION_LAYER = 'pni_mec';
  376.  
  377. // Based on the exact activation location
  378. // identified in the uploaded trace.
  379. const ACTIVATION_CALL =
  380. /(?:^|\n)activate@.*main\.bundle\.js[^:\n]*:2167:150326(?:\n|$)/;
  381.  
  382. function isAutomaticEditingActivation() {
  383. const stack = new Error().stack || '';
  384. return ACTIVATION_CALL.test(stack);
  385. }
  386.  
  387. function installLayerGuard() {
  388. const selected = window.viewer?.selectedLayer;
  389.  
  390. if (!selected) return;
  391.  
  392. if (selected === protectedSelectedLayer) {
  393. return;
  394. }
  395.  
  396. let owner = selected;
  397. let descriptor = null;
  398.  
  399. while (owner && !descriptor) {
  400. descriptor =
  401. Object.getOwnPropertyDescriptor(
  402. owner,
  403. 'layer'
  404. );
  405.  
  406. if (!descriptor) {
  407. owner = Object.getPrototypeOf(owner);
  408. }
  409. }
  410.  
  411. if (
  412. !descriptor?.get ||
  413. !descriptor?.set ||
  414. !descriptor.configurable
  415. ) {
  416. return;
  417. }
  418.  
  419. // Do not replace a non-configurable own property.
  420. const ownDescriptor =
  421. Object.getOwnPropertyDescriptor(
  422. selected,
  423. 'layer'
  424. );
  425.  
  426. if (ownDescriptor && !ownDescriptor.configurable) {
  427. return;
  428. }
  429.  
  430. const originalGet = descriptor.get;
  431. const originalSet = descriptor.set;
  432.  
  433. Object.defineProperty(selected, 'layer', {
  434. configurable: true,
  435. enumerable: descriptor.enumerable,
  436.  
  437. get() {
  438. return originalGet.call(this);
  439. },
  440.  
  441. set(value) {
  442. const current = originalGet.call(this);
  443.  
  444. const fromName = current?.name ?? null;
  445. const toName = value?.name ?? null;
  446.  
  447. const unwantedActivation =
  448. current !== value &&
  449. fromName !== SEGMENTATION_LAYER &&
  450. toName === SEGMENTATION_LAYER &&
  451. editingActive() &&
  452. isAutomaticEditingActivation();
  453.  
  454. if (unwantedActivation) {
  455. console.info(
  456. LOG,
  457. 'Blocked automatic editing selection:',
  458. fromName,
  459. '->',
  460. toName
  461. );
  462.  
  463. return;
  464. }
  465.  
  466. return originalSet.call(this, value);
  467. }
  468. });
  469.  
  470. protectedSelectedLayer = selected;
  471.  
  472. console.info(
  473. LOG,
  474. 'Experimental Cut/Merge layer guard installed'
  475. );
  476. }
  477.  
  478. // ============================================
  479. // 6. START
  480. // ============================================
  481.  
  482. function update() {
  483. try {
  484. updateLegend();
  485. updateAnnotationsHeight();
  486. protectSelectionVisibility();
  487. installLayerGuard();
  488. } catch (error) {
  489. console.warn(
  490. LOG,
  491. 'Update failed:',
  492. error
  493. );
  494. }
  495. }
  496.  
  497. function start() {
  498. installRadioFix();
  499. update();
  500. setInterval(update, 100);
  501.  
  502. console.info(LOG, 'v2.1 initialized');
  503. }
  504.  
  505. if (document.readyState === 'loading') {
  506. document.addEventListener(
  507. 'DOMContentLoaded',
  508. start,
  509. { once: true }
  510. );
  511. } else {
  512. start();
  513. }
  514. })();
  515.  
Advertisement
Add Comment
Please, Sign In to add comment