Guest User

Untitled

a guest
Aug 5th, 2026
35
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.58 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Import/export settings
  3. // @namespace http://tampermonkey.net/
  4. // @version 2026-08-05
  5. // @description Exports NG settings and imports them while preserving current XYZ coordinates and 2D/3D zoom
  6. // @author KrzysztofKruk, modified
  7. // @match https://spelunker.cave-explorer.org/
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=cave-explorer.org
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. /* global viewer */
  13.  
  14. (function () {
  15. 'use strict'
  16.  
  17. const STORAGE_KEY = 'ng-settings'
  18.  
  19. function cloneValue(value) {
  20. if (value === undefined) {
  21. return undefined
  22. }
  23.  
  24. return JSON.parse(JSON.stringify(value))
  25. }
  26.  
  27.  
  28. function exportSettings() {
  29. const settings = viewer.state.toJSON()
  30.  
  31. settings.layers.forEach(layer => {
  32. if (layer.type === 'segmentation') {
  33. delete layer.segments
  34. }
  35. else if (layer.type === 'annotation') {
  36. delete layer.annotations
  37. }
  38. })
  39.  
  40. localStorage.setItem(
  41. STORAGE_KEY,
  42. JSON.stringify(settings)
  43. )
  44.  
  45. console.log(
  46. '[Import/export settings] Settings exported.'
  47. )
  48. }
  49.  
  50.  
  51. function importSettings() {
  52. const currentSettings = viewer.state.toJSON()
  53. const currentLayerContents = {}
  54.  
  55. /*
  56. * Preserve values that belong to the newly opened task.
  57. *
  58. * position:
  59. * Current XYZ coordinates.
  60. *
  61. * dimensions:
  62. * Coordinate-space interpretation of XYZ.
  63. *
  64. * crossSectionScale:
  65. * 2D slice zoom.
  66. *
  67. * projectionScale:
  68. * 3D zoom.
  69. *
  70. * projectionDepth:
  71. * 3D camera depth.
  72. *
  73. * projectionOrientation:
  74. * Current 3D viewing direction.
  75. */
  76. const preservedTopLevelProperties = [
  77. 'dimensions',
  78. 'position',
  79. 'crossSectionScale',
  80. 'projectionScale',
  81. 'projectionDepth',
  82. 'projectionOrientation'
  83. ]
  84.  
  85. const preservedValues = {}
  86.  
  87. preservedTopLevelProperties.forEach(propertyName => {
  88. if (
  89. Object.prototype.hasOwnProperty.call(
  90. currentSettings,
  91. propertyName
  92. )
  93. ) {
  94. preservedValues[propertyName] =
  95. cloneValue(currentSettings[propertyName])
  96. }
  97. })
  98.  
  99. /*
  100. * Preserve the currently selected segments and annotations.
  101. */
  102. currentSettings.layers.forEach(layer => {
  103. if (!layer || !layer.name) {
  104. return
  105. }
  106.  
  107. if (layer.type === 'segmentation') {
  108. currentLayerContents[layer.name] = {
  109. type: 'segmentation',
  110. segments: Array.isArray(layer.segments)
  111. ? cloneValue(layer.segments)
  112. : []
  113. }
  114. }
  115. else if (layer.type === 'annotation') {
  116. currentLayerContents[layer.name] = {
  117. type: 'annotation',
  118. annotations: Array.isArray(layer.annotations)
  119. ? cloneValue(layer.annotations)
  120. : []
  121. }
  122. }
  123. })
  124.  
  125. let settingsText =
  126. localStorage.getItem(STORAGE_KEY)
  127.  
  128. if (!settingsText) {
  129. console.log(
  130. '[Import/export settings] Saved settings not found.'
  131. )
  132. return
  133. }
  134.  
  135. let settings
  136.  
  137. try {
  138. settings = JSON.parse(settingsText)
  139. }
  140. catch (error) {
  141. console.error(
  142. '[Import/export settings] Saved settings are not valid JSON:',
  143. error
  144. )
  145. return
  146. }
  147.  
  148. if (
  149. !settings ||
  150. !Array.isArray(settings.layers)
  151. ) {
  152. console.error(
  153. '[Import/export settings] Saved settings do not contain a valid layer list.'
  154. )
  155. return
  156. }
  157.  
  158. /*
  159. * Put the current task's segments and annotations into the imported state.
  160. */
  161. settings.layers.forEach(layer => {
  162. if (!layer || !layer.name) {
  163. return
  164. }
  165.  
  166. const currentContents =
  167. currentLayerContents[layer.name]
  168.  
  169. if (!currentContents) {
  170. return
  171. }
  172.  
  173. if (
  174. layer.type === 'segmentation' &&
  175. currentContents.type === 'segmentation'
  176. ) {
  177. layer.segments =
  178. cloneValue(currentContents.segments)
  179. }
  180. else if (
  181. layer.type === 'annotation' &&
  182. currentContents.type === 'annotation'
  183. ) {
  184. layer.annotations =
  185. cloneValue(currentContents.annotations)
  186. }
  187. })
  188.  
  189. /*
  190. * Replace the exported coordinates and zoom values with those from the
  191. * currently opened link before restoring the settings.
  192. */
  193. preservedTopLevelProperties.forEach(propertyName => {
  194. if (
  195. Object.prototype.hasOwnProperty.call(
  196. preservedValues,
  197. propertyName
  198. )
  199. ) {
  200. settings[propertyName] =
  201. cloneValue(preservedValues[propertyName])
  202. }
  203. else {
  204. delete settings[propertyName]
  205. }
  206. })
  207.  
  208. try {
  209. viewer.state.restoreState(settings)
  210.  
  211. /*
  212. * Reapply the preserved values after the first restoration.
  213. *
  214. * Some viewer properties can be recalculated as other settings restore,
  215. * so this second pass ensures the original XYZ and zoom remain in place.
  216. */
  217. const restoredSettings =
  218. viewer.state.toJSON()
  219.  
  220. preservedTopLevelProperties.forEach(propertyName => {
  221. if (
  222. Object.prototype.hasOwnProperty.call(
  223. preservedValues,
  224. propertyName
  225. )
  226. ) {
  227. restoredSettings[propertyName] =
  228. cloneValue(preservedValues[propertyName])
  229. }
  230. else {
  231. delete restoredSettings[propertyName]
  232. }
  233. })
  234.  
  235. viewer.state.restoreState(restoredSettings)
  236.  
  237. console.log(
  238. '[Import/export settings] Settings imported while preserving ' +
  239. 'XYZ position and 2D/3D zoom.'
  240. )
  241. }
  242. catch (error) {
  243. console.error(
  244. '[Import/export settings] Import failed:',
  245. error
  246. )
  247.  
  248. console.log(
  249. 'Current state:',
  250. JSON.stringify(currentSettings)
  251. )
  252.  
  253. console.log(
  254. 'Imported state:',
  255. JSON.stringify(settings)
  256. )
  257. }
  258. }
  259.  
  260.  
  261. let target
  262.  
  263.  
  264. function addMenu() {
  265. const menu = document.createElement('div')
  266.  
  267. menu.style.position = 'fixed'
  268. menu.style.background = '#222'
  269. menu.style.color = '#fff'
  270. menu.style.border = '1px solid #555'
  271. menu.style.padding = '5px 0'
  272. menu.style.display = 'none'
  273. menu.style.zIndex = 10000
  274. menu.style.minWidth = '120px'
  275. menu.style.fontSize = '13px'
  276. menu.style.fontFamily = 'Arial, sans-serif'
  277.  
  278. function createItem(text, onClick) {
  279. const item = document.createElement('div')
  280.  
  281. item.textContent = text
  282. item.style.padding = '7px 10px'
  283. item.style.cursor = 'pointer'
  284.  
  285. item.addEventListener('mouseenter', () => {
  286. item.style.background = '#444'
  287. })
  288.  
  289. item.addEventListener('mouseleave', () => {
  290. item.style.background = 'transparent'
  291. })
  292.  
  293. item.addEventListener('click', () => {
  294. onClick()
  295. hideMenu()
  296. })
  297.  
  298. return item
  299. }
  300.  
  301.  
  302. function hideMenu() {
  303. menu.style.display = 'none'
  304. }
  305.  
  306.  
  307. menu.appendChild(
  308. createItem(
  309. 'Export settings',
  310. () => {
  311. if (
  312. window.confirm(
  313. 'Do you want to export current settings and potentially replace the old ones?'
  314. )
  315. ) {
  316. exportSettings()
  317. }
  318. }
  319. )
  320. )
  321.  
  322.  
  323. menu.appendChild(
  324. createItem(
  325. 'Import settings',
  326. () => {
  327. importSettings()
  328. }
  329. )
  330. )
  331.  
  332.  
  333. document.body.appendChild(menu)
  334.  
  335.  
  336. target.addEventListener(
  337. 'contextmenu',
  338. event => {
  339. menu.style.left =
  340. event.clientX + 'px'
  341.  
  342. menu.style.top =
  343. event.clientY + 'px'
  344.  
  345. menu.style.display = 'block'
  346. }
  347. )
  348.  
  349.  
  350. document.addEventListener(
  351. 'click',
  352. hideMenu
  353. )
  354.  
  355.  
  356. window.addEventListener(
  357. 'blur',
  358. hideMenu
  359. )
  360.  
  361.  
  362. document.body.addEventListener(
  363. 'keyup',
  364. event => {
  365. switch (event.key) {
  366. case 'x':
  367. exportSettings()
  368. break
  369.  
  370. case '/':
  371. importSettings()
  372. break
  373. }
  374. }
  375. )
  376. }
  377.  
  378.  
  379. const waitForTopBar = setInterval(() => {
  380. target = document.querySelector(
  381. '.neuroglancer-viewer-top-row'
  382. )
  383.  
  384. if (!target) {
  385. return
  386. }
  387.  
  388. clearInterval(waitForTopBar)
  389. addMenu()
  390. }, 100)
  391. })()
Advertisement
Add Comment
Please, Sign In to add comment