Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Import/export settings
- // @namespace http://tampermonkey.net/
- // @version 2026-08-29.9
- // @description 3 named Neuroglancer presets. Imports opacity and Show default annotations while preserving link/navigation state and active layer tab.
- // @author KrzysztofKruk + modifications
- // @match https://spelunker.cave-explorer.org/*
- // @grant none
- // ==/UserScript==
- /* global viewer */
- (function () {
- 'use strict';
- const STORAGE_KEY = 'ng-settings-3-slots';
- const LEGACY_STORAGE_KEY = 'ng-settings';
- const API_NAME = 'NGSettingsPresets';
- const SLOT_COUNT = 3;
- // ==========================================================================
- // SETTINGS THAT MUST NOT MOVE BETWEEN LINKS
- // ==========================================================================
- /*
- * IMPORTANT:
- *
- * showDefaultAnnotations is intentionally NOT in this list.
- *
- * That is the global setting we now WANT to export/import.
- */
- const FORBIDDEN_TOP_LEVEL_KEYS = new Set([
- 'dimensions',
- 'position',
- 'crossSectionScale',
- 'crossSectionOrientation',
- 'projectionScale',
- 'projectionDepth',
- 'projectionOrientation',
- 'layout',
- 'selectedLayer',
- 'selection'
- ]);
- const FORBIDDEN_LAYER_KEYS = new Set([
- 'name',
- 'type',
- /*
- * Never replace data sources.
- */
- 'source',
- /*
- * Never transfer link-specific selected objects/content.
- */
- 'segments',
- 'annotations',
- /*
- * Never transfer local coordinates.
- */
- 'localDimensions',
- 'localPosition',
- 'localVelocity',
- /*
- * Never transfer transforms.
- */
- 'transform',
- /*
- * Never change Seg -> Source or another side-panel tab.
- */
- 'tab',
- 'panels'
- ]);
- // ==========================================================================
- // SEGMENTATION SETTINGS
- // ==========================================================================
- /*
- * Neuroglancer may omit properties from its JSON when they equal their
- * default value.
- *
- * Therefore we explicitly save these values.
- */
- const SEGMENTATION_DEFAULTS = Object.freeze({
- selectedAlpha: 0.5,
- notSelectedAlpha: 0,
- objectAlpha: 1,
- saturation: 1,
- hideSegmentZero: true,
- hoverHighlight: true,
- baseSegmentColoring: false,
- ignoreNullVisibleSet: true,
- colorSeed: 0,
- crossSectionRenderScale: 1,
- meshRenderScale: 10,
- meshSilhouetteRendering: 0
- });
- const SEGMENTATION_DISPLAY_KEYS =
- Object.keys(
- SEGMENTATION_DEFAULTS
- );
- // ==========================================================================
- // BASIC HELPERS
- // ==========================================================================
- function clone(value) {
- if (
- value === undefined
- ) {
- return undefined;
- }
- return JSON.parse(
- JSON.stringify(
- value
- )
- );
- }
- function own(
- object,
- key
- ) {
- return Boolean(
- object &&
- Object.prototype.hasOwnProperty.call(
- object,
- key
- )
- );
- }
- function normalizeName(
- name,
- fallback
- ) {
- const text =
- String(
- name == null
- ? ''
- : name
- ).trim();
- return (
- text ||
- fallback
- );
- }
- // ==========================================================================
- // STORAGE
- // ==========================================================================
- function defaultStore() {
- return {
- version: 9,
- slots:
- Array(
- SLOT_COUNT
- ).fill(
- null
- )
- };
- }
- function normalizeStore(
- store
- ) {
- const result =
- defaultStore();
- if (
- !store ||
- typeof store !== 'object' ||
- !Array.isArray(
- store.slots
- )
- ) {
- return result;
- }
- for (
- let i = 0;
- i < SLOT_COUNT;
- i++
- ) {
- const slot =
- store.slots[i];
- if (
- !slot ||
- typeof slot !== 'object' ||
- !slot.settings ||
- typeof slot.settings !== 'object'
- ) {
- continue;
- }
- result.slots[i] = {
- name:
- normalizeName(
- slot.name,
- `Preset ${i + 1}`
- ),
- settings:
- slot.settings,
- updatedAt:
- slot.updatedAt ||
- null
- };
- }
- return result;
- }
- function saveStore(
- store
- ) {
- try {
- localStorage.setItem(
- STORAGE_KEY,
- JSON.stringify(
- normalizeStore(
- store
- )
- )
- );
- return true;
- } catch (error) {
- console.error(
- '[Import/export settings] Could not save presets:',
- error
- );
- window.alert(
- 'Could not save settings presets.'
- );
- return false;
- }
- }
- function migrateLegacy(
- store
- ) {
- if (
- store.slots.some(
- Boolean
- )
- ) {
- return;
- }
- let text;
- try {
- text =
- localStorage.getItem(
- LEGACY_STORAGE_KEY
- );
- } catch (_) {
- return;
- }
- if (!text) {
- return;
- }
- let settings;
- try {
- settings =
- JSON.parse(
- text
- );
- } catch (_) {
- return;
- }
- if (
- !settings ||
- typeof settings !== 'object' ||
- Array.isArray(
- settings
- )
- ) {
- return;
- }
- store.slots[0] = {
- name:
- 'Imported settings',
- settings,
- updatedAt:
- new Date()
- .toISOString()
- };
- saveStore(
- store
- );
- }
- function loadStore() {
- let store =
- null;
- try {
- const text =
- localStorage.getItem(
- STORAGE_KEY
- );
- if (text) {
- store =
- JSON.parse(
- text
- );
- }
- } catch (error) {
- console.warn(
- '[Import/export settings] Could not parse preset store:',
- error
- );
- }
- store =
- normalizeStore(
- store
- );
- migrateLegacy(
- store
- );
- return store;
- }
- function getFilledSlots(
- store
- ) {
- const result =
- [];
- for (
- let i = 0;
- i < SLOT_COUNT;
- i++
- ) {
- const slot =
- store.slots[i];
- if (
- slot &&
- slot.settings
- ) {
- result.push({
- index:
- i,
- number:
- i + 1,
- name:
- slot.name,
- slot
- });
- }
- }
- return result;
- }
- // ==========================================================================
- // VIEWER STATE
- // ==========================================================================
- function getCurrentState() {
- try {
- return viewer
- .state
- .toJSON();
- } catch (error) {
- console.error(
- '[Import/export settings] Could not read current viewer state:',
- error
- );
- return null;
- }
- }
- function findCurrentLayerJson(
- state,
- name
- ) {
- if (
- !state ||
- !Array.isArray(
- state.layers
- )
- ) {
- return null;
- }
- return (
- state.layers.find(
- layer =>
- layer &&
- layer.name === name
- ) ||
- null
- );
- }
- // ==========================================================================
- // MANAGED LAYERS
- // ==========================================================================
- function getManagedLayers() {
- try {
- const managed =
- viewer &&
- viewer.layerManager &&
- viewer.layerManager
- .managedLayers;
- if (
- Array.isArray(
- managed
- )
- ) {
- return managed;
- }
- if (
- managed &&
- typeof managed[
- Symbol.iterator
- ] === 'function'
- ) {
- return Array.from(
- managed
- );
- }
- } catch (_) {
- // Ignore.
- }
- return [];
- }
- function findManagedLayerByName(
- name
- ) {
- return (
- getManagedLayers().find(
- layer =>
- layer &&
- layer.name === name
- ) ||
- null
- );
- }
- // ==========================================================================
- // TRACKABLE VALUES
- // ==========================================================================
- function readTrackable(
- trackable
- ) {
- if (
- trackable === undefined ||
- trackable === null
- ) {
- return undefined;
- }
- /*
- * Most Neuroglancer TrackableValue objects expose .value.
- */
- try {
- if (
- typeof trackable === 'object' &&
- 'value' in trackable
- ) {
- return clone(
- trackable.value
- );
- }
- } catch (_) {
- // Continue.
- }
- /*
- * Other trackables expose toJSON().
- */
- try {
- if (
- typeof trackable.toJSON === 'function'
- ) {
- const json =
- trackable.toJSON();
- if (
- json !== undefined
- ) {
- return clone(
- json
- );
- }
- }
- } catch (_) {
- // Continue.
- }
- return undefined;
- }
- function setTrackable(
- trackable,
- value,
- label
- ) {
- if (
- trackable === undefined ||
- trackable === null
- ) {
- return false;
- }
- /*
- * Preferred Neuroglancer mechanism.
- */
- if (
- typeof trackable.restoreState === 'function'
- ) {
- try {
- trackable.restoreState(
- clone(
- value
- )
- );
- console.debug(
- `[Import/export settings] Restored ${label} via restoreState().`
- );
- return true;
- } catch (error) {
- console.debug(
- `[Import/export settings] restoreState() failed for ${label}:`,
- error
- );
- }
- }
- /*
- * Fallback for TrackableValue.
- */
- try {
- if (
- typeof trackable === 'object' &&
- 'value' in trackable
- ) {
- trackable.value =
- clone(
- value
- );
- console.debug(
- `[Import/export settings] Restored ${label} via .value.`
- );
- return true;
- }
- } catch (error) {
- console.debug(
- `[Import/export settings] .value failed for ${label}:`,
- error
- );
- }
- return false;
- }
- // ==========================================================================
- // SHOW DEFAULT ANNOTATIONS
- // ==========================================================================
- /*
- * Read the live viewer trackable if possible.
- *
- * We still fall back to state.showDefaultAnnotations.
- */
- function readShowDefaultAnnotations(
- state
- ) {
- const candidates = [];
- try {
- candidates.push(
- viewer.showDefaultAnnotations
- );
- } catch (_) {
- // Ignore.
- }
- try {
- candidates.push(
- viewer.state &&
- viewer.state.viewer &&
- viewer.state.viewer
- .showDefaultAnnotations
- );
- } catch (_) {
- // Ignore.
- }
- try {
- candidates.push(
- viewer.state &&
- viewer.state
- .showDefaultAnnotations
- );
- } catch (_) {
- // Ignore.
- }
- for (
- const candidate of
- candidates
- ) {
- const value =
- readTrackable(
- candidate
- );
- if (
- typeof value === 'boolean'
- ) {
- return value;
- }
- }
- if (
- state &&
- own(
- state,
- 'showDefaultAnnotations'
- )
- ) {
- return Boolean(
- state.showDefaultAnnotations
- );
- }
- /*
- * Neuroglancer's default is true.
- *
- * If it is omitted from JSON, explicitly save true.
- */
- return true;
- }
- function applyShowDefaultAnnotations(
- enabled
- ) {
- enabled =
- Boolean(
- enabled
- );
- const candidates = [];
- try {
- candidates.push({
- label:
- 'viewer.showDefaultAnnotations',
- value:
- viewer.showDefaultAnnotations
- });
- } catch (_) {
- // Ignore.
- }
- try {
- candidates.push({
- label:
- 'viewer.state.viewer.showDefaultAnnotations',
- value:
- viewer.state &&
- viewer.state.viewer &&
- viewer.state.viewer
- .showDefaultAnnotations
- });
- } catch (_) {
- // Ignore.
- }
- try {
- candidates.push({
- label:
- 'viewer.state.showDefaultAnnotations',
- value:
- viewer.state &&
- viewer.state
- .showDefaultAnnotations
- });
- } catch (_) {
- // Ignore.
- }
- for (
- const candidate of
- candidates
- ) {
- if (
- setTrackable(
- candidate.value,
- enabled,
- candidate.label
- )
- ) {
- console.log(
- '[Import/export settings] Show default annotations set to:',
- enabled
- );
- return true;
- }
- }
- /*
- * Final fallback:
- *
- * Restore a tiny viewer-state patch containing ONLY this property.
- *
- * We deliberately do NOT pass layers, position, layout, selection,
- * zoom, or anything else.
- */
- try {
- viewer.state.restoreState({
- showDefaultAnnotations:
- enabled
- });
- console.log(
- '[Import/export settings] Show default annotations restored with top-level state patch:',
- enabled
- );
- return true;
- } catch (error) {
- console.error(
- '[Import/export settings] Could not restore showDefaultAnnotations:',
- error
- );
- return false;
- }
- }
- // ==========================================================================
- // SEGMENTATION EXPORT
- // ==========================================================================
- function enrichSegmentationExport(
- layer
- ) {
- if (
- !layer ||
- layer.type !== 'segmentation'
- ) {
- return;
- }
- const managedLayer =
- findManagedLayerByName(
- layer.name
- );
- const userLayer =
- managedLayer &&
- managedLayer.layer;
- const displayState =
- userLayer &&
- userLayer.displayState;
- for (
- const key of
- SEGMENTATION_DISPLAY_KEYS
- ) {
- let value;
- /*
- * First read live displayState.
- */
- try {
- if (
- displayState &&
- displayState[key] !== undefined
- ) {
- value =
- readTrackable(
- displayState[key]
- );
- }
- } catch (_) {
- // Ignore.
- }
- /*
- * Then try the UserLayer itself.
- */
- if (
- value === undefined
- ) {
- try {
- if (
- userLayer &&
- userLayer[key] !== undefined
- ) {
- value =
- readTrackable(
- userLayer[key]
- );
- }
- } catch (_) {
- // Ignore.
- }
- }
- /*
- * Then serialized JSON.
- */
- if (
- value === undefined &&
- own(
- layer,
- key
- )
- ) {
- value =
- clone(
- layer[key]
- );
- }
- /*
- * Finally explicit Neuroglancer default.
- */
- if (
- value === undefined &&
- own(
- SEGMENTATION_DEFAULTS,
- key
- )
- ) {
- value =
- clone(
- SEGMENTATION_DEFAULTS[key]
- );
- }
- if (
- value !== undefined
- ) {
- layer[key] =
- value;
- }
- }
- console.log(
- `[Import/export settings] Saving opacity/display for "${layer.name}":`,
- {
- selectedAlpha:
- layer.selectedAlpha,
- notSelectedAlpha:
- layer.notSelectedAlpha,
- objectAlpha:
- layer.objectAlpha
- }
- );
- }
- // ==========================================================================
- // CREATE PORTABLE PRESET
- // ==========================================================================
- function createPortableSettings() {
- let state;
- try {
- state =
- clone(
- viewer.state.toJSON()
- );
- } catch (error) {
- console.error(
- '[Import/export settings] Could not read viewer state:',
- error
- );
- window.alert(
- 'Could not read the Neuroglancer state.'
- );
- return null;
- }
- /*
- * Capture this explicitly BEFORE removing link-specific state.
- */
- state.showDefaultAnnotations =
- readShowDefaultAnnotations(
- state
- );
- console.log(
- '[Import/export settings] Saving showDefaultAnnotations:',
- state.showDefaultAnnotations
- );
- /*
- * Remove link/navigation-specific viewer state.
- */
- for (
- const key of
- FORBIDDEN_TOP_LEVEL_KEYS
- ) {
- delete state[key];
- }
- /*
- * Sanitize every layer.
- */
- if (
- Array.isArray(
- state.layers
- )
- ) {
- for (
- const layer of
- state.layers
- ) {
- if (!layer) {
- continue;
- }
- /*
- * Explicitly capture opacity/display defaults.
- */
- enrichSegmentationExport(
- layer
- );
- /*
- * Never transfer link-specific objects.
- */
- delete layer.segments;
- delete layer.annotations;
- /*
- * Never transfer local navigation.
- */
- delete layer.localPosition;
- delete layer.localDimensions;
- delete layer.localVelocity;
- /*
- * Never transfer active layer panel/tab.
- */
- delete layer.tab;
- delete layer.panels;
- }
- }
- return state;
- }
- // ==========================================================================
- // EXPORT SLOT CHOICE
- // ==========================================================================
- function chooseExportSlot(
- store
- ) {
- const lines =
- [];
- for (
- let i = 0;
- i < SLOT_COUNT;
- i++
- ) {
- const slot =
- store.slots[i];
- lines.push(
- slot
- ? `${i + 1}. ${slot.name}`
- : `${i + 1}. [empty]`
- );
- }
- const answer =
- window.prompt(
- 'Choose a slot for the current settings:\n\n' +
- lines.join('\n') +
- '\n\nEnter 1, 2, or 3:',
- '1'
- );
- if (
- answer === null
- ) {
- return null;
- }
- const number =
- Number(
- String(
- answer
- ).trim()
- );
- if (
- !Number.isInteger(
- number
- ) ||
- number < 1 ||
- number > SLOT_COUNT
- ) {
- window.alert(
- 'Please enter 1, 2, or 3.'
- );
- return null;
- }
- return (
- number - 1
- );
- }
- // ==========================================================================
- // EXPORT
- // ==========================================================================
- function exportSettings() {
- const store =
- loadStore();
- const slotIndex =
- chooseExportSlot(
- store
- );
- if (
- slotIndex === null
- ) {
- return false;
- }
- const existing =
- store.slots[
- slotIndex
- ];
- if (
- existing &&
- !window.confirm(
- `Slot ${slotIndex + 1} currently contains:\n\n` +
- `"${existing.name}"\n\n` +
- 'Replace it?'
- )
- ) {
- return false;
- }
- const enteredName =
- window.prompt(
- `Name Slot ${slotIndex + 1}:`,
- existing
- ? existing.name
- : `Preset ${slotIndex + 1}`
- );
- if (
- enteredName === null
- ) {
- return false;
- }
- const settings =
- createPortableSettings();
- if (!settings) {
- return false;
- }
- const name =
- normalizeName(
- enteredName,
- `Preset ${slotIndex + 1}`
- );
- store.slots[
- slotIndex
- ] = {
- name,
- settings,
- updatedAt:
- new Date()
- .toISOString()
- };
- if (
- !saveStore(
- store
- )
- ) {
- return false;
- }
- console.log(
- `[Import/export settings] Exported Slot ${slotIndex + 1}: "${name}".`
- );
- window.alert(
- `Settings exported.\n\n` +
- `Slot ${slotIndex + 1}: ${name}\n` +
- `Show default annotations: ` +
- (
- settings.showDefaultAnnotations
- ? 'ON'
- : 'OFF'
- )
- );
- return true;
- }
- // ==========================================================================
- // IMPORT SLOT CHOICE
- // ==========================================================================
- function chooseImportSlot(
- store,
- quietWhenEmpty
- ) {
- const slots =
- getFilledSlots(
- store
- );
- if (
- slots.length === 0
- ) {
- if (
- !quietWhenEmpty
- ) {
- window.alert(
- 'No exported settings are saved.'
- );
- }
- return null;
- }
- /*
- * Exactly one saved preset:
- * no question — import automatically.
- */
- if (
- slots.length === 1
- ) {
- console.log(
- `[Import/export settings] Only one preset exists; automatically selecting "${slots[0].name}".`
- );
- return slots[0].index;
- }
- /*
- * Multiple presets:
- * ask which one.
- */
- const answer =
- window.prompt(
- 'Choose the settings state to import:\n\n' +
- slots
- .map(
- item =>
- `${item.number}. ${item.name}`
- )
- .join('\n') +
- '\n\nEnter the slot number:',
- String(
- slots[0].number
- )
- );
- if (
- answer === null
- ) {
- return null;
- }
- const number =
- Number(
- String(
- answer
- ).trim()
- );
- const selected =
- slots.find(
- item =>
- item.number === number
- );
- if (!selected) {
- window.alert(
- 'That slot is not available.'
- );
- return null;
- }
- return selected.index;
- }
- // ==========================================================================
- // SEGMENTATION IMPORT
- // ==========================================================================
- function getSavedSegmentationValue(
- savedLayer,
- key
- ) {
- if (
- own(
- savedLayer,
- key
- )
- ) {
- return clone(
- savedLayer[key]
- );
- }
- if (
- own(
- SEGMENTATION_DEFAULTS,
- key
- )
- ) {
- return clone(
- SEGMENTATION_DEFAULTS[key]
- );
- }
- return undefined;
- }
- function applySegmentationDisplaySettings(
- managedLayer,
- savedLayer
- ) {
- if (
- !managedLayer ||
- !managedLayer.layer
- ) {
- return 0;
- }
- const userLayer =
- managedLayer.layer;
- const displayState =
- userLayer.displayState;
- let applied =
- 0;
- for (
- const key of
- SEGMENTATION_DISPLAY_KEYS
- ) {
- const value =
- getSavedSegmentationValue(
- savedLayer,
- key
- );
- if (
- value === undefined
- ) {
- continue;
- }
- let success =
- false;
- /*
- * Preferred target.
- */
- try {
- if (
- displayState &&
- displayState[key] !== undefined
- ) {
- success =
- setTrackable(
- displayState[key],
- value,
- `${managedLayer.name}.${key}`
- );
- }
- } catch (_) {
- // Continue.
- }
- /*
- * Fallback.
- */
- if (!success) {
- try {
- if (
- userLayer[key] !== undefined
- ) {
- success =
- setTrackable(
- userLayer[key],
- value,
- `${managedLayer.name}.${key}`
- );
- }
- } catch (_) {
- // Ignore.
- }
- }
- if (success) {
- applied++;
- }
- }
- console.log(
- `[Import/export settings] "${managedLayer.name}" opacity import:`,
- {
- selectedAlpha:
- getSavedSegmentationValue(
- savedLayer,
- 'selectedAlpha'
- ),
- notSelectedAlpha:
- getSavedSegmentationValue(
- savedLayer,
- 'notSelectedAlpha'
- ),
- objectAlpha:
- getSavedSegmentationValue(
- savedLayer,
- 'objectAlpha'
- )
- }
- );
- return applied;
- }
- // ==========================================================================
- // GENERIC LAYER SETTINGS
- // ==========================================================================
- function makeGenericPatch(
- savedLayer
- ) {
- const patch =
- {};
- for (
- const [
- key,
- value
- ] of
- Object.entries(
- savedLayer
- )
- ) {
- /*
- * Never touch unsafe/link-specific layer properties.
- */
- if (
- FORBIDDEN_LAYER_KEYS.has(
- key
- )
- ) {
- continue;
- }
- /*
- * Segmentation values have their own explicit importer.
- */
- if (
- SEGMENTATION_DISPLAY_KEYS.includes(
- key
- )
- ) {
- continue;
- }
- patch[key] =
- clone(
- value
- );
- }
- return patch;
- }
- function applyGenericTrackables(
- target,
- patch,
- path
- ) {
- if (
- !target ||
- !patch ||
- typeof patch !== 'object'
- ) {
- return 0;
- }
- let applied =
- 0;
- for (
- const [
- key,
- value
- ] of
- Object.entries(
- patch
- )
- ) {
- let property;
- try {
- property =
- target[key];
- } catch (_) {
- continue;
- }
- if (
- property === undefined ||
- property === null
- ) {
- continue;
- }
- if (
- setTrackable(
- property,
- value,
- `${path}.${key}`
- )
- ) {
- applied++;
- continue;
- }
- /*
- * Recurse into nested display-state objects.
- */
- if (
- value &&
- typeof value === 'object' &&
- !Array.isArray(
- value
- ) &&
- property &&
- typeof property === 'object'
- ) {
- applied +=
- applyGenericTrackables(
- property,
- value,
- `${path}.${key}`
- );
- }
- }
- return applied;
- }
- function applyGenericDisplaySettings(
- managedLayer,
- savedLayer
- ) {
- if (
- !managedLayer ||
- !managedLayer.layer
- ) {
- return 0;
- }
- const patch =
- makeGenericPatch(
- savedLayer
- );
- let applied =
- 0;
- applied +=
- applyGenericTrackables(
- managedLayer.layer,
- patch,
- managedLayer.name
- );
- if (
- managedLayer.layer
- .displayState
- ) {
- applied +=
- applyGenericTrackables(
- managedLayer.layer
- .displayState,
- patch,
- `${managedLayer.name}.displayState`
- );
- }
- return applied;
- }
- // ==========================================================================
- // IMPORT SETTINGS OBJECT
- // ==========================================================================
- function importSettingsObject(
- savedSettings,
- presetName,
- slotIndex
- ) {
- if (
- !savedSettings ||
- typeof savedSettings !== 'object'
- ) {
- return false;
- }
- const currentState =
- getCurrentState();
- if (!currentState) {
- return false;
- }
- const savedLayers =
- Array.isArray(
- savedSettings.layers
- )
- ? savedSettings.layers
- : [];
- let matched =
- 0;
- let genericProperties =
- 0;
- let segmentationProperties =
- 0;
- /*
- * ------------------------------------------------------------------------
- * STEP 1
- *
- * Restore global "Show default annotations".
- *
- * This is NOT a layer source setting.
- * ------------------------------------------------------------------------
- */
- let defaultAnnotationsApplied =
- false;
- if (
- own(
- savedSettings,
- 'showDefaultAnnotations'
- )
- ) {
- defaultAnnotationsApplied =
- applyShowDefaultAnnotations(
- savedSettings
- .showDefaultAnnotations
- );
- }
- /*
- * ------------------------------------------------------------------------
- * STEP 2
- *
- * Apply saved display settings to EXISTING layers only.
- *
- * We never replace viewer.state.layers.
- * ------------------------------------------------------------------------
- */
- for (
- const savedLayer of
- savedLayers
- ) {
- if (
- !savedLayer ||
- !savedLayer.name
- ) {
- continue;
- }
- const managedLayer =
- findManagedLayerByName(
- savedLayer.name
- );
- const currentLayer =
- findCurrentLayerJson(
- currentState,
- savedLayer.name
- );
- if (
- !managedLayer ||
- !currentLayer
- ) {
- continue;
- }
- /*
- * Avoid applying a segmentation configuration to an image layer,
- * etc.
- */
- if (
- savedLayer.type &&
- currentLayer.type &&
- savedLayer.type !==
- currentLayer.type
- ) {
- console.warn(
- `[Import/export settings] Layer "${savedLayer.name}" type differs; skipped.`
- );
- continue;
- }
- matched++;
- /*
- * Generic visual settings.
- */
- genericProperties +=
- applyGenericDisplaySettings(
- managedLayer,
- savedLayer
- );
- /*
- * ----------------------------------------------------------------------
- * STEP 3
- *
- * Opacity/display values LAST.
- *
- * This protects the opacity values from being reset by another
- * display-setting update.
- * ----------------------------------------------------------------------
- */
- if (
- savedLayer.type === 'segmentation'
- ) {
- segmentationProperties +=
- applySegmentationDisplaySettings(
- managedLayer,
- savedLayer
- );
- }
- }
- console.log(
- `[Import/export settings] SAFE import complete from Slot ${slotIndex + 1}: "${presetName}". ` +
- `${matched} layer(s) matched; ` +
- `${genericProperties} other display properties applied; ` +
- `${segmentationProperties} segmentation display properties applied; ` +
- `showDefaultAnnotations=${String(
- savedSettings.showDefaultAnnotations
- )}, applied=${defaultAnnotationsApplied}.`
- );
- return true;
- }
- // ==========================================================================
- // IMPORT
- // ==========================================================================
- function importSettings(
- options = {}
- ) {
- const store =
- loadStore();
- const slotIndex =
- chooseImportSlot(
- store,
- options.quietWhenEmpty === true
- );
- if (
- slotIndex === null
- ) {
- return {
- ok:
- false,
- reason:
- 'cancelled-or-empty'
- };
- }
- const slot =
- store.slots[
- slotIndex
- ];
- if (
- !slot ||
- !slot.settings
- ) {
- return {
- ok:
- false,
- reason:
- 'missing-slot'
- };
- }
- console.log(
- `[Import/export settings] Importing Slot ${slotIndex + 1}: "${slot.name}".`
- );
- const ok =
- importSettingsObject(
- slot.settings,
- slot.name,
- slotIndex
- );
- return {
- ok,
- slotIndex,
- slotNumber:
- slotIndex + 1,
- name:
- slot.name
- };
- }
- // ==========================================================================
- // MANAGE PRESETS
- // ==========================================================================
- function manageSlots() {
- const store =
- loadStore();
- const lines =
- Array.from(
- {
- length:
- SLOT_COUNT
- },
- (_, i) => {
- const slot =
- store.slots[i];
- return (
- slot
- ? `${i + 1}. ${slot.name}`
- : `${i + 1}. [empty]`
- );
- }
- );
- const answer =
- window.prompt(
- 'Saved settings:\n\n' +
- lines.join('\n') +
- '\n\n' +
- 'R1 / R2 / R3 = rename\n' +
- 'D1 / D2 / D3 = delete',
- ''
- );
- if (
- answer === null
- ) {
- return;
- }
- const match =
- String(
- answer
- )
- .trim()
- .toUpperCase()
- .match(
- /^([RD])([1-3])$/
- );
- if (!match) {
- return;
- }
- const action =
- match[1];
- const index =
- Number(
- match[2]
- ) - 1;
- const slot =
- store.slots[index];
- if (!slot) {
- window.alert(
- `Slot ${index + 1} is empty.`
- );
- return;
- }
- /*
- * Rename.
- */
- if (
- action === 'R'
- ) {
- const name =
- window.prompt(
- `Rename Slot ${index + 1}:`,
- slot.name
- );
- if (
- name === null
- ) {
- return;
- }
- slot.name =
- normalizeName(
- name,
- `Preset ${index + 1}`
- );
- saveStore(
- store
- );
- return;
- }
- /*
- * Delete.
- */
- if (
- action === 'D'
- ) {
- if (
- !window.confirm(
- `Delete Slot ${index + 1}: "${slot.name}"?`
- )
- ) {
- return;
- }
- store.slots[index] =
- null;
- saveStore(
- store
- );
- }
- }
- // ==========================================================================
- // PUBLIC API FOR AUTO IMPORT
- // ==========================================================================
- window[
- API_NAME
- ] = {
- version:
- '2026-08-29.9',
- storageKey:
- STORAGE_KEY,
- getPresets:
- function () {
- return getFilledSlots(
- loadStore()
- ).map(
- item => ({
- slot:
- item.number,
- name:
- item.name
- })
- );
- },
- importSettings:
- function (
- options
- ) {
- return importSettings(
- options ||
- {}
- );
- },
- exportSettings,
- manageSlots
- };
- console.log(
- '[Import/export settings] SAFE preset API v2026-08-29.9 ready.'
- );
- // ==========================================================================
- // RIGHT-CLICK MENU + KEYBOARD SHORTCUTS
- // ==========================================================================
- let topBar;
- function addMenu() {
- const menu =
- document.createElement(
- 'div'
- );
- Object.assign(
- menu.style,
- {
- position:
- 'fixed',
- background:
- '#222',
- color:
- '#fff',
- border:
- '1px solid #555',
- padding:
- '5px 0',
- display:
- 'none',
- zIndex:
- '10000',
- minWidth:
- '190px',
- font:
- '13px Arial, sans-serif'
- }
- );
- function item(
- label,
- handler
- ) {
- const element =
- document.createElement(
- 'div'
- );
- element.textContent =
- label;
- element.style.padding =
- '7px 10px';
- element.style.cursor =
- 'pointer';
- element.addEventListener(
- 'mouseenter',
- () => {
- element.style.background =
- '#444';
- }
- );
- element.addEventListener(
- 'mouseleave',
- () => {
- element.style.background =
- 'transparent';
- }
- );
- element.addEventListener(
- 'click',
- event => {
- event.stopPropagation();
- menu.style.display =
- 'none';
- handler();
- }
- );
- return element;
- }
- menu.appendChild(
- item(
- 'Export settings…',
- exportSettings
- )
- );
- menu.appendChild(
- item(
- 'Import settings…',
- () => {
- importSettings();
- }
- )
- );
- menu.appendChild(
- item(
- 'Manage saved settings…',
- manageSlots
- )
- );
- document.body.appendChild(
- menu
- );
- /*
- * Right-click Neuroglancer top bar.
- */
- topBar.addEventListener(
- 'contextmenu',
- event => {
- event.preventDefault();
- menu.style.left =
- `${event.clientX}px`;
- menu.style.top =
- `${event.clientY}px`;
- menu.style.display =
- 'block';
- }
- );
- document.addEventListener(
- 'click',
- () => {
- menu.style.display =
- 'none';
- }
- );
- /*
- * X = export
- * / = import
- */
- document.body.addEventListener(
- 'keyup',
- event => {
- const target =
- event.target;
- const tag =
- target &&
- target.tagName
- ? target.tagName
- .toLowerCase()
- : '';
- if (
- tag === 'input' ||
- tag === 'textarea' ||
- (
- target &&
- target.isContentEditable
- )
- ) {
- return;
- }
- if (
- event.key === 'x' ||
- event.key === 'X'
- ) {
- exportSettings();
- return;
- }
- if (
- event.key === '/'
- ) {
- importSettings();
- }
- }
- );
- }
- // ==========================================================================
- // START
- // ==========================================================================
- loadStore();
- const wait =
- window.setInterval(
- () => {
- topBar =
- document.querySelector(
- '.neuroglancer-viewer-top-row'
- );
- if (!topBar) {
- return;
- }
- window.clearInterval(
- wait
- );
- addMenu();
- },
- 100
- );
- })();
Add Comment
Please, Sign In to add comment