Guest User

Untitled

a guest
Aug 28th, 2026
20
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 40.51 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Import/export settings
  3. // @namespace http://tampermonkey.net/
  4. // @version 2026-08-29.9
  5. // @description 3 named Neuroglancer presets. Imports opacity and Show default annotations while preserving link/navigation state and active layer tab.
  6. // @author KrzysztofKruk + modifications
  7. // @match https://spelunker.cave-explorer.org/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. /* global viewer */
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const STORAGE_KEY = 'ng-settings-3-slots';
  17. const LEGACY_STORAGE_KEY = 'ng-settings';
  18. const API_NAME = 'NGSettingsPresets';
  19. const SLOT_COUNT = 3;
  20.  
  21.  
  22. // ==========================================================================
  23. // SETTINGS THAT MUST NOT MOVE BETWEEN LINKS
  24. // ==========================================================================
  25.  
  26. /*
  27. * IMPORTANT:
  28. *
  29. * showDefaultAnnotations is intentionally NOT in this list.
  30. *
  31. * That is the global setting we now WANT to export/import.
  32. */
  33. const FORBIDDEN_TOP_LEVEL_KEYS = new Set([
  34. 'dimensions',
  35. 'position',
  36.  
  37. 'crossSectionScale',
  38. 'crossSectionOrientation',
  39.  
  40. 'projectionScale',
  41. 'projectionDepth',
  42. 'projectionOrientation',
  43.  
  44. 'layout',
  45.  
  46. 'selectedLayer',
  47. 'selection'
  48. ]);
  49.  
  50.  
  51. const FORBIDDEN_LAYER_KEYS = new Set([
  52. 'name',
  53. 'type',
  54.  
  55. /*
  56. * Never replace data sources.
  57. */
  58. 'source',
  59.  
  60. /*
  61. * Never transfer link-specific selected objects/content.
  62. */
  63. 'segments',
  64. 'annotations',
  65.  
  66. /*
  67. * Never transfer local coordinates.
  68. */
  69. 'localDimensions',
  70. 'localPosition',
  71. 'localVelocity',
  72.  
  73. /*
  74. * Never transfer transforms.
  75. */
  76. 'transform',
  77.  
  78. /*
  79. * Never change Seg -> Source or another side-panel tab.
  80. */
  81. 'tab',
  82. 'panels'
  83. ]);
  84.  
  85.  
  86. // ==========================================================================
  87. // SEGMENTATION SETTINGS
  88. // ==========================================================================
  89.  
  90. /*
  91. * Neuroglancer may omit properties from its JSON when they equal their
  92. * default value.
  93. *
  94. * Therefore we explicitly save these values.
  95. */
  96. const SEGMENTATION_DEFAULTS = Object.freeze({
  97. selectedAlpha: 0.5,
  98. notSelectedAlpha: 0,
  99. objectAlpha: 1,
  100.  
  101. saturation: 1,
  102.  
  103. hideSegmentZero: true,
  104. hoverHighlight: true,
  105. baseSegmentColoring: false,
  106. ignoreNullVisibleSet: true,
  107.  
  108. colorSeed: 0,
  109.  
  110. crossSectionRenderScale: 1,
  111. meshRenderScale: 10,
  112. meshSilhouetteRendering: 0
  113. });
  114.  
  115.  
  116. const SEGMENTATION_DISPLAY_KEYS =
  117. Object.keys(
  118. SEGMENTATION_DEFAULTS
  119. );
  120.  
  121.  
  122. // ==========================================================================
  123. // BASIC HELPERS
  124. // ==========================================================================
  125.  
  126. function clone(value) {
  127. if (
  128. value === undefined
  129. ) {
  130. return undefined;
  131. }
  132.  
  133.  
  134. return JSON.parse(
  135. JSON.stringify(
  136. value
  137. )
  138. );
  139. }
  140.  
  141.  
  142. function own(
  143. object,
  144. key
  145. ) {
  146. return Boolean(
  147. object &&
  148. Object.prototype.hasOwnProperty.call(
  149. object,
  150. key
  151. )
  152. );
  153. }
  154.  
  155.  
  156. function normalizeName(
  157. name,
  158. fallback
  159. ) {
  160. const text =
  161. String(
  162. name == null
  163. ? ''
  164. : name
  165. ).trim();
  166.  
  167.  
  168. return (
  169. text ||
  170. fallback
  171. );
  172. }
  173.  
  174.  
  175. // ==========================================================================
  176. // STORAGE
  177. // ==========================================================================
  178.  
  179. function defaultStore() {
  180. return {
  181. version: 9,
  182.  
  183. slots:
  184. Array(
  185. SLOT_COUNT
  186. ).fill(
  187. null
  188. )
  189. };
  190. }
  191.  
  192.  
  193. function normalizeStore(
  194. store
  195. ) {
  196. const result =
  197. defaultStore();
  198.  
  199.  
  200. if (
  201. !store ||
  202. typeof store !== 'object' ||
  203. !Array.isArray(
  204. store.slots
  205. )
  206. ) {
  207. return result;
  208. }
  209.  
  210.  
  211. for (
  212. let i = 0;
  213. i < SLOT_COUNT;
  214. i++
  215. ) {
  216. const slot =
  217. store.slots[i];
  218.  
  219.  
  220. if (
  221. !slot ||
  222. typeof slot !== 'object' ||
  223. !slot.settings ||
  224. typeof slot.settings !== 'object'
  225. ) {
  226. continue;
  227. }
  228.  
  229.  
  230. result.slots[i] = {
  231. name:
  232. normalizeName(
  233. slot.name,
  234. `Preset ${i + 1}`
  235. ),
  236.  
  237. settings:
  238. slot.settings,
  239.  
  240. updatedAt:
  241. slot.updatedAt ||
  242. null
  243. };
  244. }
  245.  
  246.  
  247. return result;
  248. }
  249.  
  250.  
  251. function saveStore(
  252. store
  253. ) {
  254. try {
  255. localStorage.setItem(
  256. STORAGE_KEY,
  257.  
  258. JSON.stringify(
  259. normalizeStore(
  260. store
  261. )
  262. )
  263. );
  264.  
  265.  
  266. return true;
  267.  
  268. } catch (error) {
  269. console.error(
  270. '[Import/export settings] Could not save presets:',
  271. error
  272. );
  273.  
  274.  
  275. window.alert(
  276. 'Could not save settings presets.'
  277. );
  278.  
  279.  
  280. return false;
  281. }
  282. }
  283.  
  284.  
  285. function migrateLegacy(
  286. store
  287. ) {
  288. if (
  289. store.slots.some(
  290. Boolean
  291. )
  292. ) {
  293. return;
  294. }
  295.  
  296.  
  297. let text;
  298.  
  299.  
  300. try {
  301. text =
  302. localStorage.getItem(
  303. LEGACY_STORAGE_KEY
  304. );
  305.  
  306. } catch (_) {
  307. return;
  308. }
  309.  
  310.  
  311. if (!text) {
  312. return;
  313. }
  314.  
  315.  
  316. let settings;
  317.  
  318.  
  319. try {
  320. settings =
  321. JSON.parse(
  322. text
  323. );
  324.  
  325. } catch (_) {
  326. return;
  327. }
  328.  
  329.  
  330. if (
  331. !settings ||
  332. typeof settings !== 'object' ||
  333. Array.isArray(
  334. settings
  335. )
  336. ) {
  337. return;
  338. }
  339.  
  340.  
  341. store.slots[0] = {
  342. name:
  343. 'Imported settings',
  344.  
  345. settings,
  346.  
  347. updatedAt:
  348. new Date()
  349. .toISOString()
  350. };
  351.  
  352.  
  353. saveStore(
  354. store
  355. );
  356. }
  357.  
  358.  
  359. function loadStore() {
  360. let store =
  361. null;
  362.  
  363.  
  364. try {
  365. const text =
  366. localStorage.getItem(
  367. STORAGE_KEY
  368. );
  369.  
  370.  
  371. if (text) {
  372. store =
  373. JSON.parse(
  374. text
  375. );
  376. }
  377.  
  378. } catch (error) {
  379. console.warn(
  380. '[Import/export settings] Could not parse preset store:',
  381. error
  382. );
  383. }
  384.  
  385.  
  386. store =
  387. normalizeStore(
  388. store
  389. );
  390.  
  391.  
  392. migrateLegacy(
  393. store
  394. );
  395.  
  396.  
  397. return store;
  398. }
  399.  
  400.  
  401. function getFilledSlots(
  402. store
  403. ) {
  404. const result =
  405. [];
  406.  
  407.  
  408. for (
  409. let i = 0;
  410. i < SLOT_COUNT;
  411. i++
  412. ) {
  413. const slot =
  414. store.slots[i];
  415.  
  416.  
  417. if (
  418. slot &&
  419. slot.settings
  420. ) {
  421. result.push({
  422. index:
  423. i,
  424.  
  425. number:
  426. i + 1,
  427.  
  428. name:
  429. slot.name,
  430.  
  431. slot
  432. });
  433. }
  434. }
  435.  
  436.  
  437. return result;
  438. }
  439.  
  440.  
  441. // ==========================================================================
  442. // VIEWER STATE
  443. // ==========================================================================
  444.  
  445. function getCurrentState() {
  446. try {
  447. return viewer
  448. .state
  449. .toJSON();
  450.  
  451. } catch (error) {
  452. console.error(
  453. '[Import/export settings] Could not read current viewer state:',
  454. error
  455. );
  456.  
  457.  
  458. return null;
  459. }
  460. }
  461.  
  462.  
  463. function findCurrentLayerJson(
  464. state,
  465. name
  466. ) {
  467. if (
  468. !state ||
  469. !Array.isArray(
  470. state.layers
  471. )
  472. ) {
  473. return null;
  474. }
  475.  
  476.  
  477. return (
  478. state.layers.find(
  479. layer =>
  480. layer &&
  481. layer.name === name
  482. ) ||
  483. null
  484. );
  485. }
  486.  
  487.  
  488. // ==========================================================================
  489. // MANAGED LAYERS
  490. // ==========================================================================
  491.  
  492. function getManagedLayers() {
  493. try {
  494. const managed =
  495. viewer &&
  496. viewer.layerManager &&
  497. viewer.layerManager
  498. .managedLayers;
  499.  
  500.  
  501. if (
  502. Array.isArray(
  503. managed
  504. )
  505. ) {
  506. return managed;
  507. }
  508.  
  509.  
  510. if (
  511. managed &&
  512. typeof managed[
  513. Symbol.iterator
  514. ] === 'function'
  515. ) {
  516. return Array.from(
  517. managed
  518. );
  519. }
  520.  
  521. } catch (_) {
  522. // Ignore.
  523. }
  524.  
  525.  
  526. return [];
  527. }
  528.  
  529.  
  530. function findManagedLayerByName(
  531. name
  532. ) {
  533. return (
  534. getManagedLayers().find(
  535. layer =>
  536. layer &&
  537. layer.name === name
  538. ) ||
  539. null
  540. );
  541. }
  542.  
  543.  
  544. // ==========================================================================
  545. // TRACKABLE VALUES
  546. // ==========================================================================
  547.  
  548. function readTrackable(
  549. trackable
  550. ) {
  551. if (
  552. trackable === undefined ||
  553. trackable === null
  554. ) {
  555. return undefined;
  556. }
  557.  
  558.  
  559. /*
  560. * Most Neuroglancer TrackableValue objects expose .value.
  561. */
  562. try {
  563. if (
  564. typeof trackable === 'object' &&
  565. 'value' in trackable
  566. ) {
  567. return clone(
  568. trackable.value
  569. );
  570. }
  571. } catch (_) {
  572. // Continue.
  573. }
  574.  
  575.  
  576. /*
  577. * Other trackables expose toJSON().
  578. */
  579. try {
  580. if (
  581. typeof trackable.toJSON === 'function'
  582. ) {
  583. const json =
  584. trackable.toJSON();
  585.  
  586.  
  587. if (
  588. json !== undefined
  589. ) {
  590. return clone(
  591. json
  592. );
  593. }
  594. }
  595. } catch (_) {
  596. // Continue.
  597. }
  598.  
  599.  
  600. return undefined;
  601. }
  602.  
  603.  
  604. function setTrackable(
  605. trackable,
  606. value,
  607. label
  608. ) {
  609. if (
  610. trackable === undefined ||
  611. trackable === null
  612. ) {
  613. return false;
  614. }
  615.  
  616.  
  617. /*
  618. * Preferred Neuroglancer mechanism.
  619. */
  620. if (
  621. typeof trackable.restoreState === 'function'
  622. ) {
  623. try {
  624. trackable.restoreState(
  625. clone(
  626. value
  627. )
  628. );
  629.  
  630.  
  631. console.debug(
  632. `[Import/export settings] Restored ${label} via restoreState().`
  633. );
  634.  
  635.  
  636. return true;
  637.  
  638. } catch (error) {
  639. console.debug(
  640. `[Import/export settings] restoreState() failed for ${label}:`,
  641. error
  642. );
  643. }
  644. }
  645.  
  646.  
  647. /*
  648. * Fallback for TrackableValue.
  649. */
  650. try {
  651. if (
  652. typeof trackable === 'object' &&
  653. 'value' in trackable
  654. ) {
  655. trackable.value =
  656. clone(
  657. value
  658. );
  659.  
  660.  
  661. console.debug(
  662. `[Import/export settings] Restored ${label} via .value.`
  663. );
  664.  
  665.  
  666. return true;
  667. }
  668.  
  669. } catch (error) {
  670. console.debug(
  671. `[Import/export settings] .value failed for ${label}:`,
  672. error
  673. );
  674. }
  675.  
  676.  
  677. return false;
  678. }
  679.  
  680.  
  681. // ==========================================================================
  682. // SHOW DEFAULT ANNOTATIONS
  683. // ==========================================================================
  684.  
  685. /*
  686. * Read the live viewer trackable if possible.
  687. *
  688. * We still fall back to state.showDefaultAnnotations.
  689. */
  690. function readShowDefaultAnnotations(
  691. state
  692. ) {
  693. const candidates = [];
  694.  
  695.  
  696. try {
  697. candidates.push(
  698. viewer.showDefaultAnnotations
  699. );
  700. } catch (_) {
  701. // Ignore.
  702. }
  703.  
  704.  
  705. try {
  706. candidates.push(
  707. viewer.state &&
  708. viewer.state.viewer &&
  709. viewer.state.viewer
  710. .showDefaultAnnotations
  711. );
  712. } catch (_) {
  713. // Ignore.
  714. }
  715.  
  716.  
  717. try {
  718. candidates.push(
  719. viewer.state &&
  720. viewer.state
  721. .showDefaultAnnotations
  722. );
  723. } catch (_) {
  724. // Ignore.
  725. }
  726.  
  727.  
  728. for (
  729. const candidate of
  730. candidates
  731. ) {
  732. const value =
  733. readTrackable(
  734. candidate
  735. );
  736.  
  737.  
  738. if (
  739. typeof value === 'boolean'
  740. ) {
  741. return value;
  742. }
  743. }
  744.  
  745.  
  746. if (
  747. state &&
  748. own(
  749. state,
  750. 'showDefaultAnnotations'
  751. )
  752. ) {
  753. return Boolean(
  754. state.showDefaultAnnotations
  755. );
  756. }
  757.  
  758.  
  759. /*
  760. * Neuroglancer's default is true.
  761. *
  762. * If it is omitted from JSON, explicitly save true.
  763. */
  764. return true;
  765. }
  766.  
  767.  
  768. function applyShowDefaultAnnotations(
  769. enabled
  770. ) {
  771. enabled =
  772. Boolean(
  773. enabled
  774. );
  775.  
  776.  
  777. const candidates = [];
  778.  
  779.  
  780. try {
  781. candidates.push({
  782. label:
  783. 'viewer.showDefaultAnnotations',
  784.  
  785. value:
  786. viewer.showDefaultAnnotations
  787. });
  788. } catch (_) {
  789. // Ignore.
  790. }
  791.  
  792.  
  793. try {
  794. candidates.push({
  795. label:
  796. 'viewer.state.viewer.showDefaultAnnotations',
  797.  
  798. value:
  799. viewer.state &&
  800. viewer.state.viewer &&
  801. viewer.state.viewer
  802. .showDefaultAnnotations
  803. });
  804. } catch (_) {
  805. // Ignore.
  806. }
  807.  
  808.  
  809. try {
  810. candidates.push({
  811. label:
  812. 'viewer.state.showDefaultAnnotations',
  813.  
  814. value:
  815. viewer.state &&
  816. viewer.state
  817. .showDefaultAnnotations
  818. });
  819. } catch (_) {
  820. // Ignore.
  821. }
  822.  
  823.  
  824. for (
  825. const candidate of
  826. candidates
  827. ) {
  828. if (
  829. setTrackable(
  830. candidate.value,
  831. enabled,
  832. candidate.label
  833. )
  834. ) {
  835. console.log(
  836. '[Import/export settings] Show default annotations set to:',
  837. enabled
  838. );
  839.  
  840.  
  841. return true;
  842. }
  843. }
  844.  
  845.  
  846. /*
  847. * Final fallback:
  848. *
  849. * Restore a tiny viewer-state patch containing ONLY this property.
  850. *
  851. * We deliberately do NOT pass layers, position, layout, selection,
  852. * zoom, or anything else.
  853. */
  854. try {
  855. viewer.state.restoreState({
  856. showDefaultAnnotations:
  857. enabled
  858. });
  859.  
  860.  
  861. console.log(
  862. '[Import/export settings] Show default annotations restored with top-level state patch:',
  863. enabled
  864. );
  865.  
  866.  
  867. return true;
  868.  
  869. } catch (error) {
  870. console.error(
  871. '[Import/export settings] Could not restore showDefaultAnnotations:',
  872. error
  873. );
  874.  
  875.  
  876. return false;
  877. }
  878. }
  879.  
  880.  
  881. // ==========================================================================
  882. // SEGMENTATION EXPORT
  883. // ==========================================================================
  884.  
  885. function enrichSegmentationExport(
  886. layer
  887. ) {
  888. if (
  889. !layer ||
  890. layer.type !== 'segmentation'
  891. ) {
  892. return;
  893. }
  894.  
  895.  
  896. const managedLayer =
  897. findManagedLayerByName(
  898. layer.name
  899. );
  900.  
  901.  
  902. const userLayer =
  903. managedLayer &&
  904. managedLayer.layer;
  905.  
  906.  
  907. const displayState =
  908. userLayer &&
  909. userLayer.displayState;
  910.  
  911.  
  912. for (
  913. const key of
  914. SEGMENTATION_DISPLAY_KEYS
  915. ) {
  916. let value;
  917.  
  918.  
  919. /*
  920. * First read live displayState.
  921. */
  922. try {
  923. if (
  924. displayState &&
  925. displayState[key] !== undefined
  926. ) {
  927. value =
  928. readTrackable(
  929. displayState[key]
  930. );
  931. }
  932. } catch (_) {
  933. // Ignore.
  934. }
  935.  
  936.  
  937. /*
  938. * Then try the UserLayer itself.
  939. */
  940. if (
  941. value === undefined
  942. ) {
  943. try {
  944. if (
  945. userLayer &&
  946. userLayer[key] !== undefined
  947. ) {
  948. value =
  949. readTrackable(
  950. userLayer[key]
  951. );
  952. }
  953. } catch (_) {
  954. // Ignore.
  955. }
  956. }
  957.  
  958.  
  959. /*
  960. * Then serialized JSON.
  961. */
  962. if (
  963. value === undefined &&
  964. own(
  965. layer,
  966. key
  967. )
  968. ) {
  969. value =
  970. clone(
  971. layer[key]
  972. );
  973. }
  974.  
  975.  
  976. /*
  977. * Finally explicit Neuroglancer default.
  978. */
  979. if (
  980. value === undefined &&
  981. own(
  982. SEGMENTATION_DEFAULTS,
  983. key
  984. )
  985. ) {
  986. value =
  987. clone(
  988. SEGMENTATION_DEFAULTS[key]
  989. );
  990. }
  991.  
  992.  
  993. if (
  994. value !== undefined
  995. ) {
  996. layer[key] =
  997. value;
  998. }
  999. }
  1000.  
  1001.  
  1002. console.log(
  1003. `[Import/export settings] Saving opacity/display for "${layer.name}":`,
  1004. {
  1005. selectedAlpha:
  1006. layer.selectedAlpha,
  1007.  
  1008. notSelectedAlpha:
  1009. layer.notSelectedAlpha,
  1010.  
  1011. objectAlpha:
  1012. layer.objectAlpha
  1013. }
  1014. );
  1015. }
  1016.  
  1017.  
  1018. // ==========================================================================
  1019. // CREATE PORTABLE PRESET
  1020. // ==========================================================================
  1021.  
  1022. function createPortableSettings() {
  1023. let state;
  1024.  
  1025.  
  1026. try {
  1027. state =
  1028. clone(
  1029. viewer.state.toJSON()
  1030. );
  1031.  
  1032. } catch (error) {
  1033. console.error(
  1034. '[Import/export settings] Could not read viewer state:',
  1035. error
  1036. );
  1037.  
  1038.  
  1039. window.alert(
  1040. 'Could not read the Neuroglancer state.'
  1041. );
  1042.  
  1043.  
  1044. return null;
  1045. }
  1046.  
  1047.  
  1048. /*
  1049. * Capture this explicitly BEFORE removing link-specific state.
  1050. */
  1051. state.showDefaultAnnotations =
  1052. readShowDefaultAnnotations(
  1053. state
  1054. );
  1055.  
  1056.  
  1057. console.log(
  1058. '[Import/export settings] Saving showDefaultAnnotations:',
  1059. state.showDefaultAnnotations
  1060. );
  1061.  
  1062.  
  1063. /*
  1064. * Remove link/navigation-specific viewer state.
  1065. */
  1066. for (
  1067. const key of
  1068. FORBIDDEN_TOP_LEVEL_KEYS
  1069. ) {
  1070. delete state[key];
  1071. }
  1072.  
  1073.  
  1074. /*
  1075. * Sanitize every layer.
  1076. */
  1077. if (
  1078. Array.isArray(
  1079. state.layers
  1080. )
  1081. ) {
  1082. for (
  1083. const layer of
  1084. state.layers
  1085. ) {
  1086. if (!layer) {
  1087. continue;
  1088. }
  1089.  
  1090.  
  1091. /*
  1092. * Explicitly capture opacity/display defaults.
  1093. */
  1094. enrichSegmentationExport(
  1095. layer
  1096. );
  1097.  
  1098.  
  1099. /*
  1100. * Never transfer link-specific objects.
  1101. */
  1102. delete layer.segments;
  1103. delete layer.annotations;
  1104.  
  1105.  
  1106. /*
  1107. * Never transfer local navigation.
  1108. */
  1109. delete layer.localPosition;
  1110. delete layer.localDimensions;
  1111. delete layer.localVelocity;
  1112.  
  1113.  
  1114. /*
  1115. * Never transfer active layer panel/tab.
  1116. */
  1117. delete layer.tab;
  1118. delete layer.panels;
  1119. }
  1120. }
  1121.  
  1122.  
  1123. return state;
  1124. }
  1125.  
  1126.  
  1127. // ==========================================================================
  1128. // EXPORT SLOT CHOICE
  1129. // ==========================================================================
  1130.  
  1131. function chooseExportSlot(
  1132. store
  1133. ) {
  1134. const lines =
  1135. [];
  1136.  
  1137.  
  1138. for (
  1139. let i = 0;
  1140. i < SLOT_COUNT;
  1141. i++
  1142. ) {
  1143. const slot =
  1144. store.slots[i];
  1145.  
  1146.  
  1147. lines.push(
  1148. slot
  1149. ? `${i + 1}. ${slot.name}`
  1150. : `${i + 1}. [empty]`
  1151. );
  1152. }
  1153.  
  1154.  
  1155. const answer =
  1156. window.prompt(
  1157. 'Choose a slot for the current settings:\n\n' +
  1158. lines.join('\n') +
  1159. '\n\nEnter 1, 2, or 3:',
  1160. '1'
  1161. );
  1162.  
  1163.  
  1164. if (
  1165. answer === null
  1166. ) {
  1167. return null;
  1168. }
  1169.  
  1170.  
  1171. const number =
  1172. Number(
  1173. String(
  1174. answer
  1175. ).trim()
  1176. );
  1177.  
  1178.  
  1179. if (
  1180. !Number.isInteger(
  1181. number
  1182. ) ||
  1183. number < 1 ||
  1184. number > SLOT_COUNT
  1185. ) {
  1186. window.alert(
  1187. 'Please enter 1, 2, or 3.'
  1188. );
  1189.  
  1190.  
  1191. return null;
  1192. }
  1193.  
  1194.  
  1195. return (
  1196. number - 1
  1197. );
  1198. }
  1199.  
  1200.  
  1201. // ==========================================================================
  1202. // EXPORT
  1203. // ==========================================================================
  1204.  
  1205. function exportSettings() {
  1206. const store =
  1207. loadStore();
  1208.  
  1209.  
  1210. const slotIndex =
  1211. chooseExportSlot(
  1212. store
  1213. );
  1214.  
  1215.  
  1216. if (
  1217. slotIndex === null
  1218. ) {
  1219. return false;
  1220. }
  1221.  
  1222.  
  1223. const existing =
  1224. store.slots[
  1225. slotIndex
  1226. ];
  1227.  
  1228.  
  1229. if (
  1230. existing &&
  1231. !window.confirm(
  1232. `Slot ${slotIndex + 1} currently contains:\n\n` +
  1233. `"${existing.name}"\n\n` +
  1234. 'Replace it?'
  1235. )
  1236. ) {
  1237. return false;
  1238. }
  1239.  
  1240.  
  1241. const enteredName =
  1242. window.prompt(
  1243. `Name Slot ${slotIndex + 1}:`,
  1244. existing
  1245. ? existing.name
  1246. : `Preset ${slotIndex + 1}`
  1247. );
  1248.  
  1249.  
  1250. if (
  1251. enteredName === null
  1252. ) {
  1253. return false;
  1254. }
  1255.  
  1256.  
  1257. const settings =
  1258. createPortableSettings();
  1259.  
  1260.  
  1261. if (!settings) {
  1262. return false;
  1263. }
  1264.  
  1265.  
  1266. const name =
  1267. normalizeName(
  1268. enteredName,
  1269. `Preset ${slotIndex + 1}`
  1270. );
  1271.  
  1272.  
  1273. store.slots[
  1274. slotIndex
  1275. ] = {
  1276. name,
  1277.  
  1278. settings,
  1279.  
  1280. updatedAt:
  1281. new Date()
  1282. .toISOString()
  1283. };
  1284.  
  1285.  
  1286. if (
  1287. !saveStore(
  1288. store
  1289. )
  1290. ) {
  1291. return false;
  1292. }
  1293.  
  1294.  
  1295. console.log(
  1296. `[Import/export settings] Exported Slot ${slotIndex + 1}: "${name}".`
  1297. );
  1298.  
  1299.  
  1300. window.alert(
  1301. `Settings exported.\n\n` +
  1302. `Slot ${slotIndex + 1}: ${name}\n` +
  1303. `Show default annotations: ` +
  1304. (
  1305. settings.showDefaultAnnotations
  1306. ? 'ON'
  1307. : 'OFF'
  1308. )
  1309. );
  1310.  
  1311.  
  1312. return true;
  1313. }
  1314.  
  1315.  
  1316. // ==========================================================================
  1317. // IMPORT SLOT CHOICE
  1318. // ==========================================================================
  1319.  
  1320. function chooseImportSlot(
  1321. store,
  1322. quietWhenEmpty
  1323. ) {
  1324. const slots =
  1325. getFilledSlots(
  1326. store
  1327. );
  1328.  
  1329.  
  1330. if (
  1331. slots.length === 0
  1332. ) {
  1333. if (
  1334. !quietWhenEmpty
  1335. ) {
  1336. window.alert(
  1337. 'No exported settings are saved.'
  1338. );
  1339. }
  1340.  
  1341.  
  1342. return null;
  1343. }
  1344.  
  1345.  
  1346. /*
  1347. * Exactly one saved preset:
  1348. * no question — import automatically.
  1349. */
  1350. if (
  1351. slots.length === 1
  1352. ) {
  1353. console.log(
  1354. `[Import/export settings] Only one preset exists; automatically selecting "${slots[0].name}".`
  1355. );
  1356.  
  1357.  
  1358. return slots[0].index;
  1359. }
  1360.  
  1361.  
  1362. /*
  1363. * Multiple presets:
  1364. * ask which one.
  1365. */
  1366. const answer =
  1367. window.prompt(
  1368. 'Choose the settings state to import:\n\n' +
  1369. slots
  1370. .map(
  1371. item =>
  1372. `${item.number}. ${item.name}`
  1373. )
  1374. .join('\n') +
  1375. '\n\nEnter the slot number:',
  1376. String(
  1377. slots[0].number
  1378. )
  1379. );
  1380.  
  1381.  
  1382. if (
  1383. answer === null
  1384. ) {
  1385. return null;
  1386. }
  1387.  
  1388.  
  1389. const number =
  1390. Number(
  1391. String(
  1392. answer
  1393. ).trim()
  1394. );
  1395.  
  1396.  
  1397. const selected =
  1398. slots.find(
  1399. item =>
  1400. item.number === number
  1401. );
  1402.  
  1403.  
  1404. if (!selected) {
  1405. window.alert(
  1406. 'That slot is not available.'
  1407. );
  1408.  
  1409.  
  1410. return null;
  1411. }
  1412.  
  1413.  
  1414. return selected.index;
  1415. }
  1416.  
  1417.  
  1418. // ==========================================================================
  1419. // SEGMENTATION IMPORT
  1420. // ==========================================================================
  1421.  
  1422. function getSavedSegmentationValue(
  1423. savedLayer,
  1424. key
  1425. ) {
  1426. if (
  1427. own(
  1428. savedLayer,
  1429. key
  1430. )
  1431. ) {
  1432. return clone(
  1433. savedLayer[key]
  1434. );
  1435. }
  1436.  
  1437.  
  1438. if (
  1439. own(
  1440. SEGMENTATION_DEFAULTS,
  1441. key
  1442. )
  1443. ) {
  1444. return clone(
  1445. SEGMENTATION_DEFAULTS[key]
  1446. );
  1447. }
  1448.  
  1449.  
  1450. return undefined;
  1451. }
  1452.  
  1453.  
  1454. function applySegmentationDisplaySettings(
  1455. managedLayer,
  1456. savedLayer
  1457. ) {
  1458. if (
  1459. !managedLayer ||
  1460. !managedLayer.layer
  1461. ) {
  1462. return 0;
  1463. }
  1464.  
  1465.  
  1466. const userLayer =
  1467. managedLayer.layer;
  1468.  
  1469.  
  1470. const displayState =
  1471. userLayer.displayState;
  1472.  
  1473.  
  1474. let applied =
  1475. 0;
  1476.  
  1477.  
  1478. for (
  1479. const key of
  1480. SEGMENTATION_DISPLAY_KEYS
  1481. ) {
  1482. const value =
  1483. getSavedSegmentationValue(
  1484. savedLayer,
  1485. key
  1486. );
  1487.  
  1488.  
  1489. if (
  1490. value === undefined
  1491. ) {
  1492. continue;
  1493. }
  1494.  
  1495.  
  1496. let success =
  1497. false;
  1498.  
  1499.  
  1500. /*
  1501. * Preferred target.
  1502. */
  1503. try {
  1504. if (
  1505. displayState &&
  1506. displayState[key] !== undefined
  1507. ) {
  1508. success =
  1509. setTrackable(
  1510. displayState[key],
  1511. value,
  1512. `${managedLayer.name}.${key}`
  1513. );
  1514. }
  1515. } catch (_) {
  1516. // Continue.
  1517. }
  1518.  
  1519.  
  1520. /*
  1521. * Fallback.
  1522. */
  1523. if (!success) {
  1524. try {
  1525. if (
  1526. userLayer[key] !== undefined
  1527. ) {
  1528. success =
  1529. setTrackable(
  1530. userLayer[key],
  1531. value,
  1532. `${managedLayer.name}.${key}`
  1533. );
  1534. }
  1535. } catch (_) {
  1536. // Ignore.
  1537. }
  1538. }
  1539.  
  1540.  
  1541. if (success) {
  1542. applied++;
  1543. }
  1544. }
  1545.  
  1546.  
  1547. console.log(
  1548. `[Import/export settings] "${managedLayer.name}" opacity import:`,
  1549. {
  1550. selectedAlpha:
  1551. getSavedSegmentationValue(
  1552. savedLayer,
  1553. 'selectedAlpha'
  1554. ),
  1555.  
  1556. notSelectedAlpha:
  1557. getSavedSegmentationValue(
  1558. savedLayer,
  1559. 'notSelectedAlpha'
  1560. ),
  1561.  
  1562. objectAlpha:
  1563. getSavedSegmentationValue(
  1564. savedLayer,
  1565. 'objectAlpha'
  1566. )
  1567. }
  1568. );
  1569.  
  1570.  
  1571. return applied;
  1572. }
  1573.  
  1574.  
  1575. // ==========================================================================
  1576. // GENERIC LAYER SETTINGS
  1577. // ==========================================================================
  1578.  
  1579. function makeGenericPatch(
  1580. savedLayer
  1581. ) {
  1582. const patch =
  1583. {};
  1584.  
  1585.  
  1586. for (
  1587. const [
  1588. key,
  1589. value
  1590. ] of
  1591. Object.entries(
  1592. savedLayer
  1593. )
  1594. ) {
  1595. /*
  1596. * Never touch unsafe/link-specific layer properties.
  1597. */
  1598. if (
  1599. FORBIDDEN_LAYER_KEYS.has(
  1600. key
  1601. )
  1602. ) {
  1603. continue;
  1604. }
  1605.  
  1606.  
  1607. /*
  1608. * Segmentation values have their own explicit importer.
  1609. */
  1610. if (
  1611. SEGMENTATION_DISPLAY_KEYS.includes(
  1612. key
  1613. )
  1614. ) {
  1615. continue;
  1616. }
  1617.  
  1618.  
  1619. patch[key] =
  1620. clone(
  1621. value
  1622. );
  1623. }
  1624.  
  1625.  
  1626. return patch;
  1627. }
  1628.  
  1629.  
  1630. function applyGenericTrackables(
  1631. target,
  1632. patch,
  1633. path
  1634. ) {
  1635. if (
  1636. !target ||
  1637. !patch ||
  1638. typeof patch !== 'object'
  1639. ) {
  1640. return 0;
  1641. }
  1642.  
  1643.  
  1644. let applied =
  1645. 0;
  1646.  
  1647.  
  1648. for (
  1649. const [
  1650. key,
  1651. value
  1652. ] of
  1653. Object.entries(
  1654. patch
  1655. )
  1656. ) {
  1657. let property;
  1658.  
  1659.  
  1660. try {
  1661. property =
  1662. target[key];
  1663.  
  1664. } catch (_) {
  1665. continue;
  1666. }
  1667.  
  1668.  
  1669. if (
  1670. property === undefined ||
  1671. property === null
  1672. ) {
  1673. continue;
  1674. }
  1675.  
  1676.  
  1677. if (
  1678. setTrackable(
  1679. property,
  1680. value,
  1681. `${path}.${key}`
  1682. )
  1683. ) {
  1684. applied++;
  1685.  
  1686. continue;
  1687. }
  1688.  
  1689.  
  1690. /*
  1691. * Recurse into nested display-state objects.
  1692. */
  1693. if (
  1694. value &&
  1695. typeof value === 'object' &&
  1696. !Array.isArray(
  1697. value
  1698. ) &&
  1699. property &&
  1700. typeof property === 'object'
  1701. ) {
  1702. applied +=
  1703. applyGenericTrackables(
  1704. property,
  1705. value,
  1706. `${path}.${key}`
  1707. );
  1708. }
  1709. }
  1710.  
  1711.  
  1712. return applied;
  1713. }
  1714.  
  1715.  
  1716. function applyGenericDisplaySettings(
  1717. managedLayer,
  1718. savedLayer
  1719. ) {
  1720. if (
  1721. !managedLayer ||
  1722. !managedLayer.layer
  1723. ) {
  1724. return 0;
  1725. }
  1726.  
  1727.  
  1728. const patch =
  1729. makeGenericPatch(
  1730. savedLayer
  1731. );
  1732.  
  1733.  
  1734. let applied =
  1735. 0;
  1736.  
  1737.  
  1738. applied +=
  1739. applyGenericTrackables(
  1740. managedLayer.layer,
  1741. patch,
  1742. managedLayer.name
  1743. );
  1744.  
  1745.  
  1746. if (
  1747. managedLayer.layer
  1748. .displayState
  1749. ) {
  1750. applied +=
  1751. applyGenericTrackables(
  1752. managedLayer.layer
  1753. .displayState,
  1754.  
  1755. patch,
  1756.  
  1757. `${managedLayer.name}.displayState`
  1758. );
  1759. }
  1760.  
  1761.  
  1762. return applied;
  1763. }
  1764.  
  1765.  
  1766. // ==========================================================================
  1767. // IMPORT SETTINGS OBJECT
  1768. // ==========================================================================
  1769.  
  1770. function importSettingsObject(
  1771. savedSettings,
  1772. presetName,
  1773. slotIndex
  1774. ) {
  1775. if (
  1776. !savedSettings ||
  1777. typeof savedSettings !== 'object'
  1778. ) {
  1779. return false;
  1780. }
  1781.  
  1782.  
  1783. const currentState =
  1784. getCurrentState();
  1785.  
  1786.  
  1787. if (!currentState) {
  1788. return false;
  1789. }
  1790.  
  1791.  
  1792. const savedLayers =
  1793. Array.isArray(
  1794. savedSettings.layers
  1795. )
  1796. ? savedSettings.layers
  1797. : [];
  1798.  
  1799.  
  1800. let matched =
  1801. 0;
  1802.  
  1803. let genericProperties =
  1804. 0;
  1805.  
  1806. let segmentationProperties =
  1807. 0;
  1808.  
  1809.  
  1810. /*
  1811. * ------------------------------------------------------------------------
  1812. * STEP 1
  1813. *
  1814. * Restore global "Show default annotations".
  1815. *
  1816. * This is NOT a layer source setting.
  1817. * ------------------------------------------------------------------------
  1818. */
  1819. let defaultAnnotationsApplied =
  1820. false;
  1821.  
  1822.  
  1823. if (
  1824. own(
  1825. savedSettings,
  1826. 'showDefaultAnnotations'
  1827. )
  1828. ) {
  1829. defaultAnnotationsApplied =
  1830. applyShowDefaultAnnotations(
  1831. savedSettings
  1832. .showDefaultAnnotations
  1833. );
  1834. }
  1835.  
  1836.  
  1837. /*
  1838. * ------------------------------------------------------------------------
  1839. * STEP 2
  1840. *
  1841. * Apply saved display settings to EXISTING layers only.
  1842. *
  1843. * We never replace viewer.state.layers.
  1844. * ------------------------------------------------------------------------
  1845. */
  1846. for (
  1847. const savedLayer of
  1848. savedLayers
  1849. ) {
  1850. if (
  1851. !savedLayer ||
  1852. !savedLayer.name
  1853. ) {
  1854. continue;
  1855. }
  1856.  
  1857.  
  1858. const managedLayer =
  1859. findManagedLayerByName(
  1860. savedLayer.name
  1861. );
  1862.  
  1863.  
  1864. const currentLayer =
  1865. findCurrentLayerJson(
  1866. currentState,
  1867. savedLayer.name
  1868. );
  1869.  
  1870.  
  1871. if (
  1872. !managedLayer ||
  1873. !currentLayer
  1874. ) {
  1875. continue;
  1876. }
  1877.  
  1878.  
  1879. /*
  1880. * Avoid applying a segmentation configuration to an image layer,
  1881. * etc.
  1882. */
  1883. if (
  1884. savedLayer.type &&
  1885. currentLayer.type &&
  1886. savedLayer.type !==
  1887. currentLayer.type
  1888. ) {
  1889. console.warn(
  1890. `[Import/export settings] Layer "${savedLayer.name}" type differs; skipped.`
  1891. );
  1892.  
  1893.  
  1894. continue;
  1895. }
  1896.  
  1897.  
  1898. matched++;
  1899.  
  1900.  
  1901. /*
  1902. * Generic visual settings.
  1903. */
  1904. genericProperties +=
  1905. applyGenericDisplaySettings(
  1906. managedLayer,
  1907. savedLayer
  1908. );
  1909.  
  1910.  
  1911. /*
  1912. * ----------------------------------------------------------------------
  1913. * STEP 3
  1914. *
  1915. * Opacity/display values LAST.
  1916. *
  1917. * This protects the opacity values from being reset by another
  1918. * display-setting update.
  1919. * ----------------------------------------------------------------------
  1920. */
  1921. if (
  1922. savedLayer.type === 'segmentation'
  1923. ) {
  1924. segmentationProperties +=
  1925. applySegmentationDisplaySettings(
  1926. managedLayer,
  1927. savedLayer
  1928. );
  1929. }
  1930. }
  1931.  
  1932.  
  1933. console.log(
  1934. `[Import/export settings] SAFE import complete from Slot ${slotIndex + 1}: "${presetName}". ` +
  1935. `${matched} layer(s) matched; ` +
  1936. `${genericProperties} other display properties applied; ` +
  1937. `${segmentationProperties} segmentation display properties applied; ` +
  1938. `showDefaultAnnotations=${String(
  1939. savedSettings.showDefaultAnnotations
  1940. )}, applied=${defaultAnnotationsApplied}.`
  1941. );
  1942.  
  1943.  
  1944. return true;
  1945. }
  1946.  
  1947.  
  1948. // ==========================================================================
  1949. // IMPORT
  1950. // ==========================================================================
  1951.  
  1952. function importSettings(
  1953. options = {}
  1954. ) {
  1955. const store =
  1956. loadStore();
  1957.  
  1958.  
  1959. const slotIndex =
  1960. chooseImportSlot(
  1961. store,
  1962. options.quietWhenEmpty === true
  1963. );
  1964.  
  1965.  
  1966. if (
  1967. slotIndex === null
  1968. ) {
  1969. return {
  1970. ok:
  1971. false,
  1972.  
  1973. reason:
  1974. 'cancelled-or-empty'
  1975. };
  1976. }
  1977.  
  1978.  
  1979. const slot =
  1980. store.slots[
  1981. slotIndex
  1982. ];
  1983.  
  1984.  
  1985. if (
  1986. !slot ||
  1987. !slot.settings
  1988. ) {
  1989. return {
  1990. ok:
  1991. false,
  1992.  
  1993. reason:
  1994. 'missing-slot'
  1995. };
  1996. }
  1997.  
  1998.  
  1999. console.log(
  2000. `[Import/export settings] Importing Slot ${slotIndex + 1}: "${slot.name}".`
  2001. );
  2002.  
  2003.  
  2004. const ok =
  2005. importSettingsObject(
  2006. slot.settings,
  2007. slot.name,
  2008. slotIndex
  2009. );
  2010.  
  2011.  
  2012. return {
  2013. ok,
  2014.  
  2015. slotIndex,
  2016.  
  2017. slotNumber:
  2018. slotIndex + 1,
  2019.  
  2020. name:
  2021. slot.name
  2022. };
  2023. }
  2024.  
  2025.  
  2026. // ==========================================================================
  2027. // MANAGE PRESETS
  2028. // ==========================================================================
  2029.  
  2030. function manageSlots() {
  2031. const store =
  2032. loadStore();
  2033.  
  2034.  
  2035. const lines =
  2036. Array.from(
  2037. {
  2038. length:
  2039. SLOT_COUNT
  2040. },
  2041.  
  2042. (_, i) => {
  2043. const slot =
  2044. store.slots[i];
  2045.  
  2046.  
  2047. return (
  2048. slot
  2049. ? `${i + 1}. ${slot.name}`
  2050. : `${i + 1}. [empty]`
  2051. );
  2052. }
  2053. );
  2054.  
  2055.  
  2056. const answer =
  2057. window.prompt(
  2058. 'Saved settings:\n\n' +
  2059. lines.join('\n') +
  2060. '\n\n' +
  2061. 'R1 / R2 / R3 = rename\n' +
  2062. 'D1 / D2 / D3 = delete',
  2063. ''
  2064. );
  2065.  
  2066.  
  2067. if (
  2068. answer === null
  2069. ) {
  2070. return;
  2071. }
  2072.  
  2073.  
  2074. const match =
  2075. String(
  2076. answer
  2077. )
  2078. .trim()
  2079. .toUpperCase()
  2080. .match(
  2081. /^([RD])([1-3])$/
  2082. );
  2083.  
  2084.  
  2085. if (!match) {
  2086. return;
  2087. }
  2088.  
  2089.  
  2090. const action =
  2091. match[1];
  2092.  
  2093.  
  2094. const index =
  2095. Number(
  2096. match[2]
  2097. ) - 1;
  2098.  
  2099.  
  2100. const slot =
  2101. store.slots[index];
  2102.  
  2103.  
  2104. if (!slot) {
  2105. window.alert(
  2106. `Slot ${index + 1} is empty.`
  2107. );
  2108.  
  2109.  
  2110. return;
  2111. }
  2112.  
  2113.  
  2114. /*
  2115. * Rename.
  2116. */
  2117. if (
  2118. action === 'R'
  2119. ) {
  2120. const name =
  2121. window.prompt(
  2122. `Rename Slot ${index + 1}:`,
  2123. slot.name
  2124. );
  2125.  
  2126.  
  2127. if (
  2128. name === null
  2129. ) {
  2130. return;
  2131. }
  2132.  
  2133.  
  2134. slot.name =
  2135. normalizeName(
  2136. name,
  2137. `Preset ${index + 1}`
  2138. );
  2139.  
  2140.  
  2141. saveStore(
  2142. store
  2143. );
  2144.  
  2145.  
  2146. return;
  2147. }
  2148.  
  2149.  
  2150. /*
  2151. * Delete.
  2152. */
  2153. if (
  2154. action === 'D'
  2155. ) {
  2156. if (
  2157. !window.confirm(
  2158. `Delete Slot ${index + 1}: "${slot.name}"?`
  2159. )
  2160. ) {
  2161. return;
  2162. }
  2163.  
  2164.  
  2165. store.slots[index] =
  2166. null;
  2167.  
  2168.  
  2169. saveStore(
  2170. store
  2171. );
  2172. }
  2173. }
  2174.  
  2175.  
  2176. // ==========================================================================
  2177. // PUBLIC API FOR AUTO IMPORT
  2178. // ==========================================================================
  2179.  
  2180. window[
  2181. API_NAME
  2182. ] = {
  2183. version:
  2184. '2026-08-29.9',
  2185.  
  2186. storageKey:
  2187. STORAGE_KEY,
  2188.  
  2189.  
  2190. getPresets:
  2191. function () {
  2192. return getFilledSlots(
  2193. loadStore()
  2194. ).map(
  2195. item => ({
  2196. slot:
  2197. item.number,
  2198.  
  2199. name:
  2200. item.name
  2201. })
  2202. );
  2203. },
  2204.  
  2205.  
  2206. importSettings:
  2207. function (
  2208. options
  2209. ) {
  2210. return importSettings(
  2211. options ||
  2212. {}
  2213. );
  2214. },
  2215.  
  2216.  
  2217. exportSettings,
  2218.  
  2219. manageSlots
  2220. };
  2221.  
  2222.  
  2223. console.log(
  2224. '[Import/export settings] SAFE preset API v2026-08-29.9 ready.'
  2225. );
  2226.  
  2227.  
  2228. // ==========================================================================
  2229. // RIGHT-CLICK MENU + KEYBOARD SHORTCUTS
  2230. // ==========================================================================
  2231.  
  2232. let topBar;
  2233.  
  2234.  
  2235. function addMenu() {
  2236. const menu =
  2237. document.createElement(
  2238. 'div'
  2239. );
  2240.  
  2241.  
  2242. Object.assign(
  2243. menu.style,
  2244. {
  2245. position:
  2246. 'fixed',
  2247.  
  2248. background:
  2249. '#222',
  2250.  
  2251. color:
  2252. '#fff',
  2253.  
  2254. border:
  2255. '1px solid #555',
  2256.  
  2257. padding:
  2258. '5px 0',
  2259.  
  2260. display:
  2261. 'none',
  2262.  
  2263. zIndex:
  2264. '10000',
  2265.  
  2266. minWidth:
  2267. '190px',
  2268.  
  2269. font:
  2270. '13px Arial, sans-serif'
  2271. }
  2272. );
  2273.  
  2274.  
  2275. function item(
  2276. label,
  2277. handler
  2278. ) {
  2279. const element =
  2280. document.createElement(
  2281. 'div'
  2282. );
  2283.  
  2284.  
  2285. element.textContent =
  2286. label;
  2287.  
  2288.  
  2289. element.style.padding =
  2290. '7px 10px';
  2291.  
  2292.  
  2293. element.style.cursor =
  2294. 'pointer';
  2295.  
  2296.  
  2297. element.addEventListener(
  2298. 'mouseenter',
  2299. () => {
  2300. element.style.background =
  2301. '#444';
  2302. }
  2303. );
  2304.  
  2305.  
  2306. element.addEventListener(
  2307. 'mouseleave',
  2308. () => {
  2309. element.style.background =
  2310. 'transparent';
  2311. }
  2312. );
  2313.  
  2314.  
  2315. element.addEventListener(
  2316. 'click',
  2317. event => {
  2318. event.stopPropagation();
  2319.  
  2320.  
  2321. menu.style.display =
  2322. 'none';
  2323.  
  2324.  
  2325. handler();
  2326. }
  2327. );
  2328.  
  2329.  
  2330. return element;
  2331. }
  2332.  
  2333.  
  2334. menu.appendChild(
  2335. item(
  2336. 'Export settings…',
  2337. exportSettings
  2338. )
  2339. );
  2340.  
  2341.  
  2342. menu.appendChild(
  2343. item(
  2344. 'Import settings…',
  2345. () => {
  2346. importSettings();
  2347. }
  2348. )
  2349. );
  2350.  
  2351.  
  2352. menu.appendChild(
  2353. item(
  2354. 'Manage saved settings…',
  2355. manageSlots
  2356. )
  2357. );
  2358.  
  2359.  
  2360. document.body.appendChild(
  2361. menu
  2362. );
  2363.  
  2364.  
  2365. /*
  2366. * Right-click Neuroglancer top bar.
  2367. */
  2368. topBar.addEventListener(
  2369. 'contextmenu',
  2370. event => {
  2371. event.preventDefault();
  2372.  
  2373.  
  2374. menu.style.left =
  2375. `${event.clientX}px`;
  2376.  
  2377.  
  2378. menu.style.top =
  2379. `${event.clientY}px`;
  2380.  
  2381.  
  2382. menu.style.display =
  2383. 'block';
  2384. }
  2385. );
  2386.  
  2387.  
  2388. document.addEventListener(
  2389. 'click',
  2390. () => {
  2391. menu.style.display =
  2392. 'none';
  2393. }
  2394. );
  2395.  
  2396.  
  2397. /*
  2398. * X = export
  2399. * / = import
  2400. */
  2401. document.body.addEventListener(
  2402. 'keyup',
  2403. event => {
  2404. const target =
  2405. event.target;
  2406.  
  2407.  
  2408. const tag =
  2409. target &&
  2410. target.tagName
  2411. ? target.tagName
  2412. .toLowerCase()
  2413. : '';
  2414.  
  2415.  
  2416. if (
  2417. tag === 'input' ||
  2418. tag === 'textarea' ||
  2419. (
  2420. target &&
  2421. target.isContentEditable
  2422. )
  2423. ) {
  2424. return;
  2425. }
  2426.  
  2427.  
  2428. if (
  2429. event.key === 'x' ||
  2430. event.key === 'X'
  2431. ) {
  2432. exportSettings();
  2433.  
  2434.  
  2435. return;
  2436. }
  2437.  
  2438.  
  2439. if (
  2440. event.key === '/'
  2441. ) {
  2442. importSettings();
  2443. }
  2444. }
  2445. );
  2446. }
  2447.  
  2448.  
  2449. // ==========================================================================
  2450. // START
  2451. // ==========================================================================
  2452.  
  2453. loadStore();
  2454.  
  2455.  
  2456. const wait =
  2457. window.setInterval(
  2458. () => {
  2459. topBar =
  2460. document.querySelector(
  2461. '.neuroglancer-viewer-top-row'
  2462. );
  2463.  
  2464.  
  2465. if (!topBar) {
  2466. return;
  2467. }
  2468.  
  2469.  
  2470. window.clearInterval(
  2471. wait
  2472. );
  2473.  
  2474.  
  2475. addMenu();
  2476. },
  2477.  
  2478. 100
  2479. );
  2480.  
  2481. })();
Add Comment
Please, Sign In to add comment