Guest User

Untitled

a guest
Aug 5th, 2026
33
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.12 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Neuroglancer Auto Import Settings
  3. // @namespace eyewire-elite
  4. // @version 2.0.0
  5. // @description Automatically imports saved Neuroglancer settings as soon as the viewer state has genuinely stabilized.
  6. // @match https://spelunker.cave-explorer.org/*
  7. // @run-at document-idle
  8. // @grant unsafeWindow
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. const CONFIG = Object.freeze({
  15. settingsStorageKey: 'ng-settings',
  16.  
  17. /*
  18. * Never import immediately, even when the state initially appears stable.
  19. */
  20. minimumWaitMilliseconds: 3000,
  21.  
  22. /*
  23. * The meaningful viewer state must remain unchanged for this long.
  24. */
  25. requiredStableMilliseconds: 2500,
  26.  
  27. /*
  28. * How often readiness is checked.
  29. */
  30. checkIntervalMilliseconds: 250,
  31.  
  32. /*
  33. * Stop trying after this amount of time.
  34. */
  35. maximumWaitMilliseconds: 45000
  36. });
  37.  
  38. if (!localStorage.getItem(CONFIG.settingsStorageKey)) {
  39. console.log(
  40. '[Auto import] No saved ng-settings entry found. ' +
  41. 'The current viewer will remain unchanged.'
  42. );
  43.  
  44. return;
  45. }
  46.  
  47. const startedAt = performance.now();
  48.  
  49. let lastSnapshot = null;
  50. let stableSince = null;
  51. let importTriggered = false;
  52.  
  53. const intervalId = window.setInterval(
  54. checkViewer,
  55. CONFIG.checkIntervalMilliseconds
  56. );
  57.  
  58.  
  59. function checkViewer() {
  60. if (importTriggered) {
  61. window.clearInterval(intervalId);
  62. return;
  63. }
  64.  
  65. const now = performance.now();
  66. const totalElapsed = now - startedAt;
  67.  
  68. if (totalElapsed >= CONFIG.maximumWaitMilliseconds) {
  69. window.clearInterval(intervalId);
  70.  
  71. console.warn(
  72. '[Auto import] Viewer did not reach a stable state within ' +
  73. Math.round(CONFIG.maximumWaitMilliseconds / 1000) +
  74. ' seconds. Automatic import was skipped; press / manually.'
  75. );
  76.  
  77. return;
  78. }
  79.  
  80. if (totalElapsed < CONFIG.minimumWaitMilliseconds) {
  81. return;
  82. }
  83.  
  84. const pageViewer = getPageViewer();
  85.  
  86. if (!isViewerReady(pageViewer)) {
  87. resetStability();
  88. return;
  89. }
  90.  
  91. if (isLoginPromptVisible()) {
  92. resetStability();
  93. return;
  94. }
  95.  
  96. let snapshot;
  97.  
  98. try {
  99. snapshot = createMeaningfulSnapshot(pageViewer);
  100. } catch (error) {
  101. resetStability();
  102. return;
  103. }
  104.  
  105. if (snapshot !== lastSnapshot) {
  106. lastSnapshot = snapshot;
  107. stableSince = now;
  108. return;
  109. }
  110.  
  111. if (stableSince === null) {
  112. stableSince = now;
  113. return;
  114. }
  115.  
  116. const stableElapsed = now - stableSince;
  117.  
  118. if (
  119. stableElapsed >= CONFIG.requiredStableMilliseconds
  120. ) {
  121. importTriggered = true;
  122. window.clearInterval(intervalId);
  123.  
  124. triggerExistingImporter();
  125.  
  126. console.log(
  127. '[Auto import] Viewer stabilized after ' +
  128. (totalElapsed / 1000).toFixed(2) +
  129. ' seconds. Settings import triggered.'
  130. );
  131. }
  132. }
  133.  
  134.  
  135. function getPageViewer() {
  136. try {
  137. if (
  138. typeof unsafeWindow !== 'undefined' &&
  139. unsafeWindow &&
  140. unsafeWindow.viewer
  141. ) {
  142. return unsafeWindow.viewer;
  143. }
  144. } catch (error) {
  145. // Continue to the normal window check.
  146. }
  147.  
  148. try {
  149. if (window.viewer) {
  150. return window.viewer;
  151. }
  152. } catch (error) {
  153. // Viewer is not available yet.
  154. }
  155.  
  156. return null;
  157. }
  158.  
  159.  
  160. function isViewerReady(pageViewer) {
  161. try {
  162. if (
  163. !pageViewer ||
  164. !pageViewer.state ||
  165. typeof pageViewer.state.toJSON !== 'function'
  166. ) {
  167. return false;
  168. }
  169.  
  170. const state = pageViewer.state.toJSON();
  171.  
  172. if (
  173. !state ||
  174. !Array.isArray(state.layers) ||
  175. state.layers.length === 0
  176. ) {
  177. return false;
  178. }
  179.  
  180. const hasSegmentationLayer =
  181. state.layers.some(function (layer) {
  182. return Boolean(
  183. layer &&
  184. layer.type === 'segmentation' &&
  185. layer.source
  186. );
  187. });
  188.  
  189. if (!hasSegmentationLayer) {
  190. return false;
  191. }
  192.  
  193. const topBar = document.querySelector(
  194. '.neuroglancer-viewer-top-row'
  195. );
  196.  
  197. const renderedPanel = document.querySelector(
  198. '.neuroglancer-rendered-data-panel, canvas'
  199. );
  200.  
  201. return Boolean(topBar && renderedPanel);
  202. } catch (error) {
  203. return false;
  204. }
  205. }
  206.  
  207.  
  208. /**
  209. * Makes a snapshot of state values that commonly change during initial load.
  210. *
  211. * Large annotation arrays are reduced to counts so comparison stays cheap.
  212. */
  213. function createMeaningfulSnapshot(pageViewer) {
  214. const state = pageViewer.state.toJSON();
  215.  
  216. const layers = state.layers.map(function (layer) {
  217. const snapshotLayer = {
  218. name: layer.name || '',
  219. type: layer.type || '',
  220. source: normalizeSource(layer.source),
  221. visible:
  222. Object.prototype.hasOwnProperty.call(
  223. layer,
  224. 'visible'
  225. )
  226. ? layer.visible
  227. : null
  228. };
  229.  
  230. if (layer.type === 'segmentation') {
  231. snapshotLayer.segments =
  232. Array.isArray(layer.segments)
  233. ? layer.segments.map(String)
  234. : [];
  235.  
  236. snapshotLayer.segmentCount =
  237. snapshotLayer.segments.length;
  238. }
  239.  
  240. if (layer.type === 'annotation') {
  241. snapshotLayer.annotationCount =
  242. Array.isArray(layer.annotations)
  243. ? layer.annotations.length
  244. : 0;
  245. }
  246.  
  247. return snapshotLayer;
  248. });
  249.  
  250. const snapshot = {
  251. url: window.location.href,
  252.  
  253. dimensions:
  254. Object.prototype.hasOwnProperty.call(
  255. state,
  256. 'dimensions'
  257. )
  258. ? state.dimensions
  259. : null,
  260.  
  261. position:
  262. Object.prototype.hasOwnProperty.call(
  263. state,
  264. 'position'
  265. )
  266. ? state.position
  267. : null,
  268.  
  269. crossSectionScale:
  270. Object.prototype.hasOwnProperty.call(
  271. state,
  272. 'crossSectionScale'
  273. )
  274. ? state.crossSectionScale
  275. : null,
  276.  
  277. projectionScale:
  278. Object.prototype.hasOwnProperty.call(
  279. state,
  280. 'projectionScale'
  281. )
  282. ? state.projectionScale
  283. : null,
  284.  
  285. projectionDepth:
  286. Object.prototype.hasOwnProperty.call(
  287. state,
  288. 'projectionDepth'
  289. )
  290. ? state.projectionDepth
  291. : null,
  292.  
  293. projectionOrientation:
  294. Object.prototype.hasOwnProperty.call(
  295. state,
  296. 'projectionOrientation'
  297. )
  298. ? state.projectionOrientation
  299. : null,
  300.  
  301. layerCount: layers.length,
  302. layers: layers,
  303.  
  304. canvasCount:
  305. document.querySelectorAll('canvas').length,
  306.  
  307. renderedPanelCount:
  308. document.querySelectorAll(
  309. '.neuroglancer-rendered-data-panel'
  310. ).length
  311. };
  312.  
  313. return JSON.stringify(snapshot);
  314. }
  315.  
  316.  
  317. function normalizeSource(source) {
  318. if (source === undefined || source === null) {
  319. return null;
  320. }
  321.  
  322. if (typeof source === 'string') {
  323. return source;
  324. }
  325.  
  326. try {
  327. return JSON.stringify(source);
  328. } catch (error) {
  329. return String(source);
  330. }
  331. }
  332.  
  333.  
  334. function isLoginPromptVisible() {
  335. const bodyText = document.body
  336. ? document.body.innerText.toLowerCase()
  337. : '';
  338.  
  339. return (
  340. bodyText.includes('login required') ||
  341. bodyText.includes('request login')
  342. );
  343. }
  344.  
  345.  
  346. function resetStability() {
  347. lastSnapshot = null;
  348. stableSince = null;
  349. }
  350.  
  351.  
  352. /**
  353. * Triggers the "/" shortcut registered by the Import/export settings script.
  354. */
  355. function triggerExistingImporter() {
  356. const target = document.body || document.documentElement;
  357.  
  358. const event = new KeyboardEvent('keyup', {
  359. key: '/',
  360. code: 'Slash',
  361. keyCode: 191,
  362. which: 191,
  363. bubbles: true,
  364. cancelable: true
  365. });
  366.  
  367. target.dispatchEvent(event);
  368. }
  369. })();
Advertisement
Add Comment
Please, Sign In to add comment