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-05
- // @description Exports NG settings and imports them while preserving current XYZ coordinates and 2D/3D zoom
- // @author KrzysztofKruk, modified
- // @match https://spelunker.cave-explorer.org/
- // @icon https://www.google.com/s2/favicons?sz=64&domain=cave-explorer.org
- // @grant none
- // ==/UserScript==
- /* global viewer */
- (function () {
- 'use strict'
- const STORAGE_KEY = 'ng-settings'
- function cloneValue(value) {
- if (value === undefined) {
- return undefined
- }
- return JSON.parse(JSON.stringify(value))
- }
- function exportSettings() {
- const settings = viewer.state.toJSON()
- settings.layers.forEach(layer => {
- if (layer.type === 'segmentation') {
- delete layer.segments
- }
- else if (layer.type === 'annotation') {
- delete layer.annotations
- }
- })
- localStorage.setItem(
- STORAGE_KEY,
- JSON.stringify(settings)
- )
- console.log(
- '[Import/export settings] Settings exported.'
- )
- }
- function importSettings() {
- const currentSettings = viewer.state.toJSON()
- const currentLayerContents = {}
- /*
- * Preserve values that belong to the newly opened task.
- *
- * position:
- * Current XYZ coordinates.
- *
- * dimensions:
- * Coordinate-space interpretation of XYZ.
- *
- * crossSectionScale:
- * 2D slice zoom.
- *
- * projectionScale:
- * 3D zoom.
- *
- * projectionDepth:
- * 3D camera depth.
- *
- * projectionOrientation:
- * Current 3D viewing direction.
- */
- const preservedTopLevelProperties = [
- 'dimensions',
- 'position',
- 'crossSectionScale',
- 'projectionScale',
- 'projectionDepth',
- 'projectionOrientation'
- ]
- const preservedValues = {}
- preservedTopLevelProperties.forEach(propertyName => {
- if (
- Object.prototype.hasOwnProperty.call(
- currentSettings,
- propertyName
- )
- ) {
- preservedValues[propertyName] =
- cloneValue(currentSettings[propertyName])
- }
- })
- /*
- * Preserve the currently selected segments and annotations.
- */
- currentSettings.layers.forEach(layer => {
- if (!layer || !layer.name) {
- return
- }
- if (layer.type === 'segmentation') {
- currentLayerContents[layer.name] = {
- type: 'segmentation',
- segments: Array.isArray(layer.segments)
- ? cloneValue(layer.segments)
- : []
- }
- }
- else if (layer.type === 'annotation') {
- currentLayerContents[layer.name] = {
- type: 'annotation',
- annotations: Array.isArray(layer.annotations)
- ? cloneValue(layer.annotations)
- : []
- }
- }
- })
- let settingsText =
- localStorage.getItem(STORAGE_KEY)
- if (!settingsText) {
- console.log(
- '[Import/export settings] Saved settings not found.'
- )
- return
- }
- let settings
- try {
- settings = JSON.parse(settingsText)
- }
- catch (error) {
- console.error(
- '[Import/export settings] Saved settings are not valid JSON:',
- error
- )
- return
- }
- if (
- !settings ||
- !Array.isArray(settings.layers)
- ) {
- console.error(
- '[Import/export settings] Saved settings do not contain a valid layer list.'
- )
- return
- }
- /*
- * Put the current task's segments and annotations into the imported state.
- */
- settings.layers.forEach(layer => {
- if (!layer || !layer.name) {
- return
- }
- const currentContents =
- currentLayerContents[layer.name]
- if (!currentContents) {
- return
- }
- if (
- layer.type === 'segmentation' &&
- currentContents.type === 'segmentation'
- ) {
- layer.segments =
- cloneValue(currentContents.segments)
- }
- else if (
- layer.type === 'annotation' &&
- currentContents.type === 'annotation'
- ) {
- layer.annotations =
- cloneValue(currentContents.annotations)
- }
- })
- /*
- * Replace the exported coordinates and zoom values with those from the
- * currently opened link before restoring the settings.
- */
- preservedTopLevelProperties.forEach(propertyName => {
- if (
- Object.prototype.hasOwnProperty.call(
- preservedValues,
- propertyName
- )
- ) {
- settings[propertyName] =
- cloneValue(preservedValues[propertyName])
- }
- else {
- delete settings[propertyName]
- }
- })
- try {
- viewer.state.restoreState(settings)
- /*
- * Reapply the preserved values after the first restoration.
- *
- * Some viewer properties can be recalculated as other settings restore,
- * so this second pass ensures the original XYZ and zoom remain in place.
- */
- const restoredSettings =
- viewer.state.toJSON()
- preservedTopLevelProperties.forEach(propertyName => {
- if (
- Object.prototype.hasOwnProperty.call(
- preservedValues,
- propertyName
- )
- ) {
- restoredSettings[propertyName] =
- cloneValue(preservedValues[propertyName])
- }
- else {
- delete restoredSettings[propertyName]
- }
- })
- viewer.state.restoreState(restoredSettings)
- console.log(
- '[Import/export settings] Settings imported while preserving ' +
- 'XYZ position and 2D/3D zoom.'
- )
- }
- catch (error) {
- console.error(
- '[Import/export settings] Import failed:',
- error
- )
- console.log(
- 'Current state:',
- JSON.stringify(currentSettings)
- )
- console.log(
- 'Imported state:',
- JSON.stringify(settings)
- )
- }
- }
- let target
- function addMenu() {
- const menu = document.createElement('div')
- menu.style.position = 'fixed'
- menu.style.background = '#222'
- menu.style.color = '#fff'
- menu.style.border = '1px solid #555'
- menu.style.padding = '5px 0'
- menu.style.display = 'none'
- menu.style.zIndex = 10000
- menu.style.minWidth = '120px'
- menu.style.fontSize = '13px'
- menu.style.fontFamily = 'Arial, sans-serif'
- function createItem(text, onClick) {
- const item = document.createElement('div')
- item.textContent = text
- item.style.padding = '7px 10px'
- item.style.cursor = 'pointer'
- item.addEventListener('mouseenter', () => {
- item.style.background = '#444'
- })
- item.addEventListener('mouseleave', () => {
- item.style.background = 'transparent'
- })
- item.addEventListener('click', () => {
- onClick()
- hideMenu()
- })
- return item
- }
- function hideMenu() {
- menu.style.display = 'none'
- }
- menu.appendChild(
- createItem(
- 'Export settings',
- () => {
- if (
- window.confirm(
- 'Do you want to export current settings and potentially replace the old ones?'
- )
- ) {
- exportSettings()
- }
- }
- )
- )
- menu.appendChild(
- createItem(
- 'Import settings',
- () => {
- importSettings()
- }
- )
- )
- document.body.appendChild(menu)
- target.addEventListener(
- 'contextmenu',
- event => {
- menu.style.left =
- event.clientX + 'px'
- menu.style.top =
- event.clientY + 'px'
- menu.style.display = 'block'
- }
- )
- document.addEventListener(
- 'click',
- hideMenu
- )
- window.addEventListener(
- 'blur',
- hideMenu
- )
- document.body.addEventListener(
- 'keyup',
- event => {
- switch (event.key) {
- case 'x':
- exportSettings()
- break
- case '/':
- importSettings()
- break
- }
- }
- )
- }
- const waitForTopBar = setInterval(() => {
- target = document.querySelector(
- '.neuroglancer-viewer-top-row'
- )
- if (!target) {
- return
- }
- clearInterval(waitForTopBar)
- addMenu()
- }, 100)
- })()
Advertisement
Add Comment
Please, Sign In to add comment