From 16b17a115e2efaa92831e23c4dd530ebd9c9c5ee Mon Sep 17 00:00:00 2001 From: franmagn Date: Fri, 10 Oct 2025 21:00:47 +0200 Subject: [PATCH 01/34] feat: :sparkles: Add menu section and basic logic for annotationPanel This new menu section includes a button to add new annotations and lists already-existing annotations --- src/AnnotationControl/annotationPanel.js | 150 +++++++++++++++++++++++ src/potreeViewer.js | 2 + 2 files changed, 152 insertions(+) create mode 100644 src/AnnotationControl/annotationPanel.js diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js new file mode 100644 index 0000000..2d5bbb1 --- /dev/null +++ b/src/AnnotationControl/annotationPanel.js @@ -0,0 +1,150 @@ +/** + * Annotations Panel + * Injects a custom Annotations section for storing camera positions, + * that will (later) be used for letting users rapidly jump to saved views. + */ +export function initAnnotationsPanel(viewer) { + // Container management + const existingListContainer = document.getElementById('annotations_list'); + let targetContainer = existingListContainer; + if (!targetContainer) { + const menu = document.getElementById('potree_menu'); + if (menu) { + const header = document.createElement('h3'); + header.id = 'menu_camera_annotations'; + const headerSpan = document.createElement('span'); + headerSpan.textContent = 'Annotations'; + header.appendChild(headerSpan); + + const panel = document.createElement('div'); + panel.className = 'pv-menu-list annotations-panel'; + + const listContainerDiv = document.createElement('div'); + listContainerDiv.id = 'annotations_list'; + listContainerDiv.className = 'auto'; + panel.appendChild(listContainerDiv); + + // Insert after measurement panel but before tools, or at end if not found + const measurements = document.querySelector('.measurements-panel'); + if (measurements) { + menu.insertBefore(panel, measurements.nextSibling); + menu.insertBefore(header, panel); + } else { + menu.appendChild(header); + menu.appendChild(panel); + } + + if ($(menu).accordion) { + try { + $(menu).accordion('refresh'); + } catch (e) {} + } + + // Toggle collapse + header.addEventListener('click', () => { + if ($(menu).accordion && $(menu).data('uiAccordion')) return; + if (window.jQuery) { + const $p = window.jQuery(panel); + $p.is(':visible') ? $p.slideUp(350) : $p.slideDown(350); + return; + } + }); + targetContainer = panel.querySelector('#annotations_list'); + } + } + if (!targetContainer) { + console.warn('Annotations list container not found and dynamic injection failed'); + return; + } + + // UI update + function updateAnnotationsList() { + // Implementation for listing annotations + targetContainer.innerHTML = ''; + const annotationsTree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); + if (!annotationsTree) return; + let annotationsRoot = annotationsTree.get_json('annotations'); + if (!annotationsRoot || !annotationsRoot.children || annotationsRoot.children.length === 0) { + const empty = document.createElement('div'); + empty.className = 'annotation-empty'; + empty.textContent = 'No annotations yet'; + targetContainer.appendChild(empty); + return; + } + annotationsRoot.children.forEach(node => { + const row = document.createElement('div'); + row.className = 'annotation-row'; + + const label = document.createElement('span'); + label.textContent = node.text || 'Unnamed'; + label.className = 'annotation-label'; + + // Jump button (to be implemented) + /* + const jumpBtn = document.createElement('button'); + jumpBtn.textContent = 'Jump'; + jumpBtn.title = 'Move to this position'; + jumpBtn.onclick = () => { + // Implement jumping logic if applicable + }; + row.appendChild(jumpBtn); + */ + + // Delete button + const delBtn = document.createElement('button'); + delBtn.textContent = 'x'; + delBtn.title = 'Delete annotation'; + delBtn.onclick = () => { + annotationsTree.delete_node(node.id); + updateAnnotationsList(); + }; + + row.appendChild(label); + row.appendChild(delBtn); + targetContainer.appendChild(row); + }); + } + + // Add Annotation UI (mimics Potree toolbar/pinpoint-button logic) + function createAddButton() { + const btn = document.createElement('button'); + btn.textContent = '+ Add Annotation'; + btn.style.margin = '8px 0'; + btn.onclick = () => { + // Show this panel, if collapsed + const menu = document.getElementById('potree_menu'); + const annotationHeader = document.getElementById('menu_camera_annotations'); + if (annotationHeader && annotationHeader.nextElementSibling) { + $(annotationHeader.nextElementSibling).slideDown(); + } + // Start Potree annotation insertion + let annotation = viewer.annotationTool.startInsertion(); + + // Wait for annotation creation, then select in jsTree + setTimeout(() => { + let tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); + if (!tree || !annotation) return; + let annotationsRoot = tree.get_json("annotations"); + let jsonNode = annotationsRoot && + annotationsRoot.children && + annotationsRoot.children.find(child => child.data.uuid === annotation.uuid); + if (jsonNode) { + $.jstree.reference(jsonNode.id).deselect_all(); + $.jstree.reference(jsonNode.id).select_node(jsonNode.id); + } + updateAnnotationsList(); + }, 200); // A short delay for Potree/jsTree to update + }; + targetContainer.parentElement.insertBefore(btn, targetContainer); + } + + createAddButton(); + updateAnnotationsList(); + + // Listen to Potree's annotation events to auto-refresh our list + if (viewer.scene && viewer.scene.annotations) { + viewer.scene.annotations.addEventListener('annotation_added', updateAnnotationsList); + viewer.scene.annotations.addEventListener('annotation_removed', updateAnnotationsList); + viewer.scene.annotations.addEventListener('annotation_changed', updateAnnotationsList); + } +} diff --git a/src/potreeViewer.js b/src/potreeViewer.js index fb709f0..24fb4c9 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,3 +1,4 @@ +import { initAnnotationsPanel } from './AnnotationControl/AnnotationPanel.js' import { initElevationControls } from './ElevationControl/elevationControl.js' import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js' import { ecef } from './config.js' @@ -37,6 +38,7 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { initElevationControls(viewer) initMeasurementsPanel(viewer) + initAnnotationsPanel(viewer) }) const e = await Potree.loadPointCloud(pointcloudUrl) From 1cef52903b95e1fe460840e90a8ddeb29b8fdfe5 Mon Sep 17 00:00:00 2001 From: franmagn Date: Sun, 12 Oct 2025 19:25:25 +0200 Subject: [PATCH 02/34] feat: add logic behind jump and delete buttons, and add coordinates visualization Now is possible to jump between different saved positions (annotations) and to actually delete one. Also added the coordinates of the camera and of the pivot point (to be fixed) --- src/AnnotationControl/annotationPanel.js | 92 ++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 2d5bbb1..0f20776 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -58,6 +58,14 @@ export function initAnnotationsPanel(viewer) { } // UI update + // Helper: normalize different vector shapes into [x,y,z] (necessary for handling both Three.js Vector3 and serialized data stored in the jsTree) + function vecToArray(v) { + if (!v) return null; + if (Array.isArray(v)) return v; + if (typeof v.toArray === 'function') return v.toArray(); + if (v.x != null && v.y != null && v.z != null) return [v.x, v.y, v.z]; + return null; + } function updateAnnotationsList() { // Implementation for listing annotations targetContainer.innerHTML = ''; @@ -79,28 +87,83 @@ export function initAnnotationsPanel(viewer) { label.textContent = node.text || 'Unnamed'; label.className = 'annotation-label'; - // Jump button (to be implemented) - /* + // Jump button + const jumpBtn = document.createElement('button'); jumpBtn.textContent = 'Jump'; jumpBtn.title = 'Move to this position'; + // Move the viewer to the saved camera position/pivot for this annotation (if present) jumpBtn.onclick = () => { - // Implement jumping logic if applicable + const ann = node.data || {}; + const camPos = vecToArray(ann.cameraPosition || ann.camera_position || ann.cameraPos); + const annPos = vecToArray(ann.position || ann.annotationPosition || ann.pos); + + if (camPos && viewer && viewer.scene && viewer.scene.view && typeof viewer.scene.view.setView === 'function') { + const target = annPos || vecToArray(ann.cameraTarget) || viewer.scene.view.getPivot(); + viewer.scene.view.setView(camPos, target, 500); + } }; row.appendChild(jumpBtn); - */ + // Delete button const delBtn = document.createElement('button'); delBtn.textContent = 'x'; delBtn.title = 'Delete annotation'; + // Delete an annotation: remove live scene annotation (handles/DOM) then remove the tree node delBtn.onclick = () => { - annotationsTree.delete_node(node.id); + const annData = node.data || {}; + const uuid = annData.uuid; + + if (uuid && viewer && viewer.scene && viewer.scene.annotations) { + // Find the live annotation instance by UUID + let candidates = []; + try { candidates = (viewer.scene.annotations.flatten && viewer.scene.annotations.flatten()) || (viewer.scene.annotations.descendants && viewer.scene.annotations.descendants()) || viewer.scene.annotations.children || []; } catch (e) { candidates = viewer.scene.annotations.children || []; } + + let live = null; + if (Array.isArray(candidates)) { + for (let a of candidates) { + if (!a) continue; + if (a.uuid === uuid || (a.data && a.data.uuid === uuid)) { live = a; break; } + } + } + + if (live) { + if (typeof live.removeHandles === 'function') live.removeHandles(viewer); + if (typeof viewer.scene.removeAnnotation === 'function') { + viewer.scene.removeAnnotation(live); + } else if (viewer.scene.annotations && typeof viewer.scene.annotations.remove === 'function') { + viewer.scene.annotations.remove(live); + } + } + } + + // remove from sidebar/tree + try { $('#jstree_scene').jstree('delete_node', node.id); } catch (e) { try { $.jstree.reference(node.id).delete_node(node.id); } catch (_) {} } updateAnnotationsList(); }; row.appendChild(label); row.appendChild(delBtn); + // show saved camera info if present + try { + const ann = node.data || {}; + + const cam = vecToArray(ann.cameraPosition || ann.camera_position || ann.cameraPos); + const camTarget = vecToArray(ann.cameraTarget || ann.camera_target || ann.cameraPivot); + + if (cam || camTarget) { + const info = document.createElement('div'); + info.className = 'annotation-info'; + info.style.fontSize = '0.8em'; + info.style.marginLeft = '8px'; + const fmt = (v) => (v ? v.map(c => Number(c).toFixed(3)).join(', ') : '—'); + info.innerHTML = `Cam: ${fmt(cam)}
Pivot saved: ${fmt(camTarget)}`; + row.appendChild(info); + } + } catch (e) { + // ignore formatting errors + } targetContainer.appendChild(row); }); } @@ -117,6 +180,18 @@ export function initAnnotationsPanel(viewer) { if (annotationHeader && annotationHeader.nextElementSibling) { $(annotationHeader.nextElementSibling).slideDown(); } + // Capture current camera view (position + pivot) at the moment the user clicks Add + let camPos = null; + let camTarget = null; + try { + if (viewer && viewer.scene && viewer.scene.view) { + camPos = viewer.scene.view.position && typeof viewer.scene.view.position.toArray === 'function' ? viewer.scene.view.position.toArray() : (viewer.scene.view.position ? [viewer.scene.view.position.x, viewer.scene.view.position.y, viewer.scene.view.position.z] : null); + camTarget = viewer.scene.view.getPivot && typeof viewer.scene.view.getPivot === 'function' ? (viewer.scene.view.getPivot().toArray ? viewer.scene.view.getPivot().toArray() : [viewer.scene.view.getPivot().x, viewer.scene.view.getPivot().y, viewer.scene.view.getPivot().z]) : null; + } + } catch (e) { + console.warn('Could not read current view for annotation', e); + } + // Start Potree annotation insertion let annotation = viewer.annotationTool.startInsertion(); @@ -124,6 +199,13 @@ export function initAnnotationsPanel(viewer) { setTimeout(() => { let tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); if (!tree || !annotation) return; + // Persist captured camera info on the annotation object so it will be available in the jsTree node data + try { + if (camPos) annotation.cameraPosition = camPos; + if (camTarget) annotation.cameraTarget = camTarget; + } catch (e) { + console.warn('Could not attach camera info to annotation', e); + } let annotationsRoot = tree.get_json("annotations"); let jsonNode = annotationsRoot && annotationsRoot.children && From 1b5543f20f194ddaaed2082786f83063e98c5c72 Mon Sep 17 00:00:00 2001 From: franmagn Date: Mon, 13 Oct 2025 10:54:48 +0200 Subject: [PATCH 03/34] style(#4): ran prettier --- src/AnnotationControl/annotationPanel.js | 331 ++++++++++++++--------- 1 file changed, 206 insertions(+), 125 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 0f20776..1217bde 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -5,228 +5,309 @@ */ export function initAnnotationsPanel(viewer) { // Container management - const existingListContainer = document.getElementById('annotations_list'); - let targetContainer = existingListContainer; + const existingListContainer = document.getElementById('annotations_list') + let targetContainer = existingListContainer if (!targetContainer) { - const menu = document.getElementById('potree_menu'); + const menu = document.getElementById('potree_menu') if (menu) { - const header = document.createElement('h3'); - header.id = 'menu_camera_annotations'; - const headerSpan = document.createElement('span'); - headerSpan.textContent = 'Annotations'; - header.appendChild(headerSpan); + const header = document.createElement('h3') + header.id = 'menu_camera_annotations' + const headerSpan = document.createElement('span') + headerSpan.textContent = 'Annotations' + header.appendChild(headerSpan) - const panel = document.createElement('div'); - panel.className = 'pv-menu-list annotations-panel'; + const panel = document.createElement('div') + panel.className = 'pv-menu-list annotations-panel' - const listContainerDiv = document.createElement('div'); - listContainerDiv.id = 'annotations_list'; - listContainerDiv.className = 'auto'; - panel.appendChild(listContainerDiv); + const listContainerDiv = document.createElement('div') + listContainerDiv.id = 'annotations_list' + listContainerDiv.className = 'auto' + panel.appendChild(listContainerDiv) // Insert after measurement panel but before tools, or at end if not found - const measurements = document.querySelector('.measurements-panel'); + const measurements = document.querySelector('.measurements-panel') if (measurements) { - menu.insertBefore(panel, measurements.nextSibling); - menu.insertBefore(header, panel); + menu.insertBefore(panel, measurements.nextSibling) + menu.insertBefore(header, panel) } else { - menu.appendChild(header); - menu.appendChild(panel); + menu.appendChild(header) + menu.appendChild(panel) } if ($(menu).accordion) { try { - $(menu).accordion('refresh'); + $(menu).accordion('refresh') } catch (e) {} } // Toggle collapse header.addEventListener('click', () => { - if ($(menu).accordion && $(menu).data('uiAccordion')) return; + if ($(menu).accordion && $(menu).data('uiAccordion')) return if (window.jQuery) { - const $p = window.jQuery(panel); - $p.is(':visible') ? $p.slideUp(350) : $p.slideDown(350); - return; + const $p = window.jQuery(panel) + $p.is(':visible') ? $p.slideUp(350) : $p.slideDown(350) + return } - }); - targetContainer = panel.querySelector('#annotations_list'); + }) + targetContainer = panel.querySelector('#annotations_list') } } if (!targetContainer) { - console.warn('Annotations list container not found and dynamic injection failed'); - return; + console.warn( + 'Annotations list container not found and dynamic injection failed' + ) + return } // UI update // Helper: normalize different vector shapes into [x,y,z] (necessary for handling both Three.js Vector3 and serialized data stored in the jsTree) function vecToArray(v) { - if (!v) return null; - if (Array.isArray(v)) return v; - if (typeof v.toArray === 'function') return v.toArray(); - if (v.x != null && v.y != null && v.z != null) return [v.x, v.y, v.z]; - return null; + if (!v) return null + if (Array.isArray(v)) return v + if (typeof v.toArray === 'function') return v.toArray() + if (v.x != null && v.y != null && v.z != null) return [v.x, v.y, v.z] + return null } function updateAnnotationsList() { // Implementation for listing annotations - targetContainer.innerHTML = ''; - const annotationsTree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); - if (!annotationsTree) return; - let annotationsRoot = annotationsTree.get_json('annotations'); - if (!annotationsRoot || !annotationsRoot.children || annotationsRoot.children.length === 0) { - const empty = document.createElement('div'); - empty.className = 'annotation-empty'; - empty.textContent = 'No annotations yet'; - targetContainer.appendChild(empty); - return; + targetContainer.innerHTML = '' + const annotationsTree = + $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (!annotationsTree) return + let annotationsRoot = annotationsTree.get_json('annotations') + if ( + !annotationsRoot || + !annotationsRoot.children || + annotationsRoot.children.length === 0 + ) { + const empty = document.createElement('div') + empty.className = 'annotation-empty' + empty.textContent = 'No annotations yet' + targetContainer.appendChild(empty) + return } - annotationsRoot.children.forEach(node => { - const row = document.createElement('div'); - row.className = 'annotation-row'; + annotationsRoot.children.forEach((node) => { + const row = document.createElement('div') + row.className = 'annotation-row' - const label = document.createElement('span'); - label.textContent = node.text || 'Unnamed'; - label.className = 'annotation-label'; + const label = document.createElement('span') + label.textContent = node.text || 'Unnamed' + label.className = 'annotation-label' // Jump button - - const jumpBtn = document.createElement('button'); - jumpBtn.textContent = 'Jump'; - jumpBtn.title = 'Move to this position'; + + const jumpBtn = document.createElement('button') + jumpBtn.textContent = 'Jump' + jumpBtn.title = 'Move to this position' // Move the viewer to the saved camera position/pivot for this annotation (if present) jumpBtn.onclick = () => { - const ann = node.data || {}; - const camPos = vecToArray(ann.cameraPosition || ann.camera_position || ann.cameraPos); - const annPos = vecToArray(ann.position || ann.annotationPosition || ann.pos); + const ann = node.data || {} + const camPos = vecToArray( + ann.cameraPosition || ann.camera_position || ann.cameraPos + ) + const annPos = vecToArray( + ann.position || ann.annotationPosition || ann.pos + ) - if (camPos && viewer && viewer.scene && viewer.scene.view && typeof viewer.scene.view.setView === 'function') { - const target = annPos || vecToArray(ann.cameraTarget) || viewer.scene.view.getPivot(); - viewer.scene.view.setView(camPos, target, 500); + if ( + camPos && + viewer && + viewer.scene && + viewer.scene.view && + typeof viewer.scene.view.setView === 'function' + ) { + const target = + annPos || + vecToArray(ann.cameraTarget) || + viewer.scene.view.getPivot() + viewer.scene.view.setView(camPos, target, 4000) } - }; - row.appendChild(jumpBtn); - + } + row.appendChild(jumpBtn) // Delete button - const delBtn = document.createElement('button'); - delBtn.textContent = 'x'; - delBtn.title = 'Delete annotation'; + const delBtn = document.createElement('button') + delBtn.textContent = 'x' + delBtn.title = 'Delete annotation' // Delete an annotation: remove live scene annotation (handles/DOM) then remove the tree node delBtn.onclick = () => { - const annData = node.data || {}; - const uuid = annData.uuid; + const annData = node.data || {} + const uuid = annData.uuid if (uuid && viewer && viewer.scene && viewer.scene.annotations) { // Find the live annotation instance by UUID - let candidates = []; - try { candidates = (viewer.scene.annotations.flatten && viewer.scene.annotations.flatten()) || (viewer.scene.annotations.descendants && viewer.scene.annotations.descendants()) || viewer.scene.annotations.children || []; } catch (e) { candidates = viewer.scene.annotations.children || []; } + let candidates = [] + try { + candidates = + (viewer.scene.annotations.flatten && + viewer.scene.annotations.flatten()) || + (viewer.scene.annotations.descendants && + viewer.scene.annotations.descendants()) || + viewer.scene.annotations.children || + [] + } catch (e) { + candidates = viewer.scene.annotations.children || [] + } - let live = null; + let live = null if (Array.isArray(candidates)) { for (let a of candidates) { - if (!a) continue; - if (a.uuid === uuid || (a.data && a.data.uuid === uuid)) { live = a; break; } + if (!a) continue + if (a.uuid === uuid || (a.data && a.data.uuid === uuid)) { + live = a + break + } } } if (live) { - if (typeof live.removeHandles === 'function') live.removeHandles(viewer); + if (typeof live.removeHandles === 'function') + live.removeHandles(viewer) if (typeof viewer.scene.removeAnnotation === 'function') { - viewer.scene.removeAnnotation(live); - } else if (viewer.scene.annotations && typeof viewer.scene.annotations.remove === 'function') { - viewer.scene.annotations.remove(live); + viewer.scene.removeAnnotation(live) + } else if ( + viewer.scene.annotations && + typeof viewer.scene.annotations.remove === 'function' + ) { + viewer.scene.annotations.remove(live) } } } // remove from sidebar/tree - try { $('#jstree_scene').jstree('delete_node', node.id); } catch (e) { try { $.jstree.reference(node.id).delete_node(node.id); } catch (_) {} } - updateAnnotationsList(); - }; + try { + $('#jstree_scene').jstree('delete_node', node.id) + } catch (e) { + try { + $.jstree.reference(node.id).delete_node(node.id) + } catch (_) {} + } + updateAnnotationsList() + } - row.appendChild(label); - row.appendChild(delBtn); + row.appendChild(label) + row.appendChild(delBtn) // show saved camera info if present try { - const ann = node.data || {}; + const ann = node.data || {} - const cam = vecToArray(ann.cameraPosition || ann.camera_position || ann.cameraPos); - const camTarget = vecToArray(ann.cameraTarget || ann.camera_target || ann.cameraPivot); + const cam = vecToArray( + ann.cameraPosition || ann.camera_position || ann.cameraPos + ) + const camTarget = vecToArray( + ann.cameraTarget || ann.camera_target || ann.cameraPivot + ) if (cam || camTarget) { - const info = document.createElement('div'); - info.className = 'annotation-info'; - info.style.fontSize = '0.8em'; - info.style.marginLeft = '8px'; - const fmt = (v) => (v ? v.map(c => Number(c).toFixed(3)).join(', ') : '—'); - info.innerHTML = `Cam: ${fmt(cam)}
Pivot saved: ${fmt(camTarget)}`; - row.appendChild(info); + const info = document.createElement('div') + info.className = 'annotation-info' + info.style.fontSize = '0.8em' + info.style.marginLeft = '8px' + const fmt = (v) => + v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—' + info.innerHTML = `Cam: ${fmt(cam)}
Pivot saved: ${fmt(camTarget)}` + row.appendChild(info) } } catch (e) { // ignore formatting errors } - targetContainer.appendChild(row); - }); + targetContainer.appendChild(row) + }) } // Add Annotation UI (mimics Potree toolbar/pinpoint-button logic) function createAddButton() { - const btn = document.createElement('button'); - btn.textContent = '+ Add Annotation'; - btn.style.margin = '8px 0'; + const btn = document.createElement('button') + btn.textContent = '+ Add Annotation' + btn.style.margin = '8px 0' btn.onclick = () => { // Show this panel, if collapsed - const menu = document.getElementById('potree_menu'); - const annotationHeader = document.getElementById('menu_camera_annotations'); + const menu = document.getElementById('potree_menu') + const annotationHeader = document.getElementById( + 'menu_camera_annotations' + ) if (annotationHeader && annotationHeader.nextElementSibling) { - $(annotationHeader.nextElementSibling).slideDown(); + $(annotationHeader.nextElementSibling).slideDown() } // Capture current camera view (position + pivot) at the moment the user clicks Add - let camPos = null; - let camTarget = null; + let camPos = null + let camTarget = null try { if (viewer && viewer.scene && viewer.scene.view) { - camPos = viewer.scene.view.position && typeof viewer.scene.view.position.toArray === 'function' ? viewer.scene.view.position.toArray() : (viewer.scene.view.position ? [viewer.scene.view.position.x, viewer.scene.view.position.y, viewer.scene.view.position.z] : null); - camTarget = viewer.scene.view.getPivot && typeof viewer.scene.view.getPivot === 'function' ? (viewer.scene.view.getPivot().toArray ? viewer.scene.view.getPivot().toArray() : [viewer.scene.view.getPivot().x, viewer.scene.view.getPivot().y, viewer.scene.view.getPivot().z]) : null; + camPos = + viewer.scene.view.position && + typeof viewer.scene.view.position.toArray === 'function' + ? viewer.scene.view.position.toArray() + : viewer.scene.view.position + ? [ + viewer.scene.view.position.x, + viewer.scene.view.position.y, + viewer.scene.view.position.z + ] + : null + camTarget = + viewer.scene.view.getPivot && + typeof viewer.scene.view.getPivot === 'function' + ? viewer.scene.view.getPivot().toArray + ? viewer.scene.view.getPivot().toArray() + : [ + viewer.scene.view.getPivot().x, + viewer.scene.view.getPivot().y, + viewer.scene.view.getPivot().z + ] + : null } } catch (e) { - console.warn('Could not read current view for annotation', e); + console.warn('Could not read current view for annotation', e) } // Start Potree annotation insertion - let annotation = viewer.annotationTool.startInsertion(); + let annotation = viewer.annotationTool.startInsertion() // Wait for annotation creation, then select in jsTree setTimeout(() => { - let tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); - if (!tree || !annotation) return; + let tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (!tree || !annotation) return // Persist captured camera info on the annotation object so it will be available in the jsTree node data try { - if (camPos) annotation.cameraPosition = camPos; - if (camTarget) annotation.cameraTarget = camTarget; + if (camPos) annotation.cameraPosition = camPos + if (camTarget) annotation.cameraTarget = camTarget } catch (e) { - console.warn('Could not attach camera info to annotation', e); + console.warn('Could not attach camera info to annotation', e) } - let annotationsRoot = tree.get_json("annotations"); - let jsonNode = annotationsRoot && - annotationsRoot.children && - annotationsRoot.children.find(child => child.data.uuid === annotation.uuid); + let annotationsRoot = tree.get_json('annotations') + let jsonNode = + annotationsRoot && + annotationsRoot.children && + annotationsRoot.children.find( + (child) => child.data.uuid === annotation.uuid + ) if (jsonNode) { - $.jstree.reference(jsonNode.id).deselect_all(); - $.jstree.reference(jsonNode.id).select_node(jsonNode.id); + $.jstree.reference(jsonNode.id).deselect_all() + $.jstree.reference(jsonNode.id).select_node(jsonNode.id) } - updateAnnotationsList(); - }, 200); // A short delay for Potree/jsTree to update - }; - targetContainer.parentElement.insertBefore(btn, targetContainer); + updateAnnotationsList() + }, 200) // A short delay for Potree/jsTree to update + } + targetContainer.parentElement.insertBefore(btn, targetContainer) } - createAddButton(); - updateAnnotationsList(); + createAddButton() + updateAnnotationsList() // Listen to Potree's annotation events to auto-refresh our list if (viewer.scene && viewer.scene.annotations) { - viewer.scene.annotations.addEventListener('annotation_added', updateAnnotationsList); - viewer.scene.annotations.addEventListener('annotation_removed', updateAnnotationsList); - viewer.scene.annotations.addEventListener('annotation_changed', updateAnnotationsList); + viewer.scene.annotations.addEventListener( + 'annotation_added', + updateAnnotationsList + ) + viewer.scene.annotations.addEventListener( + 'annotation_removed', + updateAnnotationsList + ) + viewer.scene.annotations.addEventListener( + 'annotation_changed', + updateAnnotationsList + ) } } From f92fb4c4e74c3b75baaf9c30d8e0479eeadfb0bd Mon Sep 17 00:00:00 2001 From: franmagn Date: Mon, 13 Oct 2025 12:43:21 +0200 Subject: [PATCH 04/34] style(#4): make the old annotation button invisible add annotationPanel.css file and just inserted the css code to make the original annotation button not visible in the toolbar --- index.html | 4 ++++ src/AnnotationControl/annotationPanel.css | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 src/AnnotationControl/annotationPanel.css diff --git a/index.html b/index.html index 9aee3f9..afd2eee 100644 --- a/index.html +++ b/index.html @@ -38,6 +38,10 @@ rel="stylesheet" href="/src/MeasurementControl/measurementsPanel.css" /> + diff --git a/src/AnnotationControl/annotationPanel.css b/src/AnnotationControl/annotationPanel.css new file mode 100644 index 0000000..f5a8398 --- /dev/null +++ b/src/AnnotationControl/annotationPanel.css @@ -0,0 +1,10 @@ +/* Hide Potree annotation icon specifically */ +/* This targets the that references annotation.svg. */ +img.button-icon[src$="/annotation.svg"]{ + display: none !important; + visibility: hidden !important; + width: 0 !important; + height: 0 !important; + margin: 0 !important; + padding: 0 !important; +} \ No newline at end of file From 052e946cd3dde748813ead16772cd576ff9a6126 Mon Sep 17 00:00:00 2001 From: franmagn Date: Mon, 13 Oct 2025 12:54:47 +0200 Subject: [PATCH 05/34] refactor(#4): changed texts from "annotations" to "saved locations" --- src/AnnotationControl/annotationPanel.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 1217bde..1136252 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -13,7 +13,7 @@ export function initAnnotationsPanel(viewer) { const header = document.createElement('h3') header.id = 'menu_camera_annotations' const headerSpan = document.createElement('span') - headerSpan.textContent = 'Annotations' + headerSpan.textContent = 'Saved Locations' header.appendChild(headerSpan) const panel = document.createElement('div') @@ -82,7 +82,7 @@ export function initAnnotationsPanel(viewer) { ) { const empty = document.createElement('div') empty.className = 'annotation-empty' - empty.textContent = 'No annotations yet' + empty.textContent = 'No saved positions yet' targetContainer.appendChild(empty) return } @@ -128,14 +128,14 @@ export function initAnnotationsPanel(viewer) { // Delete button const delBtn = document.createElement('button') delBtn.textContent = 'x' - delBtn.title = 'Delete annotation' - // Delete an annotation: remove live scene annotation (handles/DOM) then remove the tree node + delBtn.title = 'Delete saved position' + // Delete an annotation: remove annotation from the renderer scene (live annotation), then remove the tree node delBtn.onclick = () => { const annData = node.data || {} const uuid = annData.uuid if (uuid && viewer && viewer.scene && viewer.scene.annotations) { - // Find the live annotation instance by UUID + // Find the live annotation instance by UUID (Universally Unique Identifier) let candidates = [] try { candidates = @@ -205,7 +205,7 @@ export function initAnnotationsPanel(viewer) { info.style.marginLeft = '8px' const fmt = (v) => v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—' - info.innerHTML = `Cam: ${fmt(cam)}
Pivot saved: ${fmt(camTarget)}` + info.innerHTML = `Camera coordinates: ${fmt(cam)}
Pivot coordinates: ${fmt(camTarget)}` row.appendChild(info) } } catch (e) { @@ -218,7 +218,7 @@ export function initAnnotationsPanel(viewer) { // Add Annotation UI (mimics Potree toolbar/pinpoint-button logic) function createAddButton() { const btn = document.createElement('button') - btn.textContent = '+ Add Annotation' + btn.textContent = '+ Add a new location' btn.style.margin = '8px 0' btn.onclick = () => { // Show this panel, if collapsed @@ -295,7 +295,7 @@ export function initAnnotationsPanel(viewer) { createAddButton() updateAnnotationsList() - // Listen to Potree's annotation events to auto-refresh our list + // Listen to Potree's annotation events to auto-refresh the list if (viewer.scene && viewer.scene.annotations) { viewer.scene.annotations.addEventListener( 'annotation_added', From cec877435f27d6996897d36b592705effec26c53 Mon Sep 17 00:00:00 2001 From: franmagn Date: Mon, 13 Oct 2025 15:34:12 +0200 Subject: [PATCH 06/34] refactor(#4): add "Saved Point coordinates" to the list of annotations --- src/AnnotationControl/annotationPanel.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 1136252..1ccd379 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -198,14 +198,18 @@ export function initAnnotationsPanel(viewer) { ann.cameraTarget || ann.camera_target || ann.cameraPivot ) - if (cam || camTarget) { + const pointPos = vecToArray( + ann.position || ann.annotationPosition || ann.pos + ) + + if (cam || camTarget || pointPos) { const info = document.createElement('div') info.className = 'annotation-info' info.style.fontSize = '0.8em' info.style.marginLeft = '8px' const fmt = (v) => v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—' - info.innerHTML = `Camera coordinates: ${fmt(cam)}
Pivot coordinates: ${fmt(camTarget)}` + info.innerHTML = `Camera coordinates: ${fmt(cam)}
Pivot coordinates: ${fmt(camTarget)}
Saved Point coordinates: ${fmt(pointPos)}`; row.appendChild(info) } } catch (e) { From 53343ed0fd5f30cc2d5b8b7667dfac4263b8786c Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 14 Oct 2025 18:05:07 +0200 Subject: [PATCH 07/34] refactor(#4): add monotonic numbering (in sidebar) to created annotations add an increasing number to each new annotation created. As for now, only in the sidebar (jstree) --- src/AnnotationControl/annotationPanel.js | 60 +++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 1ccd379..3d47f8e 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -68,6 +68,41 @@ export function initAnnotationsPanel(viewer) { if (v.x != null && v.y != null && v.z != null) return [v.x, v.y, v.z] return null } + // Monotonic numbering: assign an increasing number to each UUID and never reuse numbers. + // Stored in-memory for the session; later we can persist to a file. + const _uuidToIndex = new Map() + let _nextIndex = 1 + + function _ensureIndexForUUID(uuid) { + if (!uuid) return null + if (!_uuidToIndex.has(uuid)) { + _uuidToIndex.set(uuid, _nextIndex++) + } + return _uuidToIndex.get(uuid) + } + + function _renameJSTreeNode(nodeId, text) { + try { + if (window.$ && $.jstree) { + if ($.jstree.reference) { + const ref = $.jstree.reference(nodeId) + if (ref && typeof ref.rename_node === 'function') { + ref.rename_node(nodeId, text) + return + } + } + // fallback to global selector + if ($('#jstree_scene') && $('#jstree_scene').jstree) { + try { + $('#jstree_scene').jstree('rename_node', nodeId, text) + return + } catch (e) {} + } + } + } catch (e) { + // ignore rename failures + } + } function updateAnnotationsList() { // Implementation for listing annotations targetContainer.innerHTML = '' @@ -86,12 +121,33 @@ export function initAnnotationsPanel(viewer) { targetContainer.appendChild(empty) return } + // Assign monotonic indices for any node UUIDs encountered and rename unlabeled nodes + try { + const items = (annotationsRoot.children || []).map((n) => ({ data: n.data || {}, node: n })) + for (const it of items) { + const uuid = (it.data && it.data.uuid) || null + if (!uuid) continue + const idx = _ensureIndexForUUID(uuid) + const text = (it.node && it.node.text) || '' + const shouldRename = !text || text.trim() === '' || text === 'Unnamed' || text === 'Annotation Title' + if (shouldRename && idx != null) { + const newName = `Annotation #${idx}` + _renameJSTreeNode(it.node.id, newName) + it.node.text = newName + } + } + } catch (e) { + // ignore failures; we just won't rename nodes in that case + } + + // Build list entries for each annotation node annotationsRoot.children.forEach((node) => { + const row = document.createElement('div') row.className = 'annotation-row' - const label = document.createElement('span') - label.textContent = node.text || 'Unnamed' + const label = document.createElement('span') + label.textContent = node.text || 'Unnamed' label.className = 'annotation-label' // Jump button From 1634ea853f56d91d9354fd52fd0bcb48ac896a1e Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 14 Oct 2025 18:18:10 +0200 Subject: [PATCH 08/34] refactor(#4): add monotonic numbering (in live scene) to created annotations add an increasing number to each new annotation created. Now works also in the live scene (for each UUID) --- src/AnnotationControl/annotationPanel.js | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 3d47f8e..79d1d9c 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -103,6 +103,27 @@ export function initAnnotationsPanel(viewer) { // ignore rename failures } } + + function _findLiveAnnotationByUUID(uuid) { + if (!uuid || !viewer || !viewer.scene || !viewer.scene.annotations) return null + try { + const coll = + (viewer.scene.annotations.flatten && viewer.scene.annotations.flatten()) || + (viewer.scene.annotations.descendants && viewer.scene.annotations.descendants()) || + viewer.scene.annotations.children || [] + for (const a of coll) { + if (!a) continue + if (a.uuid === uuid || (a.data && a.data.uuid === uuid)) return a + } + } catch (e) { + // fallback + try { + const coll2 = viewer.scene.annotations.children || [] + for (const a of coll2) if (a && (a.uuid === uuid || (a.data && a.data.uuid === uuid))) return a + } catch (e) {} + } + return null + } function updateAnnotationsList() { // Implementation for listing annotations targetContainer.innerHTML = '' @@ -134,6 +155,18 @@ export function initAnnotationsPanel(viewer) { const newName = `Annotation #${idx}` _renameJSTreeNode(it.node.id, newName) it.node.text = newName + // update live annotation object's title/name so in-scene label matches + try { + const live = _findLiveAnnotationByUUID(it.data.uuid) + if (live) { + if (typeof live.title !== 'undefined') live.title = newName + if (typeof live.name !== 'undefined') live.name = newName + // for Potree Annotation object, data.title or similar may be used + if (live.data) live.data.title = newName + } + } catch (e) { + // ignore live-update failures + } } } } catch (e) { From 94a7959f20b571cc265a75ecf7c9f0b5f06422f5 Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 14 Oct 2025 18:53:42 +0200 Subject: [PATCH 09/34] feat(#4): :memo: add possibility to rename the title of a saved position from the sidebar it is now possible to double-click on a saved position's name to rename it. The update is cascaded to the live-scene elements --- src/AnnotationControl/annotationPanel.js | 101 ++++++++++++++++++++--- 1 file changed, 91 insertions(+), 10 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 79d1d9c..5feb80e 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -69,7 +69,6 @@ export function initAnnotationsPanel(viewer) { return null } // Monotonic numbering: assign an increasing number to each UUID and never reuse numbers. - // Stored in-memory for the session; later we can persist to a file. const _uuidToIndex = new Map() let _nextIndex = 1 @@ -114,7 +113,7 @@ export function initAnnotationsPanel(viewer) { for (const a of coll) { if (!a) continue if (a.uuid === uuid || (a.data && a.data.uuid === uuid)) return a - } + } } catch (e) { // fallback try { @@ -161,7 +160,6 @@ export function initAnnotationsPanel(viewer) { if (live) { if (typeof live.title !== 'undefined') live.title = newName if (typeof live.name !== 'undefined') live.name = newName - // for Potree Annotation object, data.title or similar may be used if (live.data) live.data.title = newName } } catch (e) { @@ -170,18 +168,29 @@ export function initAnnotationsPanel(viewer) { } } } catch (e) { - // ignore failures; we just won't rename nodes in that case + // ignore failures } - // Build list entries for each annotation node - annotationsRoot.children.forEach((node) => { + // Build list entries for each annotation node + annotationsRoot.children.forEach((node) => { const row = document.createElement('div') row.className = 'annotation-row' - const label = document.createElement('span') - label.textContent = node.text || 'Unnamed' + const label = document.createElement('span') + label.textContent = node.text || 'Unnamed' label.className = 'annotation-label' + // attach uuid for editing + try { + const uuid = (node.data && node.data.uuid) || null + if (uuid) label.dataset.uuid = uuid + } catch (e) {} + // double-click label to edit + label.addEventListener('dblclick', (ev) => { + ev.stopPropagation() + const uuid = label.dataset.uuid + if (uuid) startInlineEditForUUID(uuid) + }) // Jump button @@ -209,7 +218,7 @@ export function initAnnotationsPanel(viewer) { annPos || vecToArray(ann.cameraTarget) || viewer.scene.view.getPivot() - viewer.scene.view.setView(camPos, target, 4000) + viewer.scene.view.setView(camPos, target, 4000) //animation duration in ms } } row.appendChild(jumpBtn) @@ -276,7 +285,7 @@ export function initAnnotationsPanel(viewer) { row.appendChild(label) row.appendChild(delBtn) - // show saved camera info if present + // show saved camera and point info try { const ann = node.data || {} @@ -308,6 +317,78 @@ export function initAnnotationsPanel(viewer) { }) } + // Start inline editing for a given annotation UUID (opens an input in the sidebar) + function startInlineEditForUUID(uuid) { + if (!uuid) return + // find the row label element + const labelEl = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) + if (!labelEl) return + const oldText = labelEl.textContent || '' + const input = document.createElement('input') + input.type = 'text' + input.value = oldText + input.className = 'annotation-edit-input' + labelEl.replaceWith(input) + input.focus() + input.select() + + function finish(commit) { + const newText = commit ? input.value.trim() || oldText : oldText + // restore label + const newLabel = document.createElement('span') + newLabel.className = 'annotation-label' + newLabel.textContent = newText + newLabel.dataset.uuid = uuid + newLabel.addEventListener('dblclick', (ev) => { + ev.stopPropagation() + startInlineEditForUUID(uuid) + }) + input.replaceWith(newLabel) + // commit to jsTree and live annotation + _commitEditedName(uuid, newText) + } + + input.addEventListener('blur', () => finish(true)) + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + finish(true) + } else if (e.key === 'Escape') { + finish(false) + } + }) + } + + function _commitEditedName(uuid, name) { + if (!uuid) return + // update jsTree node text + try { + const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (tree) { + const annotationsRoot = tree.get_json('annotations') + if (annotationsRoot && annotationsRoot.children) { + const node = annotationsRoot.children.find((c) => (c.data && c.data.uuid) === uuid) + if (node) { + _renameJSTreeNode(node.id, name) + node.text = name + } + } + } + } catch (e) {} + + // update live annotation + try { + const live = _findLiveAnnotationByUUID(uuid) + if (live) { + if (typeof live.title !== 'undefined') live.title = name + if (typeof live.name !== 'undefined') live.name = name + if (live.data) live.data.title = name + } + } catch (e) {} + + // refresh sidebar to reflect changes + setTimeout(updateAnnotationsList, 0) + } + // Add Annotation UI (mimics Potree toolbar/pinpoint-button logic) function createAddButton() { const btn = document.createElement('button') From 939af8d88633021817a0047a01841c34931b1617 Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 14 Oct 2025 19:10:45 +0200 Subject: [PATCH 10/34] fix(#4): :bug: address NotFoundError This fix solves the NotFoundError, throwed whenever there was an attempt of removing an already removed target --- src/AnnotationControl/annotationPanel.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 5feb80e..d1e86fb 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -343,7 +343,23 @@ export function initAnnotationsPanel(viewer) { ev.stopPropagation() startInlineEditForUUID(uuid) }) - input.replaceWith(newLabel) + // The input may have been removed already by an external update. + // Check connectivity before replacing to avoid DOMNotFound errors. + try { + if (input.isConnected) { + input.replaceWith(newLabel) + } else { + // If input no longer in DOM, ensure any existing label is updated instead + const existing = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) + if (existing) existing.textContent = newText + } + } catch (e) { + // ignore DOM replacement errors and try to update label if present + try { + const existing = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) + if (existing) existing.textContent = newText + } catch (_) {} + } // commit to jsTree and live annotation _commitEditedName(uuid, newText) } From b3acc9d9fb35b9af9e6d30cfd1dd892cc033b6be Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 14 Oct 2025 19:32:26 +0200 Subject: [PATCH 11/34] fix(#4): :bug: there was a conflict with "Data for point X" table fixed behaviour conflict triggered by "Add a new location" button --- src/AnnotationControl/annotationPanel.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index d1e86fb..89a8792 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -465,17 +465,6 @@ export function initAnnotationsPanel(viewer) { } catch (e) { console.warn('Could not attach camera info to annotation', e) } - let annotationsRoot = tree.get_json('annotations') - let jsonNode = - annotationsRoot && - annotationsRoot.children && - annotationsRoot.children.find( - (child) => child.data.uuid === annotation.uuid - ) - if (jsonNode) { - $.jstree.reference(jsonNode.id).deselect_all() - $.jstree.reference(jsonNode.id).select_node(jsonNode.id) - } updateAnnotationsList() }, 200) // A short delay for Potree/jsTree to update } From 71614e82f65dca61e839e4c1d6564d5a39c193a3 Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 14 Oct 2025 19:58:51 +0200 Subject: [PATCH 12/34] fix(#4): :bug: initially hide "Saved Point Coordinates" value and wait for label placement to update Before of this fix, whenever there was the creation of a new saved position, it had Placeholder values from potree's build, that were updated only after the creation/deletion of another position. Now plaeholder value is substituted by "---" and there is a eventListener that waits for the label to be placed before updating its values --- src/AnnotationControl/annotationPanel.js | 43 +++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 89a8792..c234299 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -296,18 +296,34 @@ export function initAnnotationsPanel(viewer) { ann.cameraTarget || ann.camera_target || ann.cameraPivot ) - const pointPos = vecToArray( + // Start with serialized position, then prefer the live object's current position + let pointPos = vecToArray( ann.position || ann.annotationPosition || ann.pos ) + try { + const live = _findLiveAnnotationByUUID(ann.uuid) + if (live) { + const livePos = vecToArray(live.position || (live.data && live.data.position)) + if (livePos) pointPos = livePos + } + } catch (e) {} + + // Hide Potree's default placeholder coordinates until the annotation is actually placed + function approxEqual(a, b, eps = 1e-3) { + if (!a || !b || a.length !== b.length) return false + for (let i = 0; i < a.length; i++) if (Math.abs(Number(a[i]) - Number(b[i])) > eps) return false + return true + } + const PLACEHOLDER_POS = [589748.27, 231444.54, 753.675] + if (pointPos && approxEqual(pointPos, PLACEHOLDER_POS)) pointPos = null if (cam || camTarget || pointPos) { const info = document.createElement('div') info.className = 'annotation-info' info.style.fontSize = '0.8em' info.style.marginLeft = '8px' - const fmt = (v) => - v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—' - info.innerHTML = `Camera coordinates: ${fmt(cam)}
Pivot coordinates: ${fmt(camTarget)}
Saved Point coordinates: ${fmt(pointPos)}`; + const fmt = (v) => (v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—') + info.innerHTML = `Camera coordinates: ${fmt(cam)}
Pivot coordinates: ${fmt(camTarget)}
Saved Point coordinates: ${fmt(pointPos)}` row.appendChild(info) } } catch (e) { @@ -454,6 +470,25 @@ export function initAnnotationsPanel(viewer) { // Start Potree annotation insertion let annotation = viewer.annotationTool.startInsertion() + // Wait for the actual placement (left click) before updating the sidebar. + try { + const dom = viewer && viewer.renderer && viewer.renderer.domElement + if (dom) { + const onMouseUp = (ev) => { + try { + if (ev.button === 0) { + // left click = placement finished + setTimeout(() => updateAnnotationsList(), 50) + dom.removeEventListener('mouseup', onMouseUp, true) + } + } catch (e) {} + } + dom.addEventListener('mouseup', onMouseUp, true) + } + } catch (e) { + // ignore + } + // Wait for annotation creation, then select in jsTree setTimeout(() => { let tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() From 8b701d3041d7246c551ca70948c0b1f77833120d Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 15 Oct 2025 12:11:58 +0200 Subject: [PATCH 13/34] feat(#4): :pencil2: add description in the sidebar and make it editable + small refactor Now it is possible to see the Annotation Description in the sidebar, and if double-clicked, it can be edited. The Annotation is mirrored in the liev scene object. Lastly, there is a small refactor of the code to have some helpers --- src/AnnotationControl/annotationPanel.js | 193 ++++++++++++++++++++++- 1 file changed, 187 insertions(+), 6 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index c234299..6a2b9bb 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -80,6 +80,53 @@ export function initAnnotationsPanel(viewer) { return _uuidToIndex.get(uuid) } + // Helper: safe accessors and utilities for jsTree and annotation descriptions + function _getJSTree() { + try { + return $('#jstree_scene').jstree && $('#jstree_scene').jstree() + } catch (e) { + return null + } + } + + function _getJSTreeInstance() { + try { + return ($('#jstree_scene').jstree && $('#jstree_scene').jstree(true)) || + (typeof $.jstree !== 'undefined' && $.jstree.reference && $.jstree.reference('#jstree_scene')) || + null + } catch (e) { + return null + } + } + + function _getAnnotationsRoot() { + const t = _getJSTree() + try { + return t ? t.get_json('annotations') : null + } catch (e) { + return null + } + } + + function _findNodeInAnnotationsRootByUUID(root, uuid) { + if (!root || !root.children || !uuid) return null + return root.children.find((c) => (c.data && c.data.uuid) === uuid) || null + } + + function _getDescriptionForUUID(uuid, nodeData) { + // Prefer live annotation description, then node data (desc/description) + if (!uuid && !nodeData) return '' + try { + const live = _findLiveAnnotationByUUID(uuid) + if (live) { + if (live.data && typeof live.data.description !== 'undefined') return String(live.data.description || '') + if (typeof live.description !== 'undefined') return String(live.description || '') + } + } catch (e) {} + const ann = nodeData || {} + return (ann.description && String(ann.description)) || (ann.desc && String(ann.desc)) || '' + } + function _renameJSTreeNode(nodeId, text) { try { if (window.$ && $.jstree) { @@ -126,10 +173,9 @@ export function initAnnotationsPanel(viewer) { function updateAnnotationsList() { // Implementation for listing annotations targetContainer.innerHTML = '' - const annotationsTree = - $('#jstree_scene').jstree && $('#jstree_scene').jstree() + const annotationsTree = _getJSTree() if (!annotationsTree) return - let annotationsRoot = annotationsTree.get_json('annotations') + let annotationsRoot = _getAnnotationsRoot() if ( !annotationsRoot || !annotationsRoot.children || @@ -142,8 +188,8 @@ export function initAnnotationsPanel(viewer) { return } // Assign monotonic indices for any node UUIDs encountered and rename unlabeled nodes - try { - const items = (annotationsRoot.children || []).map((n) => ({ data: n.data || {}, node: n })) + try { + const items = (annotationsRoot.children || []).map((n) => ({ data: n.data || {}, node: n })) for (const it of items) { const uuid = (it.data && it.data.uuid) || null if (!uuid) continue @@ -218,7 +264,7 @@ export function initAnnotationsPanel(viewer) { annPos || vecToArray(ann.cameraTarget) || viewer.scene.view.getPivot() - viewer.scene.view.setView(camPos, target, 4000) //animation duration in ms + viewer.scene.view.setView(camPos, target, 1000) //animation duration in ms } } row.appendChild(jumpBtn) @@ -285,6 +331,28 @@ export function initAnnotationsPanel(viewer) { row.appendChild(label) row.appendChild(delBtn) + // Description view (shows "No description" when empty) and supports dblclick edit + try { + const ann = node.data || {} + const descText = _getDescriptionForUUID(ann.uuid, ann) + const display = descText.trim() ? descText : 'Annotation Description' + const desc = document.createElement('div') + desc.className = 'annotation-desc' + desc.textContent = display + desc.dataset.uuid = (ann && ann.uuid) || '' + desc.dataset.raw = descText.trim() + desc.style.fontSize = '0.85em' + desc.style.marginLeft = '8px' + desc.addEventListener('dblclick', (ev) => { + ev.stopPropagation() + const u = desc.dataset.uuid + if (u) startInlineDescriptionEditForUUID(u) + }) + row.appendChild(desc) + } catch (e) { + // ignore description rendering errors + } + // show saved camera and point info try { const ann = node.data || {} @@ -390,6 +458,119 @@ export function initAnnotationsPanel(viewer) { }) } + // Start inline editing for an annotation description (multiline) + function startInlineDescriptionEditForUUID(uuid) { + if (!uuid) return + const descEl = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`) + if (!descEl) return + let oldText = descEl.dataset.raw || descEl.textContent || '' + try { + const live = _findLiveAnnotationByUUID(uuid) + if (live) { + if (live.data && typeof live.data.description !== 'undefined') { + oldText = String(live.data.description || '') + } else if (typeof live.description !== 'undefined') { + oldText = String(live.description || '') + } + } + } catch (e) {} + + const ta = document.createElement('textarea') + ta.className = 'annotation-edit-textarea' + // Prefill textarea with existing description + ta.value = oldText + ta.rows = 1 + ta.style.width = '100%' + descEl.replaceWith(ta) + ta.focus() + try { + ta.select() + } catch (e) {} + + function finish(commit) { + const newText = commit ? (ta.value.trim() || '') : oldText + const displayText = newText ? newText : 'Annotation Description' + const newDesc = document.createElement('div') + newDesc.className = 'annotation-desc' + newDesc.textContent = displayText + newDesc.dataset.uuid = uuid + newDesc.dataset.raw = newText + newDesc.style.fontSize = '0.85em' + newDesc.style.marginLeft = '8px' + newDesc.addEventListener('dblclick', (ev) => { + ev.stopPropagation() + startInlineDescriptionEditForUUID(uuid) + }) + + try { + if (ta.isConnected) { + ta.replaceWith(newDesc) + } else { + const existing = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`) + if (existing) existing.textContent = displayText + } + } catch (e) { + try { + const existing = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`) + if (existing) existing.textContent = displayText + } catch (_) {} + } + + _commitEditedDescription(uuid, newText) + } + + ta.addEventListener('blur', () => finish(true)) + ta.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + // Ctrl+Enter or Cmd+Enter to commit multiline + finish(true) + } else if (e.key === 'Escape') { + finish(false) + } + }) + } + + function _commitEditedDescription(uuid, description) { + if (!uuid) return + try { + const tree = _getJSTree() + if (tree) { + const annotationsRoot = _getAnnotationsRoot() + const node = _findNodeInAnnotationsRootByUUID(annotationsRoot, uuid) + if (node) { + node.data = node.data || {} + node.data.description = description + + try { + const jsTreeInst = _getJSTreeInstance() + if (jsTreeInst) { + const jsNode = jsTreeInst.get_node(node.id) + if (jsNode) { + jsNode.original = jsNode.original || {} + jsNode.original.data = jsNode.original.data || {} + jsNode.original.data.description = description + } + } + } catch (e) { + // If we couldn't access the internal node, continue anyway + } + } + } + } catch (e) {} + + try { + const live = _findLiveAnnotationByUUID(uuid) + if (live) { + live.data = live.data || {} + live.data.description = description + if (typeof live.description !== 'undefined') live.description = description + if (typeof live.desc !== 'undefined') live.desc = description + } + } catch (e) {} + + setTimeout(updateAnnotationsList, 0) + } + function _commitEditedName(uuid, name) { if (!uuid) return // update jsTree node text From 7d804b76526134d92a1e5c044ffa9ab252d202e0 Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 15 Oct 2025 14:49:49 +0200 Subject: [PATCH 14/34] refactor(#4): remove camera pivot coordinates --- src/AnnotationControl/annotationPanel.js | 31 +++++------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 6a2b9bb..188a2f4 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -260,11 +260,8 @@ export function initAnnotationsPanel(viewer) { viewer.scene.view && typeof viewer.scene.view.setView === 'function' ) { - const target = - annPos || - vecToArray(ann.cameraTarget) || - viewer.scene.view.getPivot() - viewer.scene.view.setView(camPos, target, 1000) //animation duration in ms + const target = annPos || null + viewer.scene.view.setView(camPos, target, 1000) // animation duration in ms } } row.appendChild(jumpBtn) @@ -360,9 +357,6 @@ export function initAnnotationsPanel(viewer) { const cam = vecToArray( ann.cameraPosition || ann.camera_position || ann.cameraPos ) - const camTarget = vecToArray( - ann.cameraTarget || ann.camera_target || ann.cameraPivot - ) // Start with serialized position, then prefer the live object's current position let pointPos = vecToArray( @@ -385,13 +379,13 @@ export function initAnnotationsPanel(viewer) { const PLACEHOLDER_POS = [589748.27, 231444.54, 753.675] if (pointPos && approxEqual(pointPos, PLACEHOLDER_POS)) pointPos = null - if (cam || camTarget || pointPos) { + if (cam || pointPos) { const info = document.createElement('div') info.className = 'annotation-info' info.style.fontSize = '0.8em' info.style.marginLeft = '8px' const fmt = (v) => (v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—') - info.innerHTML = `Camera coordinates: ${fmt(cam)}
Pivot coordinates: ${fmt(camTarget)}
Saved Point coordinates: ${fmt(pointPos)}` + info.innerHTML = `Camera coordinates: ${fmt(cam)}
Saved Point coordinates: ${fmt(pointPos)}` row.appendChild(info) } } catch (e) { @@ -616,9 +610,8 @@ export function initAnnotationsPanel(viewer) { if (annotationHeader && annotationHeader.nextElementSibling) { $(annotationHeader.nextElementSibling).slideDown() } - // Capture current camera view (position + pivot) at the moment the user clicks Add - let camPos = null - let camTarget = null + // Capture current camera view (position) at the moment the user clicks Add + let camPos = null try { if (viewer && viewer.scene && viewer.scene.view) { camPos = @@ -632,17 +625,6 @@ export function initAnnotationsPanel(viewer) { viewer.scene.view.position.z ] : null - camTarget = - viewer.scene.view.getPivot && - typeof viewer.scene.view.getPivot === 'function' - ? viewer.scene.view.getPivot().toArray - ? viewer.scene.view.getPivot().toArray() - : [ - viewer.scene.view.getPivot().x, - viewer.scene.view.getPivot().y, - viewer.scene.view.getPivot().z - ] - : null } } catch (e) { console.warn('Could not read current view for annotation', e) @@ -677,7 +659,6 @@ export function initAnnotationsPanel(viewer) { // Persist captured camera info on the annotation object so it will be available in the jsTree node data try { if (camPos) annotation.cameraPosition = camPos - if (camTarget) annotation.cameraTarget = camTarget } catch (e) { console.warn('Could not attach camera info to annotation', e) } From 4dad112a423c8cd78d81e052358b630453084659 Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 15 Oct 2025 20:29:10 +0200 Subject: [PATCH 15/34] style(#4): :art: add stylings for annotationPanel in the sidebar --- src/AnnotationControl/annotationPanel.css | 271 +++++++++++++++++++++- src/AnnotationControl/annotationPanel.js | 127 ++++++---- 2 files changed, 358 insertions(+), 40 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.css b/src/AnnotationControl/annotationPanel.css index f5a8398..f8bebc8 100644 --- a/src/AnnotationControl/annotationPanel.css +++ b/src/AnnotationControl/annotationPanel.css @@ -7,4 +7,273 @@ img.button-icon[src$="/annotation.svg"]{ height: 0 !important; margin: 0 !important; padding: 0 !important; -} \ No newline at end of file +} + +.annotation-desc { + font-size: 0.85em; + margin-left: 8px; +} + +.annotation-info { + font-size: 0.8em; + margin-left: 8px; +} + +.annotation-edit-textarea { + width: 100%; +} + +.annotation-add-button { + margin: 10px 0; +} + +.annotation-empty { + opacity: 0.6; + padding: 10px; + text-align: center; +} + +.annotation-row { + display: flex; + flex-direction: column; /* stack header above body */ + gap: 6px; + padding: 6px 8px; + margin: 10px 2px; + border-radius: 6px; + background: #2c3539; + border: 1px solid transparent; + transition: background 0.15s, border-color 0.15s; + color: #d9e2e6; +} + +.annotation-row .annotation-label { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.annotation-header { + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} +.annotation-body { + width: 100%; + margin-top: 6px; +} + +/* By default hide the body (details); show when row has open */ +.annotation-body { + display: none; +} +.annotation-row.open .annotation-body { + display: block; +} + +.annotation-row .toggle-triangle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 12px; + color: #8fb9c9; + vertical-align: middle; + cursor: pointer; +} +.annotation-row .toggle-triangle::after { + content: '▸'; +} +.annotation-row.open .toggle-triangle::after { + content: '▾'; +} + +/* Jump button */ +.annotation-row .jump-btn { + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + color: #7fbcd3; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 8px; + border: 1px solid rgba(127,188,211,0.28); + cursor: pointer; + transition: transform 0.12s ease, background 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, color 0.12s ease; + box-shadow: none; +} +.annotation-row .jump-btn::after { + content: '→'; + font-weight: 700; + font-size: 13px; + line-height: 1; +} +.annotation-row .jump-btn:hover { + color: #7fbcd3; + border-color: rgba(14,166,217,0.44); + box-shadow: 0 8px 18px rgba(14,166,217,0.14), 0 0 0 3px rgba(14,166,217,0.06); + background: transparent; +} +.annotation-row .jump-btn:focus { + outline: none; + border-color: rgba(14,166,217,0.44); + box-shadow: 0 8px 22px rgba(14,166,217,0.18), 0 0 0 4px rgba(14,166,217,0.06); +} +.annotation-row .jump-btn:active, +.annotation-row .jump-btn[aria-pressed="true"] { + transform: translateY(1px); + background: linear-gradient(180deg, #28c1ff 0%, #0ea6d9 100%); + color: #fff; + border-color: transparent; + box-shadow: 0 6px 20px rgba(14,166,217,0.26), inset 0 1px 0 rgba(255,255,255,0.14); +} + +.annotation-row .jump-btn.recently-pressed { + transform: translateY(1px); + background: linear-gradient(180deg, #28c1ff 0%, #0ea6d9 100%); + color: #fff; + border-color: transparent; + box-shadow: 0 6px 20px rgba(14,166,217,0.26), inset 0 1px 0 rgba(255,255,255,0.14); + transition: background 1.4s cubic-bezier(.2,.9,.2,1), box-shadow 1.4s cubic-bezier(.2,.9,.2,1), transform 0.12s ease, color 1.0s ease, opacity 1.4s ease; +} + +.annotation-row .jump-btn.jump-disabled, +.annotation-row .jump-btn:disabled { + opacity: 0.44; + color: #9fbfcf; + border-color: rgba(127,188,211,0.12); + box-shadow: none; + cursor: default; + pointer-events: none; +} + +.annotation-row .jump-btn.jump-disabled:hover, +.annotation-row .jump-btn.jump-disabled:focus, +.annotation-row .jump-btn:disabled:hover, +.annotation-row .jump-btn:disabled:focus { + transform: none; + box-shadow: none; + background: transparent; +} + + +.annotation-row .del-btn { + background: #3b2626; + border: 1px solid #5a3a3a; + color: #ff9a9a; + font-weight: 600; + font-size: 11px; + line-height: 1; + padding: 4px 6px; + border-radius: 4px; + cursor: pointer; + transition: + background 0.15s, + color 0.15s; + margin-left: 8px; +} +.annotation-row .del-btn:hover { + background: #5a2d2d; + color: #fff; +} + +.annotation-row:hover { + background: #354045; + border-color: #425056; +} +.annotation-row.active { + background: #1f4b63; + border-color: #2f6b8c; + box-shadow: 0 0 0 1px #2f6b8c66; +} + +.annotation-row .annotation-desc, +.annotation-row .annotation-info { + background: #2f383d; + padding: 8px; + border: 1px solid #404a50; + border-radius: 4px; + color: #cfd5d8; + font-family: inherit; + font-size: 12px; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.annotation-row .annotation-desc, +.annotation-row .annotation-info { + display: none; +} +.annotation-row.open .annotation-desc, +.annotation-row.open .annotation-info { + display: block; + margin-top: 6px; +} + +.annotation-header .annotation-label { + margin-right: 8px; +} +.annotation-header .controls { + margin-left: auto; + display: flex; + gap: 8px; + align-items: center; + flex: 0 0 auto; +} + +.annotation-header input[type="text"], +.annotation-header .annotation-label input[type="text"] { + width: 100%; + max-width: 100%; + box-sizing: border-box; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Make the caret/triangle take up fixed space so it doesn't shift */ +.annotation-row .toggle-triangle { + flex: 0 0 18px; +} + +/* Add button */ +.annotation-add-button { + background: linear-gradient(180deg,#f6f6f6 0%, #e9e9e9 100%); + color: #222; + padding: 8px 16px; + min-width: 140px; + height: 38px; + display: block; + margin: 12px auto; + border-radius: 6px; + font-size: 13px; + font-weight: 700; + box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset; + border: 1px solid #cfcfcf; + cursor: pointer; + text-align: center; +} +.annotation-add-button .add-label { + color: #222; + font-weight: 700; +} +.annotation-add-button:hover { + background: linear-gradient(180deg,#f3f3f3 0%, #e2e2e2 100%); + border-color: #bfbfbf; +} +.annotation-add-button:active { + transform: translateY(1px); + background: linear-gradient(180deg,#e9e9e9 0%, #dbdbdb 100%); + box-shadow: inset 0 2px 6px rgba(0,0,0,0.06); +} +.annotation-add-button:focus { + outline: 2px solid rgba(100,100,100,0.12); + outline-offset: 2px; +} + diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 188a2f4..f7a574c 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -208,9 +208,7 @@ export function initAnnotationsPanel(viewer) { if (typeof live.name !== 'undefined') live.name = newName if (live.data) live.data.title = newName } - } catch (e) { - // ignore live-update failures - } + } catch (e) {} } } } catch (e) { @@ -220,8 +218,14 @@ export function initAnnotationsPanel(viewer) { // Build list entries for each annotation node annotationsRoot.children.forEach((node) => { - const row = document.createElement('div') - row.className = 'annotation-row' + const row = document.createElement('div') + row.className = 'annotation-row' + + // header and body structure + const header = document.createElement('div') + header.className = 'annotation-header' + const body = document.createElement('div') + body.className = 'annotation-body' const label = document.createElement('span') label.textContent = node.text || 'Unnamed' @@ -238,11 +242,17 @@ export function initAnnotationsPanel(viewer) { if (uuid) startInlineEditForUUID(uuid) }) - // Jump button + // triangular toggle (collapsed/open) + const toggle = document.createElement('span') + toggle.className = 'toggle-triangle' + toggle.title = 'Toggle details' + + // Jump button - const jumpBtn = document.createElement('button') - jumpBtn.textContent = 'Jump' - jumpBtn.title = 'Move to this position' + const jumpBtn = document.createElement('button') + jumpBtn.className = 'jump-btn' + jumpBtn.title = 'Move to this position' + jumpBtn.setAttribute('aria-label', 'Jump to saved view') // Move the viewer to the saved camera position/pivot for this annotation (if present) jumpBtn.onclick = () => { const ann = node.data || {} @@ -262,14 +272,28 @@ export function initAnnotationsPanel(viewer) { ) { const target = annPos || null viewer.scene.view.setView(camPos, target, 1000) // animation duration in ms + + // Transient visual feedback: mark button as recently pressed so CSS can + // show the filled gradient and glow, then fade it back automatically. + try { + jumpBtn.setAttribute('aria-pressed', 'true') + jumpBtn.classList.add('recently-pressed') + // remove after short delay to allow CSS fade-out + window.setTimeout(() => { + try { + jumpBtn.classList.remove('recently-pressed') + jumpBtn.setAttribute('aria-pressed', 'false') + } catch (e) {} + }, 200) + } catch (e) {} } } - row.appendChild(jumpBtn) - // Delete button - const delBtn = document.createElement('button') - delBtn.textContent = 'x' - delBtn.title = 'Delete saved position' + // Delete button + const delBtn = document.createElement('button') + delBtn.className = 'del-btn' + delBtn.textContent = '✖' + delBtn.title = 'Delete saved position' // Delete an annotation: remove annotation from the renderer scene (live annotation), then remove the tree node delBtn.onclick = () => { const annData = node.data || {} @@ -326,9 +350,25 @@ export function initAnnotationsPanel(viewer) { updateAnnotationsList() } - row.appendChild(label) - row.appendChild(delBtn) - // Description view (shows "No description" when empty) and supports dblclick edit + // Append elements into header: toggle, label, then controls (jump/delete) + const controls = document.createElement('div') + controls.className = 'controls' + controls.appendChild(jumpBtn) + controls.appendChild(delBtn) + + header.appendChild(toggle) + header.appendChild(label) + header.appendChild(controls) + row.appendChild(header) + + // Wire toggle to show/hide details + try { + toggle.addEventListener('click', (ev) => { + ev.stopPropagation() + row.classList.toggle('open') + }) + } catch (e) {} + // Description view,supports dblclick edit try { const ann = node.data || {} const descText = _getDescriptionForUUID(ann.uuid, ann) @@ -338,14 +378,12 @@ export function initAnnotationsPanel(viewer) { desc.textContent = display desc.dataset.uuid = (ann && ann.uuid) || '' desc.dataset.raw = descText.trim() - desc.style.fontSize = '0.85em' - desc.style.marginLeft = '8px' desc.addEventListener('dblclick', (ev) => { ev.stopPropagation() const u = desc.dataset.uuid if (u) startInlineDescriptionEditForUUID(u) }) - row.appendChild(desc) + body.appendChild(desc) } catch (e) { // ignore description rendering errors } @@ -382,15 +420,26 @@ export function initAnnotationsPanel(viewer) { if (cam || pointPos) { const info = document.createElement('div') info.className = 'annotation-info' - info.style.fontSize = '0.8em' - info.style.marginLeft = '8px' const fmt = (v) => (v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—') - info.innerHTML = `Camera coordinates: ${fmt(cam)}
Saved Point coordinates: ${fmt(pointPos)}` - row.appendChild(info) + info.innerHTML = `Camera coordinates: ${fmt(cam)}

Saved Point coordinates: ${fmt(pointPos)}` + body.appendChild(info) + + // Enable or disable jump button depending on whether we have a saved point position + try { + if (pointPos && Array.isArray(pointPos) && pointPos.length === 3) { + jumpBtn.disabled = false + jumpBtn.classList.remove('jump-disabled') + } else { + jumpBtn.disabled = true + jumpBtn.classList.add('jump-disabled') + } + } catch (e) {} } } catch (e) { // ignore formatting errors } + // Append body last so it's hidden until opened + row.appendChild(body) targetContainer.appendChild(row) }) } @@ -398,7 +447,6 @@ export function initAnnotationsPanel(viewer) { // Start inline editing for a given annotation UUID (opens an input in the sidebar) function startInlineEditForUUID(uuid) { if (!uuid) return - // find the row label element const labelEl = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) if (!labelEl) return const oldText = labelEl.textContent || '' @@ -421,13 +469,11 @@ export function initAnnotationsPanel(viewer) { ev.stopPropagation() startInlineEditForUUID(uuid) }) - // The input may have been removed already by an external update. - // Check connectivity before replacing to avoid DOMNotFound errors. + try { if (input.isConnected) { input.replaceWith(newLabel) } else { - // If input no longer in DOM, ensure any existing label is updated instead const existing = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) if (existing) existing.textContent = newText } @@ -470,11 +516,10 @@ export function initAnnotationsPanel(viewer) { } catch (e) {} const ta = document.createElement('textarea') - ta.className = 'annotation-edit-textarea' + ta.className = 'annotation-edit-textarea' // Prefill textarea with existing description ta.value = oldText ta.rows = 1 - ta.style.width = '100%' descEl.replaceWith(ta) ta.focus() try { @@ -485,12 +530,10 @@ export function initAnnotationsPanel(viewer) { const newText = commit ? (ta.value.trim() || '') : oldText const displayText = newText ? newText : 'Annotation Description' const newDesc = document.createElement('div') - newDesc.className = 'annotation-desc' - newDesc.textContent = displayText + newDesc.className = 'annotation-desc' + newDesc.textContent = displayText newDesc.dataset.uuid = uuid newDesc.dataset.raw = newText - newDesc.style.fontSize = '0.85em' - newDesc.style.marginLeft = '8px' newDesc.addEventListener('dblclick', (ev) => { ev.stopPropagation() startInlineDescriptionEditForUUID(uuid) @@ -515,8 +558,13 @@ export function initAnnotationsPanel(viewer) { ta.addEventListener('blur', () => finish(true)) ta.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { - // Ctrl+Enter or Cmd+Enter to commit multiline + if (e.key === 'Enter') { + // Shift+Enter: insert newline (allow default). Enter: finish and save. + if (e.shiftKey) { + // allow default -> newline inserted + return + } + e.preventDefault() finish(true) } else if (e.key === 'Escape') { finish(false) @@ -598,9 +646,10 @@ export function initAnnotationsPanel(viewer) { // Add Annotation UI (mimics Potree toolbar/pinpoint-button logic) function createAddButton() { - const btn = document.createElement('button') - btn.textContent = '+ Add a new location' - btn.style.margin = '8px 0' + const btn = document.createElement('button') + btn.className = 'annotation-add-button' + btn.setAttribute('aria-label', 'Add a new saved location') + btn.innerHTML = `Add a location` btn.onclick = () => { // Show this panel, if collapsed const menu = document.getElementById('potree_menu') From 89d65f4b96b7bd22614b2b8a9fbafb10dc2f1a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 20 Oct 2025 10:49:23 +0200 Subject: [PATCH 16/34] feat(#4): :sparkles: Made a small express server for saving annotations --- server.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ vite.config.js | 14 ++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 server.js create mode 100644 vite.config.js diff --git a/server.js b/server.js new file mode 100644 index 0000000..d11befc --- /dev/null +++ b/server.js @@ -0,0 +1,48 @@ +import express from 'express' +import cors from 'cors' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +const app = express() +const PORT = process.env.PORT || 5174 + +app.use(cors()) +app.use(express.json({ limit: '2mb' })) + +// Directory where annotations are stored alongside Vite's public folder +const annotationsDir = path.join(dirname, 'public', 'annotations') +const annotationsFile = path.join(annotationsDir, 'annotations.json') + +// Ensure directory exists +function ensureDirSync(dir) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) +} + +// Save annotations JSON +app.post('/api/annotations', (req, res) => { + try { + const body = req.body + if (!body || typeof body !== 'object') { + return res.status(400).json({ error: 'Invalid JSON body' }) + } + if (!body.folders || typeof body.folders !== 'object') { + return res.status(400).json({ error: 'Missing folders in payload' }) + } + + ensureDirSync(annotationsDir) + const data = JSON.stringify(body, null, 2) + fs.writeFileSync(annotationsFile, data, 'utf8') + return res.json({ saved: true, path: '/annotations/annotations.json' }) + } catch (err) { + console.error('Failed to save annotations:', err) + return res.status(500).json({ error: 'Failed to save annotations' }) + } +}) + +app.listen(PORT, () => { + console.log(`API server listening on http://localhost:${PORT}`) +}) diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..afeed41 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:5174', + changeOrigin: true, + secure: false + } + } + } +}) From 77b328c6791907f614327442e8137d78c2a86875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 20 Oct 2025 10:54:16 +0200 Subject: [PATCH 17/34] feat(#4): :sparkles: Made a function for storing annotations The data is sent by API to the small express server and stored in a json. the objects in the json will be fetched when opening the app and displayed --- public/annotations/annotations.json | 20 +++ src/Annotations/persistence.js | 226 ++++++++++++++++++++++++++++ src/potreeViewer.js | 6 + 3 files changed, 252 insertions(+) create mode 100644 public/annotations/annotations.json create mode 100644 src/Annotations/persistence.js diff --git a/public/annotations/annotations.json b/public/annotations/annotations.json new file mode 100644 index 0000000..62e1690 --- /dev/null +++ b/public/annotations/annotations.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "pointcloudKey": "_pointclouds_data_converted_metadata_json", + "folders": { + "General": [ + { + "title": "Test1", + "description": "Test1 description", + "position": [ + 1183815.569987793, + 63692.700009765635, + 6243873.499987793 + ], + "cameraPosition": null, + "cameraTarget": null, + "children": [] + } + ] + } +} \ No newline at end of file diff --git a/src/Annotations/persistence.js b/src/Annotations/persistence.js new file mode 100644 index 0000000..3713ea3 --- /dev/null +++ b/src/Annotations/persistence.js @@ -0,0 +1,226 @@ +export function initAnnotationPersistence(viewer, options = {}) { + const jsonUrl = options.jsonUrl || '/annotations/annotations.json' + const defaultSaveUrl = (() => { + try { + if ( + typeof window !== 'undefined' && + window.location && + window.location.port === '5173' + ) { + // Dev fallback: post directly to API server to avoid proxy issues + return 'http://localhost:5174/api/annotations' + } + } catch {} + return '/api/annotations' + })() + const saveUrl = options.saveUrl || defaultSaveUrl + const autosave = Boolean(options.autosave) + const folderResolver = options.getAnnotationFolder || defaultGetFolder + const folderSetter = options.setAnnotationFolder || defaultSetFolder + const key = deriveKey(options.pointcloudUrl || options.pointcloudPath) + const STORAGE_KEY = `molloy_annotations_snapshot_${key}` + + function defaultGetFolder(ann) { + return ann?.userData?.folder || 'General' + } + function defaultSetFolder(ann, folder) { + if (!ann.userData) ann.userData = {} + ann.userData.folder = folder || 'General' + } + + function posToArray(p) { + if (!p) return null + if (Array.isArray(p)) return p + if (typeof p.x === 'number') return [p.x, p.y, p.z] + if (typeof p.toArray === 'function') return p.toArray() + return null + } + + function serializeNode(ann) { + return { + title: ann.title || '', + description: ann.description || '', + position: posToArray( + ann.position || ann._position || ann?.marker?.position + ), + cameraPosition: posToArray(ann.cameraPosition), + cameraTarget: posToArray(ann.cameraTarget), + children: (ann.children || []).map(serializeNode) + } + } + + function serializeGrouped() { + const root = viewer.scene.annotations + const folders = {} + for (const ann of root.children || []) { + const f = folderResolver(ann) || 'General' + if (!folders[f]) folders[f] = [] + folders[f].push(serializeNode(ann)) + } + return { version: 1, pointcloudKey: key, folders } + } + + async function loadFromJson() { + try { + const res = await fetch(jsonUrl, { cache: 'no-store' }) + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + const data = await res.json() + applyJsonToScene(data) + // snapshot to localStorage for quick restore + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) + } catch {} + } catch (err) { + // fallback to local snapshot + try { + const snap = localStorage.getItem(STORAGE_KEY) + if (snap) applyJsonToScene(JSON.parse(snap)) + } catch {} + } + } + + function applyJsonToScene(json) { + if (!json || !json.folders) return + const root = viewer.scene.annotations + for (const ch of [...(root.children || [])]) root.remove(ch) + + const folders = json.folders || {} + for (const folderName of Object.keys(folders)) { + const list = folders[folderName] || [] + for (const item of list) addAnnotationRec(root, item, folderName) + } + } + + function addAnnotationRec(parent, item, folderName) { + const pos = item.position || [0, 0, 0] + const ann = new Potree.Annotation({ + position: new THREE.Vector3(pos[0], pos[1], pos[2]), + title: item.title || '' + }) + ann.description = item.description || '' + if (item.cameraPosition) + ann.cameraPosition = new THREE.Vector3(...item.cameraPosition) + if (item.cameraTarget) + ann.cameraTarget = new THREE.Vector3(...item.cameraTarget) + if (folderSetter) folderSetter(ann, folderName) + parent.add(ann) + watchAnnotationDeep(ann) + for (const child of item.children || []) + addAnnotationRec(ann, child, folderName) + } + + function deriveKey(urlLike) { + try { + const u = String(urlLike || '').trim() + if (!u) return 'default' + return u + .replace(/[^a-z0-9]+/gi, '_') + .toLowerCase() + .slice(-80) + } catch { + return 'default' + } + } + + function getAnnotationsJSON() { + return serializeGrouped() + } + + function snapshotToLocalStorage() { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(serializeGrouped())) + } catch {} + } + + async function saveToServer(payload) { + const data = payload || serializeGrouped() + try { + const headers = { 'Content-Type': 'application/json' } + const res = await fetch(saveUrl, { + method: 'POST', + headers, + body: JSON.stringify(data) + }) + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + return await res.json() + } catch (err) { + throw err + } + } + + // Autosnapshot after add/remove + const root = viewer.scene.annotations + let snapshotTimer = null + const debouncedSnapshot = () => { + if (snapshotTimer) cancelAnimationFrame(snapshotTimer) + snapshotTimer = requestAnimationFrame(async () => { + snapshotToLocalStorage() + if (autosave) { + try { + await saveToServer() + } catch {} + } + }) + } + + // Patch Potree.Annotation.prototype to watch all additions/removals in the tree + if (Potree?.Annotation && !Potree.Annotation.prototype._persistPatched) { + Potree.Annotation.prototype._persistPatched = true + const _protoAdd = Potree.Annotation.prototype.add + const _protoRemove = Potree.Annotation.prototype.remove + Potree.Annotation.prototype.add = function (...args) { + const child = args[0] + const r = _protoAdd.apply(this, args) + if (child) watchAnnotationDeep(child) + debouncedSnapshot() + return r + } + Potree.Annotation.prototype.remove = function (...args) { + const r = _protoRemove.apply(this, args) + debouncedSnapshot() + return r + } + } + + function watchAnnotationDeep(ann) { + if (!ann || ann._persistWatched) return + ann._persistWatched = true + watchAnnotation(ann) + ;(ann.children || []).forEach(watchAnnotationDeep) + } + + function defineWatchedProp(obj, key) { + try { + let _val = obj[key] + Object.defineProperty(obj, key, { + configurable: true, + enumerable: true, + get() { + return _val + }, + set(v) { + _val = v + debouncedSnapshot() + } + }) + } catch {} + } + + function watchAnnotation(ann) { + defineWatchedProp(ann, 'title') + defineWatchedProp(ann, 'description') + defineWatchedProp(ann, 'cameraPosition') + defineWatchedProp(ann, 'cameraTarget') + } + + loadFromJson() + watchAnnotationDeep(root) + + return { + getAnnotationsJSON, + snapshotToLocalStorage, + loadFromJson, + serializeGrouped, + saveToServer + } +} diff --git a/src/potreeViewer.js b/src/potreeViewer.js index fb709f0..9ccd25e 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,5 +1,6 @@ import { initElevationControls } from './ElevationControl/elevationControl.js' import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js' +import { initAnnotationPersistence } from './Annotations/persistence.js' import { ecef } from './config.js' /** @@ -37,6 +38,11 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { initElevationControls(viewer) initMeasurementsPanel(viewer) + initAnnotationPersistence(viewer, { + jsonUrl: '/annotations/annotations.json', + autosave: true, + pointcloudUrl + }) }) const e = await Potree.loadPointCloud(pointcloudUrl) From 527a9aff3a49f0a6fb6fd8621a48339a9edc786a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 20 Oct 2025 10:55:24 +0200 Subject: [PATCH 18/34] build(#4): :building_construction: Added some dependencies --- package-lock.json | 843 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 + 2 files changed, 848 insertions(+) diff --git a/package-lock.json b/package-lock.json index a63c17f..9fa226d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,10 @@ "": { "name": "molloyexplorer", "version": "0.0.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.1" + }, "devDependencies": { "prettier": "^3.6.2", "vite": "^7.1.2" @@ -755,6 +759,223 @@ "dev": true, "license": "MIT" }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -797,6 +1018,67 @@ "@esbuild/win32-x64": "0.25.9" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -815,6 +1097,42 @@ } } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -830,6 +1148,206 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -849,6 +1367,63 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -914,6 +1489,58 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rollup": { "version": "4.50.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", @@ -955,6 +1582,164 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -965,6 +1750,15 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -982,6 +1776,55 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", diff --git a/package.json b/package.json index 716f981..8aa6c17 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,16 @@ "type": "module", "scripts": { "dev": "vite", + "server": "node server.js", "build": "vite build", "preview": "vite preview", "format": "prettier --write .", "format:check": "prettier --check ." }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.1" + }, "devDependencies": { "prettier": "^3.6.2", "vite": "^7.1.2" From c98a21a95a7cec3742c1fb1a6be9702b153c5f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 20 Oct 2025 11:04:12 +0200 Subject: [PATCH 19/34] reinstalled package lock --- package-lock.json | 1384 +++++++++++++++++---------------------------- 1 file changed, 530 insertions(+), 854 deletions(-) diff --git a/package-lock.json b/package-lock.json index b8ead1d..1bb01a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,599 +47,52 @@ "node": ">= 6" } }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", - "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", - "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", - "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", - "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", - "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", - "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", - "cpu": [ - "x64" - ], + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", - "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", - "cpu": [ - "arm" - ], + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", - "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", - "cpu": [ - "arm" - ], + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "ms": "^2.1.1" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", - "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", - "cpu": [ - "arm64" - ], + "node_modules/@cypress/xvfb/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", - "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", "cpu": [ "arm64" ], @@ -647,125 +100,14 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", - "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", - "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", - "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", - "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", - "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", - "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", - "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", - "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", - "cpu": [ - "arm64" + "darwin" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { + "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", - "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", "cpu": [ "arm64" ], @@ -773,48 +115,18 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", - "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", - "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" + "darwin" ] }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", "dev": true, "license": "MIT", "optional": true, @@ -854,6 +166,17 @@ "@types/node": "*" } }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -954,6 +277,10 @@ ], "license": "MIT" }, + "node_modules/array-flatten": { + "version": "1.1.1", + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -1063,6 +390,28 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "1.20.3", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1098,6 +447,13 @@ "node": "*" } }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -1110,9 +466,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1124,9 +477,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1319,6 +669,34 @@ "node": ">=4.0.0" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1326,6 +704,17 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1342,9 +731,9 @@ } }, "node_modules/cypress": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.4.0.tgz", - "integrity": "sha512-+GC/Y/LXAcaMCzfuM7vRx5okRmonceZbr0ORUAoOrZt/5n2eGK8yh04bok1bWSjZ32wRHrZESqkswQ6biArN5w==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.5.0.tgz", + "integrity": "sha512-7jXBsh5hTfjxr9QQONC2IbdTj0nxSyU8x4eiarMZBzXzCj3pedKviUx8JnLcE4vL8e0TsOzp70WSLRORjEssRA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1400,6 +789,31 @@ "node": "^20.1.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/cypress/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/cypress/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1418,24 +832,13 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, "node_modules/delayed-stream": { @@ -1448,11 +851,23 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -1474,6 +889,10 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1481,6 +900,13 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -1507,9 +933,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1517,9 +940,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1527,9 +947,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1556,8 +973,6 @@ }, "node_modules/esbuild": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1596,6 +1011,10 @@ "@esbuild/win32-x64": "0.25.9" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1606,6 +1025,13 @@ "node": ">=0.8.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -1650,6 +1076,50 @@ "node": ">=4" } }, + "node_modules/express": { + "version": "4.21.2", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1678,6 +1148,31 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -1700,8 +1195,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -1732,6 +1225,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1759,6 +1268,20 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -1777,10 +1300,7 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -1792,9 +1312,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1802,9 +1319,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1827,9 +1341,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -1883,9 +1394,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1913,9 +1421,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1959,9 +1464,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1970,6 +1472,20 @@ "node": ">= 0.4" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-signature": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", @@ -1995,6 +1511,16 @@ "node": ">=8.12.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2026,6 +1552,10 @@ "node": ">=8" } }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, "node_modules/ini": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", @@ -2036,6 +1566,13 @@ "node": ">=10" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2283,14 +1820,25 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -2298,11 +1846,25 @@ "dev": true, "license": "MIT" }, + "node_modules/methods": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2310,9 +1872,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -2342,16 +1901,11 @@ } }, "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "version": "2.0.0", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -2367,6 +1921,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -2380,11 +1941,15 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2393,6 +1958,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2442,6 +2017,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2452,6 +2034,10 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -2468,15 +2054,11 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -2498,8 +2080,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -2527,8 +2107,6 @@ }, "node_modules/prettier": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -2564,6 +2142,17 @@ "node": ">= 0.6.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", @@ -2583,13 +2172,10 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, + "version": "6.13.0", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -2598,6 +2184,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -2631,8 +2237,6 @@ }, "node_modules/rollup": { "version": "4.50.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", - "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", "dev": true, "license": "MIT", "dependencies": { @@ -2682,9 +2286,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -2703,9 +2304,6 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -2721,6 +2319,56 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2746,9 +2394,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2766,9 +2411,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2783,9 +2425,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -2802,9 +2441,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -2844,8 +2480,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2878,6 +2512,13 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2978,8 +2619,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3023,6 +2662,13 @@ "node": ">=14.14" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -3083,6 +2729,17 @@ "node": ">=8" } }, + "node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/undici-types": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", @@ -3101,6 +2758,13 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -3111,6 +2775,13 @@ "node": ">=8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -3121,6 +2792,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -3138,8 +2816,6 @@ }, "node_modules/vite": { "version": "7.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", - "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", "dependencies": { From 67de0a1edc5c3827c64108f29e8ca2ee3915e7b8 Mon Sep 17 00:00:00 2001 From: franmagn Date: Mon, 20 Oct 2025 15:19:51 +0200 Subject: [PATCH 20/34] fix(#4): small fix to merge with other branch --- src/AnnotationControl/annotationPanel.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index f7a574c..5c4a9f9 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -1,3 +1,5 @@ +import {initAnnotationPersistence} from './persistence' + /** * Annotations Panel * Injects a custom Annotations section for storing camera positions, @@ -719,6 +721,7 @@ export function initAnnotationsPanel(viewer) { createAddButton() updateAnnotationsList() + initAnnotationPersistence(viewer) // Listen to Potree's annotation events to auto-refresh the list if (viewer.scene && viewer.scene.annotations) { From b626233ad5fe60bf7b4c9f837ce12d6ce1b419e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 20 Oct 2025 15:25:36 +0200 Subject: [PATCH 21/34] feat(#4): :sparkles: update annotation persitence to work with the UI --- public/annotations/annotations.json | 15 -- .../persistence.js | 179 ++++++++++++------ 2 files changed, 125 insertions(+), 69 deletions(-) rename src/{Annotations => AnnotationControl}/persistence.js (51%) diff --git a/public/annotations/annotations.json b/public/annotations/annotations.json index 62e1690..e9b78ed 100644 --- a/public/annotations/annotations.json +++ b/public/annotations/annotations.json @@ -1,20 +1,5 @@ { "version": 1, - "pointcloudKey": "_pointclouds_data_converted_metadata_json", "folders": { - "General": [ - { - "title": "Test1", - "description": "Test1 description", - "position": [ - 1183815.569987793, - 63692.700009765635, - 6243873.499987793 - ], - "cameraPosition": null, - "cameraTarget": null, - "children": [] - } - ] } } \ No newline at end of file diff --git a/src/Annotations/persistence.js b/src/AnnotationControl/persistence.js similarity index 51% rename from src/Annotations/persistence.js rename to src/AnnotationControl/persistence.js index 3713ea3..b9e1283 100644 --- a/src/Annotations/persistence.js +++ b/src/AnnotationControl/persistence.js @@ -13,12 +13,13 @@ export function initAnnotationPersistence(viewer, options = {}) { } catch {} return '/api/annotations' })() - const saveUrl = options.saveUrl || defaultSaveUrl - const autosave = Boolean(options.autosave) - const folderResolver = options.getAnnotationFolder || defaultGetFolder - const folderSetter = options.setAnnotationFolder || defaultSetFolder - const key = deriveKey(options.pointcloudUrl || options.pointcloudPath) - const STORAGE_KEY = `molloy_annotations_snapshot_${key}` + const saveUrl = defaultSaveUrl + const autosave = true + const folderResolver = defaultGetFolder + const folderSetter = defaultSetFolder + const STORAGE_KEY = `molloy_annotations_snapshot` + const _watchedAnnotations = new Set() + const _titleDescCache = new WeakMap() function defaultGetFolder(ann) { return ann?.userData?.folder || 'General' @@ -40,12 +41,8 @@ export function initAnnotationPersistence(viewer, options = {}) { return { title: ann.title || '', description: ann.description || '', - position: posToArray( - ann.position || ann._position || ann?.marker?.position - ), + position: posToArray((ann?.marker && ann?.marker?.position) ? ann.marker.position : (ann.position || ann._position)), cameraPosition: posToArray(ann.cameraPosition), - cameraTarget: posToArray(ann.cameraTarget), - children: (ann.children || []).map(serializeNode) } } @@ -57,7 +54,7 @@ export function initAnnotationPersistence(viewer, options = {}) { if (!folders[f]) folders[f] = [] folders[f].push(serializeNode(ann)) } - return { version: 1, pointcloudKey: key, folders } + return { version: 1, folders } } async function loadFromJson() { @@ -80,6 +77,7 @@ export function initAnnotationPersistence(viewer, options = {}) { } function applyJsonToScene(json) { + console.log('Loading annotations from JSON...', json) if (!json || !json.folders) return const root = viewer.scene.annotations for (const ch of [...(root.children || [])]) root.remove(ch) @@ -92,34 +90,29 @@ export function initAnnotationPersistence(viewer, options = {}) { } function addAnnotationRec(parent, item, folderName) { - const pos = item.position || [0, 0, 0] + const posArr = Array.isArray(item.position) ? item.position : [0, 0, 0] const ann = new Potree.Annotation({ - position: new THREE.Vector3(pos[0], pos[1], pos[2]), - title: item.title || '' + position: new THREE.Vector3(posArr[0], posArr[1], posArr[2]), + title: item.title || '', }) ann.description = item.description || '' - if (item.cameraPosition) - ann.cameraPosition = new THREE.Vector3(...item.cameraPosition) - if (item.cameraTarget) - ann.cameraTarget = new THREE.Vector3(...item.cameraTarget) + if (Array.isArray(item.cameraPosition)) ann.cameraPosition = new THREE.Vector3(...item.cameraPosition) + if (folderSetter) folderSetter(ann, folderName) parent.add(ann) - watchAnnotationDeep(ann) - for (const child of item.children || []) - addAnnotationRec(ann, child, folderName) - } - function deriveKey(urlLike) { - try { - const u = String(urlLike || '').trim() - if (!u) return 'default' - return u - .replace(/[^a-z0-9]+/gi, '_') - .toLowerCase() - .slice(-80) - } catch { - return 'default' - } + watchAnnotationDeep(ann) + // If Potree attaches a marker asynchronously, hook into it on the next frame + requestAnimationFrame(() => { + try { + if (ann.marker && ann.marker.position) { + if (typeof ann.marker.position.onChange === 'function') { + ann.marker.position.onChange(() => debouncedSnapshot()) + } + } + } catch {} + }) + for (const child of item.children || []) addAnnotationRec(ann, child, folderName) } function getAnnotationsJSON() { @@ -163,6 +156,15 @@ export function initAnnotationPersistence(viewer, options = {}) { }) } + // Expose minimal handle for the poller to access closure values safely + try { + window.__annPersist = { + watched: _watchedAnnotations, + cache: _titleDescCache, + debouncedSnapshot + } + } catch {} + // Patch Potree.Annotation.prototype to watch all additions/removals in the tree if (Potree?.Annotation && !Potree.Annotation.prototype._persistPatched) { Potree.Annotation.prototype._persistPatched = true @@ -172,7 +174,8 @@ export function initAnnotationPersistence(viewer, options = {}) { const child = args[0] const r = _protoAdd.apply(this, args) if (child) watchAnnotationDeep(child) - debouncedSnapshot() + // Delay a bit so position can settle after placement + requestAnimationFrame(() => requestAnimationFrame(debouncedSnapshot)) return r } Potree.Annotation.prototype.remove = function (...args) { @@ -182,39 +185,77 @@ export function initAnnotationPersistence(viewer, options = {}) { } } + // Snapshot whenever an annotation changes (e.g., placed/moved/edited) + function onAnnotationChanged() { + debouncedSnapshot() + } + try { + root.addEventListener('annotation_changed', onAnnotationChanged) + root.addEventListener('annotation_added', () => { + requestAnimationFrame(() => requestAnimationFrame(debouncedSnapshot)) + }) + root.addEventListener('annotation_removed', onAnnotationChanged) + } catch (_) {} + function watchAnnotationDeep(ann) { if (!ann || ann._persistWatched) return ann._persistWatched = true watchAnnotation(ann) + // Watch vector changes to capture post-placement updates reliably + watchVectorChanges(ann.position) + if (ann.marker && ann.marker.position) watchVectorChanges(ann.marker.position) + _watchedAnnotations.add(ann) + // cache current title/desc + try { _titleDescCache.set(ann, { t: ann.title, d: ann.description }) } catch {} ;(ann.children || []).forEach(watchAnnotationDeep) } - function defineWatchedProp(obj, key) { - try { - let _val = obj[key] - Object.defineProperty(obj, key, { - configurable: true, - enumerable: true, - get() { - return _val - }, - set(v) { - _val = v - debouncedSnapshot() - } - }) - } catch {} + function patchSetMethod(obj, methodName) { + if (!obj || typeof obj[methodName] !== 'function') return + const original = obj[methodName] + if (original._persistPatched) return + const patched = function (...args) { + const r = original.apply(this, args) + debouncedSnapshot() + return r + } + patched._persistPatched = true + obj[methodName] = patched } function watchAnnotation(ann) { - defineWatchedProp(ann, 'title') - defineWatchedProp(ann, 'description') - defineWatchedProp(ann, 'cameraPosition') - defineWatchedProp(ann, 'cameraTarget') + patchSetMethod(ann, 'setTitle') + patchSetMethod(ann, 'setDescription') + if (ann.cameraPosition) watchVectorChanges(ann.cameraPosition) + } + + function watchVectorChanges(vec) { + if (!vec) return + try { + if (typeof vec.onChange === 'function' && !vec._persistOnChangeHooked) { + vec._persistOnChangeHooked = true + vec.onChange(() => debouncedSnapshot()) + } else if (!vec._persistPollHooked) { + // Fallback: brief polling window to detect early changes when onChange is unavailable + vec._persistPollHooked = true + let frames = 0 + let last = `${vec.x},${vec.y},${vec.z}` + const poll = () => { + const cur = `${vec.x},${vec.y},${vec.z}` + if (cur !== last) { + last = cur + debouncedSnapshot() + } + if (frames++ < 90) requestAnimationFrame(poll) // ~1.5s at 60fps + } + requestAnimationFrame(poll) + } + } catch {} } loadFromJson() watchAnnotationDeep(root) + startTitleDescPoller() return { getAnnotationsJSON, @@ -224,3 +265,33 @@ export function initAnnotationPersistence(viewer, options = {}) { saveToServer } } + +function startTitleDescPoller() { + // Use a module-level flag to avoid multiple pollers + if (startTitleDescPoller._started) return + startTitleDescPoller._started = true + try { + const tick = () => { + try { + const h = window.__annPersist + if (h && h.watched && h.cache) { + let changed = false + h.watched.forEach((ann) => { + const prev = h.cache.get(ann) || { t: undefined, d: undefined } + const curT = ann.title + const curD = ann.description + if (prev.t !== curT || prev.d !== curD) { + h.cache.set(ann, { t: curT, d: curD }) + changed = true + } + }) + if (changed && typeof h.debouncedSnapshot === 'function') { + h.debouncedSnapshot() + } + } + } catch {} + requestAnimationFrame(tick) + } + requestAnimationFrame(tick) + } catch {} +} From 1350936275a89305c1a8cb13ac37de3655a4180c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 20 Oct 2025 15:30:51 +0200 Subject: [PATCH 22/34] fix(#4): :bug: fixed import --- src/potreeViewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 24fb4c9..8e657eb 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,4 +1,4 @@ -import { initAnnotationsPanel } from './AnnotationControl/AnnotationPanel.js' +import { initAnnotationsPanel } from './AnnotationControl/annotationPanel.js' import { initElevationControls } from './ElevationControl/elevationControl.js' import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js' import { ecef } from './config.js' From 138d58be613db2a65daa5e0462c74c24ccea28da Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 22 Oct 2025 11:20:58 +0200 Subject: [PATCH 23/34] style(#4): :art: some small UI changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - made it possible to click everywhere on the single annotation panel to open it, and not only on the toggle triangle - added a small pencil icon to indicate the possibility to edit the title text - switched “camera coordinates” with “saved point coordinates” in the annotation panel - made it that the coordinates are listed below the “type of coordinate” title, and are on different lines --- src/AnnotationControl/annotationPanel.css | 15 ++++++ src/AnnotationControl/annotationPanel.js | 66 ++++++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.css b/src/AnnotationControl/annotationPanel.css index f8bebc8..1c95c7b 100644 --- a/src/AnnotationControl/annotationPanel.css +++ b/src/AnnotationControl/annotationPanel.css @@ -218,6 +218,21 @@ img.button-icon[src$="/annotation.svg"]{ .annotation-header .annotation-label { margin-right: 8px; } + +/* tiny pencil hint showing that the title is editable via double-click */ +.annotation-header .annotation-label .annotation-edit-hint { + margin-left: 8px; + font-size: 0.85em; + color: rgba(223,230,233,0.72); + opacity: 0.9; + cursor: default; + user-select: none; + vertical-align: middle; + margin-top: -1px; +} +.annotation-header .annotation-label:hover .annotation-edit-hint { + color: rgba(255,255,255,0.95); +} .annotation-header .controls { margin-left: auto; display: flex; diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 5c4a9f9..7ead99b 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -244,6 +244,14 @@ export function initAnnotationsPanel(viewer) { if (uuid) startInlineEditForUUID(uuid) }) + // Small edit hint icon to indicate double-click to edit + const editHint = document.createElement('span') + editHint.className = 'annotation-edit-hint' + editHint.title = 'Double-click to edit title' + editHint.textContent = '🖉' + // keep the hint non-interactive (don't steal pointer events) + label.appendChild(editHint) + // triangular toggle (collapsed/open) const toggle = document.createElement('span') toggle.className = 'toggle-triangle' @@ -363,6 +371,31 @@ export function initAnnotationsPanel(viewer) { header.appendChild(controls) row.appendChild(header) + // Allow clicking anywhere on the header to toggle the details, but + // ignore clicks that originate from interactive controls (jump/delete) + // so those buttons keep their original behavior. + try { + header.addEventListener('click', (ev) => { + try { + // If the click was inside the controls area (jump/delete), do nothing + if (ev.target && ev.target.closest && ev.target.closest('.controls')) { + return + } + + // If the click was on an interactive element (button, input, textarea, a), ignore + const interactive = ['BUTTON', 'INPUT', 'TEXTAREA', 'A', 'SELECT', 'LABEL'] + if (ev.target && ev.target.tagName && interactive.indexOf(ev.target.tagName) >= 0) { + return + } + + // Otherwise toggle the row + row.classList.toggle('open') + } catch (e) { + // fail silently + } + }) + } catch (e) {} + // Wire toggle to show/hide details try { toggle.addEventListener('click', (ev) => { @@ -419,11 +452,14 @@ export function initAnnotationsPanel(viewer) { const PLACEHOLDER_POS = [589748.27, 231444.54, 753.675] if (pointPos && approxEqual(pointPos, PLACEHOLDER_POS)) pointPos = null + // Format the camera and point positions for display if (cam || pointPos) { const info = document.createElement('div') info.className = 'annotation-info' const fmt = (v) => (v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—') - info.innerHTML = `Camera coordinates: ${fmt(cam)}

Saved Point coordinates: ${fmt(pointPos)}` + const fmtPoint = fmt(pointPos) ? String(fmt(pointPos)).replace(/,\s*/g, ',
') : '—' + const fmtCam = fmt(cam) ? String(fmt(cam)).replace(/,\s*/g, ',
') : '—' + info.innerHTML = `Saved Point coordinates:
${fmtPoint}

Camera coordinates:
${fmtCam}` body.appendChild(info) // Enable or disable jump button depending on whether we have a saved point position @@ -451,7 +487,23 @@ export function initAnnotationsPanel(viewer) { if (!uuid) return const labelEl = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) if (!labelEl) return - const oldText = labelEl.textContent || '' + // Compute the visible title text without the edit-hint glyph. + // The label contains a text node (the title) and a child .annotation-edit-hint span. + let oldText = '' + try { + for (const node of Array.from(labelEl.childNodes)) { + if (node.nodeType === Node.TEXT_NODE) { + oldText += node.nodeValue || '' + } + } + oldText = (oldText || '').trim() + } catch (e) { + oldText = labelEl.textContent || '' + } + // Capture existing hint so we can re-attach it after editing + const existingHint = labelEl.querySelector && labelEl.querySelector('.annotation-edit-hint') + const existingHintText = existingHint ? existingHint.textContent : null + const existingHintTitle = existingHint ? existingHint.title : null const input = document.createElement('input') input.type = 'text' input.value = oldText @@ -471,6 +523,16 @@ export function initAnnotationsPanel(viewer) { ev.stopPropagation() startInlineEditForUUID(uuid) }) + // Re-attach the edit hint so the hint persists after editing + try { + const hint = document.createElement('span') + hint.className = 'annotation-edit-hint' + if (existingHintTitle) hint.title = existingHintTitle + hint.textContent = existingHintText != null ? existingHintText : '✎' + newLabel.appendChild(hint) + } catch (e) { + // ignore hint restoration errors + } try { if (input.isConnected) { From f0bbe5c587acc255f9f36c461996ec7f98e5628c Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 22 Oct 2025 11:27:07 +0200 Subject: [PATCH 24/34] style(#4): :art: ran prettier --- src/AnnotationControl/annotationPanel.css | 342 ++++++++++++---------- src/AnnotationControl/annotationPanel.js | 209 ++++++++----- src/AnnotationControl/persistence.js | 25 +- 3 files changed, 332 insertions(+), 244 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.css b/src/AnnotationControl/annotationPanel.css index 1c95c7b..b1f4131 100644 --- a/src/AnnotationControl/annotationPanel.css +++ b/src/AnnotationControl/annotationPanel.css @@ -1,136 +1,149 @@ /* Hide Potree annotation icon specifically */ /* This targets the that references annotation.svg. */ -img.button-icon[src$="/annotation.svg"]{ - display: none !important; - visibility: hidden !important; - width: 0 !important; - height: 0 !important; - margin: 0 !important; - padding: 0 !important; +img.button-icon[src$='/annotation.svg'] { + display: none !important; + visibility: hidden !important; + width: 0 !important; + height: 0 !important; + margin: 0 !important; + padding: 0 !important; } .annotation-desc { - font-size: 0.85em; - margin-left: 8px; + font-size: 0.85em; + margin-left: 8px; } .annotation-info { - font-size: 0.8em; - margin-left: 8px; + font-size: 0.8em; + margin-left: 8px; } .annotation-edit-textarea { - width: 100%; + width: 100%; } .annotation-add-button { - margin: 10px 0; + margin: 10px 0; } .annotation-empty { - opacity: 0.6; - padding: 10px; - text-align: center; + opacity: 0.6; + padding: 10px; + text-align: center; } .annotation-row { display: flex; flex-direction: column; /* stack header above body */ - gap: 6px; - padding: 6px 8px; - margin: 10px 2px; + gap: 6px; + padding: 6px 8px; + margin: 10px 2px; border-radius: 6px; background: #2c3539; border: 1px solid transparent; - transition: background 0.15s, border-color 0.15s; + transition: + background 0.15s, + border-color 0.15s; color: #d9e2e6; } .annotation-row .annotation-label { - flex: 1; - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .annotation-header { - display: flex; - align-items: center; - gap: 8px; - width: 100%; + display: flex; + align-items: center; + gap: 8px; + width: 100%; } .annotation-body { - width: 100%; - margin-top: 6px; + width: 100%; + margin-top: 6px; } /* By default hide the body (details); show when row has open */ .annotation-body { - display: none; + display: none; } .annotation-row.open .annotation-body { - display: block; + display: block; } .annotation-row .toggle-triangle { - display: inline-flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - font-size: 12px; - color: #8fb9c9; - vertical-align: middle; - cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 12px; + color: #8fb9c9; + vertical-align: middle; + cursor: pointer; } .annotation-row .toggle-triangle::after { - content: '▸'; + content: '▸'; } .annotation-row.open .toggle-triangle::after { - content: '▾'; + content: '▾'; } /* Jump button */ .annotation-row .jump-btn { - width: 28px; - height: 28px; - border-radius: 50%; - background: transparent; - color: #7fbcd3; - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 8px; - border: 1px solid rgba(127,188,211,0.28); - cursor: pointer; - transition: transform 0.12s ease, background 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, color 0.12s ease; - box-shadow: none; + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + color: #7fbcd3; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 8px; + border: 1px solid rgba(127, 188, 211, 0.28); + cursor: pointer; + transition: + transform 0.12s ease, + background 0.18s ease, + box-shadow 0.18s ease, + border-color 0.18s ease, + color 0.12s ease; + box-shadow: none; } .annotation-row .jump-btn::after { - content: '→'; - font-weight: 700; - font-size: 13px; - line-height: 1; + content: '→'; + font-weight: 700; + font-size: 13px; + line-height: 1; } .annotation-row .jump-btn:hover { color: #7fbcd3; - border-color: rgba(14,166,217,0.44); - box-shadow: 0 8px 18px rgba(14,166,217,0.14), 0 0 0 3px rgba(14,166,217,0.06); + border-color: rgba(14, 166, 217, 0.44); + box-shadow: + 0 8px 18px rgba(14, 166, 217, 0.14), + 0 0 0 3px rgba(14, 166, 217, 0.06); background: transparent; } .annotation-row .jump-btn:focus { - outline: none; - border-color: rgba(14,166,217,0.44); - box-shadow: 0 8px 22px rgba(14,166,217,0.18), 0 0 0 4px rgba(14,166,217,0.06); + outline: none; + border-color: rgba(14, 166, 217, 0.44); + box-shadow: + 0 8px 22px rgba(14, 166, 217, 0.18), + 0 0 0 4px rgba(14, 166, 217, 0.06); } .annotation-row .jump-btn:active, -.annotation-row .jump-btn[aria-pressed="true"] { +.annotation-row .jump-btn[aria-pressed='true'] { transform: translateY(1px); background: linear-gradient(180deg, #28c1ff 0%, #0ea6d9 100%); color: #fff; border-color: transparent; - box-shadow: 0 6px 20px rgba(14,166,217,0.26), inset 0 1px 0 rgba(255,255,255,0.14); + box-shadow: + 0 6px 20px rgba(14, 166, 217, 0.26), + inset 0 1px 0 rgba(255, 255, 255, 0.14); } .annotation-row .jump-btn.recently-pressed { @@ -138,157 +151,162 @@ img.button-icon[src$="/annotation.svg"]{ background: linear-gradient(180deg, #28c1ff 0%, #0ea6d9 100%); color: #fff; border-color: transparent; - box-shadow: 0 6px 20px rgba(14,166,217,0.26), inset 0 1px 0 rgba(255,255,255,0.14); - transition: background 1.4s cubic-bezier(.2,.9,.2,1), box-shadow 1.4s cubic-bezier(.2,.9,.2,1), transform 0.12s ease, color 1.0s ease, opacity 1.4s ease; + box-shadow: + 0 6px 20px rgba(14, 166, 217, 0.26), + inset 0 1px 0 rgba(255, 255, 255, 0.14); + transition: + background 1.4s cubic-bezier(0.2, 0.9, 0.2, 1), + box-shadow 1.4s cubic-bezier(0.2, 0.9, 0.2, 1), + transform 0.12s ease, + color 1s ease, + opacity 1.4s ease; } .annotation-row .jump-btn.jump-disabled, .annotation-row .jump-btn:disabled { - opacity: 0.44; - color: #9fbfcf; - border-color: rgba(127,188,211,0.12); - box-shadow: none; - cursor: default; - pointer-events: none; + opacity: 0.44; + color: #9fbfcf; + border-color: rgba(127, 188, 211, 0.12); + box-shadow: none; + cursor: default; + pointer-events: none; } .annotation-row .jump-btn.jump-disabled:hover, .annotation-row .jump-btn.jump-disabled:focus, .annotation-row .jump-btn:disabled:hover, .annotation-row .jump-btn:disabled:focus { - transform: none; - box-shadow: none; - background: transparent; + transform: none; + box-shadow: none; + background: transparent; } - .annotation-row .del-btn { - background: #3b2626; - border: 1px solid #5a3a3a; - color: #ff9a9a; - font-weight: 600; - font-size: 11px; - line-height: 1; - padding: 4px 6px; - border-radius: 4px; - cursor: pointer; - transition: - background 0.15s, - color 0.15s; - margin-left: 8px; + background: #3b2626; + border: 1px solid #5a3a3a; + color: #ff9a9a; + font-weight: 600; + font-size: 11px; + line-height: 1; + padding: 4px 6px; + border-radius: 4px; + cursor: pointer; + transition: + background 0.15s, + color 0.15s; + margin-left: 8px; } .annotation-row .del-btn:hover { - background: #5a2d2d; - color: #fff; + background: #5a2d2d; + color: #fff; } .annotation-row:hover { - background: #354045; - border-color: #425056; + background: #354045; + border-color: #425056; } .annotation-row.active { - background: #1f4b63; - border-color: #2f6b8c; - box-shadow: 0 0 0 1px #2f6b8c66; + background: #1f4b63; + border-color: #2f6b8c; + box-shadow: 0 0 0 1px #2f6b8c66; } .annotation-row .annotation-desc, .annotation-row .annotation-info { - background: #2f383d; - padding: 8px; - border: 1px solid #404a50; - border-radius: 4px; - color: #cfd5d8; - font-family: inherit; - font-size: 12px; - white-space: pre-wrap; - overflow-wrap: anywhere; + background: #2f383d; + padding: 8px; + border: 1px solid #404a50; + border-radius: 4px; + color: #cfd5d8; + font-family: inherit; + font-size: 12px; + white-space: pre-wrap; + overflow-wrap: anywhere; } .annotation-row .annotation-desc, .annotation-row .annotation-info { - display: none; + display: none; } .annotation-row.open .annotation-desc, .annotation-row.open .annotation-info { - display: block; - margin-top: 6px; + display: block; + margin-top: 6px; } .annotation-header .annotation-label { - margin-right: 8px; + margin-right: 8px; } /* tiny pencil hint showing that the title is editable via double-click */ .annotation-header .annotation-label .annotation-edit-hint { - margin-left: 8px; - font-size: 0.85em; - color: rgba(223,230,233,0.72); - opacity: 0.9; - cursor: default; - user-select: none; - vertical-align: middle; - margin-top: -1px; + margin-left: 8px; + font-size: 0.85em; + color: rgba(223, 230, 233, 0.72); + opacity: 0.9; + cursor: default; + user-select: none; + vertical-align: middle; + margin-top: -1px; } .annotation-header .annotation-label:hover .annotation-edit-hint { - color: rgba(255,255,255,0.95); + color: rgba(255, 255, 255, 0.95); } .annotation-header .controls { - margin-left: auto; - display: flex; - gap: 8px; - align-items: center; - flex: 0 0 auto; + margin-left: auto; + display: flex; + gap: 8px; + align-items: center; + flex: 0 0 auto; } -.annotation-header input[type="text"], -.annotation-header .annotation-label input[type="text"] { - width: 100%; - max-width: 100%; - box-sizing: border-box; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.annotation-header input[type='text'], +.annotation-header .annotation-label input[type='text'] { + width: 100%; + max-width: 100%; + box-sizing: border-box; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* Make the caret/triangle take up fixed space so it doesn't shift */ -.annotation-row .toggle-triangle { - flex: 0 0 18px; +.annotation-row .toggle-triangle { + flex: 0 0 18px; } /* Add button */ .annotation-add-button { - background: linear-gradient(180deg,#f6f6f6 0%, #e9e9e9 100%); - color: #222; - padding: 8px 16px; - min-width: 140px; - height: 38px; - display: block; - margin: 12px auto; - border-radius: 6px; - font-size: 13px; - font-weight: 700; - box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset; - border: 1px solid #cfcfcf; - cursor: pointer; - text-align: center; + background: linear-gradient(180deg, #f6f6f6 0%, #e9e9e9 100%); + color: #222; + padding: 8px 16px; + min-width: 140px; + height: 38px; + display: block; + margin: 12px auto; + border-radius: 6px; + font-size: 13px; + font-weight: 700; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6) inset; + border: 1px solid #cfcfcf; + cursor: pointer; + text-align: center; } .annotation-add-button .add-label { - color: #222; - font-weight: 700; + color: #222; + font-weight: 700; } .annotation-add-button:hover { - background: linear-gradient(180deg,#f3f3f3 0%, #e2e2e2 100%); - border-color: #bfbfbf; + background: linear-gradient(180deg, #f3f3f3 0%, #e2e2e2 100%); + border-color: #bfbfbf; } .annotation-add-button:active { - transform: translateY(1px); - background: linear-gradient(180deg,#e9e9e9 0%, #dbdbdb 100%); - box-shadow: inset 0 2px 6px rgba(0,0,0,0.06); + transform: translateY(1px); + background: linear-gradient(180deg, #e9e9e9 0%, #dbdbdb 100%); + box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.06); } .annotation-add-button:focus { - outline: 2px solid rgba(100,100,100,0.12); - outline-offset: 2px; + outline: 2px solid rgba(100, 100, 100, 0.12); + outline-offset: 2px; } - diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 7ead99b..66ff909 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -1,4 +1,4 @@ -import {initAnnotationPersistence} from './persistence' +import { initAnnotationPersistence } from './persistence' /** * Annotations Panel @@ -93,9 +93,13 @@ export function initAnnotationsPanel(viewer) { function _getJSTreeInstance() { try { - return ($('#jstree_scene').jstree && $('#jstree_scene').jstree(true)) || - (typeof $.jstree !== 'undefined' && $.jstree.reference && $.jstree.reference('#jstree_scene')) || + return ( + ($('#jstree_scene').jstree && $('#jstree_scene').jstree(true)) || + (typeof $.jstree !== 'undefined' && + $.jstree.reference && + $.jstree.reference('#jstree_scene')) || null + ) } catch (e) { return null } @@ -121,12 +125,18 @@ export function initAnnotationsPanel(viewer) { try { const live = _findLiveAnnotationByUUID(uuid) if (live) { - if (live.data && typeof live.data.description !== 'undefined') return String(live.data.description || '') - if (typeof live.description !== 'undefined') return String(live.description || '') + if (live.data && typeof live.data.description !== 'undefined') + return String(live.data.description || '') + if (typeof live.description !== 'undefined') + return String(live.description || '') } } catch (e) {} const ann = nodeData || {} - return (ann.description && String(ann.description)) || (ann.desc && String(ann.desc)) || '' + return ( + (ann.description && String(ann.description)) || + (ann.desc && String(ann.desc)) || + '' + ) } function _renameJSTreeNode(nodeId, text) { @@ -153,21 +163,27 @@ export function initAnnotationsPanel(viewer) { } function _findLiveAnnotationByUUID(uuid) { - if (!uuid || !viewer || !viewer.scene || !viewer.scene.annotations) return null + if (!uuid || !viewer || !viewer.scene || !viewer.scene.annotations) + return null try { const coll = - (viewer.scene.annotations.flatten && viewer.scene.annotations.flatten()) || - (viewer.scene.annotations.descendants && viewer.scene.annotations.descendants()) || - viewer.scene.annotations.children || [] + (viewer.scene.annotations.flatten && + viewer.scene.annotations.flatten()) || + (viewer.scene.annotations.descendants && + viewer.scene.annotations.descendants()) || + viewer.scene.annotations.children || + [] for (const a of coll) { if (!a) continue if (a.uuid === uuid || (a.data && a.data.uuid === uuid)) return a - } + } } catch (e) { // fallback try { const coll2 = viewer.scene.annotations.children || [] - for (const a of coll2) if (a && (a.uuid === uuid || (a.data && a.data.uuid === uuid))) return a + for (const a of coll2) + if (a && (a.uuid === uuid || (a.data && a.data.uuid === uuid))) + return a } catch (e) {} } return null @@ -190,14 +206,21 @@ export function initAnnotationsPanel(viewer) { return } // Assign monotonic indices for any node UUIDs encountered and rename unlabeled nodes - try { - const items = (annotationsRoot.children || []).map((n) => ({ data: n.data || {}, node: n })) + try { + const items = (annotationsRoot.children || []).map((n) => ({ + data: n.data || {}, + node: n + })) for (const it of items) { const uuid = (it.data && it.data.uuid) || null if (!uuid) continue const idx = _ensureIndexForUUID(uuid) const text = (it.node && it.node.text) || '' - const shouldRename = !text || text.trim() === '' || text === 'Unnamed' || text === 'Annotation Title' + const shouldRename = + !text || + text.trim() === '' || + text === 'Unnamed' || + text === 'Annotation Title' if (shouldRename && idx != null) { const newName = `Annotation #${idx}` _renameJSTreeNode(it.node.id, newName) @@ -217,17 +240,16 @@ export function initAnnotationsPanel(viewer) { // ignore failures } - // Build list entries for each annotation node - annotationsRoot.children.forEach((node) => { + // Build list entries for each annotation node + annotationsRoot.children.forEach((node) => { + const row = document.createElement('div') + row.className = 'annotation-row' - const row = document.createElement('div') - row.className = 'annotation-row' - - // header and body structure - const header = document.createElement('div') - header.className = 'annotation-header' - const body = document.createElement('div') - body.className = 'annotation-body' + // header and body structure + const header = document.createElement('div') + header.className = 'annotation-header' + const body = document.createElement('div') + body.className = 'annotation-body' const label = document.createElement('span') label.textContent = node.text || 'Unnamed' @@ -252,17 +274,17 @@ export function initAnnotationsPanel(viewer) { // keep the hint non-interactive (don't steal pointer events) label.appendChild(editHint) - // triangular toggle (collapsed/open) - const toggle = document.createElement('span') - toggle.className = 'toggle-triangle' - toggle.title = 'Toggle details' + // triangular toggle (collapsed/open) + const toggle = document.createElement('span') + toggle.className = 'toggle-triangle' + toggle.title = 'Toggle details' - // Jump button + // Jump button - const jumpBtn = document.createElement('button') - jumpBtn.className = 'jump-btn' - jumpBtn.title = 'Move to this position' - jumpBtn.setAttribute('aria-label', 'Jump to saved view') + const jumpBtn = document.createElement('button') + jumpBtn.className = 'jump-btn' + jumpBtn.title = 'Move to this position' + jumpBtn.setAttribute('aria-label', 'Jump to saved view') // Move the viewer to the saved camera position/pivot for this annotation (if present) jumpBtn.onclick = () => { const ann = node.data || {} @@ -299,11 +321,11 @@ export function initAnnotationsPanel(viewer) { } } - // Delete button - const delBtn = document.createElement('button') - delBtn.className = 'del-btn' - delBtn.textContent = '✖' - delBtn.title = 'Delete saved position' + // Delete button + const delBtn = document.createElement('button') + delBtn.className = 'del-btn' + delBtn.textContent = '✖' + delBtn.title = 'Delete saved position' // Delete an annotation: remove annotation from the renderer scene (live annotation), then remove the tree node delBtn.onclick = () => { const annData = node.data || {} @@ -360,16 +382,16 @@ export function initAnnotationsPanel(viewer) { updateAnnotationsList() } - // Append elements into header: toggle, label, then controls (jump/delete) - const controls = document.createElement('div') - controls.className = 'controls' - controls.appendChild(jumpBtn) - controls.appendChild(delBtn) + // Append elements into header: toggle, label, then controls (jump/delete) + const controls = document.createElement('div') + controls.className = 'controls' + controls.appendChild(jumpBtn) + controls.appendChild(delBtn) - header.appendChild(toggle) - header.appendChild(label) - header.appendChild(controls) - row.appendChild(header) + header.appendChild(toggle) + header.appendChild(label) + header.appendChild(controls) + row.appendChild(header) // Allow clicking anywhere on the header to toggle the details, but // ignore clicks that originate from interactive controls (jump/delete) @@ -378,13 +400,28 @@ export function initAnnotationsPanel(viewer) { header.addEventListener('click', (ev) => { try { // If the click was inside the controls area (jump/delete), do nothing - if (ev.target && ev.target.closest && ev.target.closest('.controls')) { + if ( + ev.target && + ev.target.closest && + ev.target.closest('.controls') + ) { return } // If the click was on an interactive element (button, input, textarea, a), ignore - const interactive = ['BUTTON', 'INPUT', 'TEXTAREA', 'A', 'SELECT', 'LABEL'] - if (ev.target && ev.target.tagName && interactive.indexOf(ev.target.tagName) >= 0) { + const interactive = [ + 'BUTTON', + 'INPUT', + 'TEXTAREA', + 'A', + 'SELECT', + 'LABEL' + ] + if ( + ev.target && + ev.target.tagName && + interactive.indexOf(ev.target.tagName) >= 0 + ) { return } @@ -438,7 +475,9 @@ export function initAnnotationsPanel(viewer) { try { const live = _findLiveAnnotationByUUID(ann.uuid) if (live) { - const livePos = vecToArray(live.position || (live.data && live.data.position)) + const livePos = vecToArray( + live.position || (live.data && live.data.position) + ) if (livePos) pointPos = livePos } } catch (e) {} @@ -446,7 +485,8 @@ export function initAnnotationsPanel(viewer) { // Hide Potree's default placeholder coordinates until the annotation is actually placed function approxEqual(a, b, eps = 1e-3) { if (!a || !b || a.length !== b.length) return false - for (let i = 0; i < a.length; i++) if (Math.abs(Number(a[i]) - Number(b[i])) > eps) return false + for (let i = 0; i < a.length; i++) + if (Math.abs(Number(a[i]) - Number(b[i])) > eps) return false return true } const PLACEHOLDER_POS = [589748.27, 231444.54, 753.675] @@ -456,9 +496,14 @@ export function initAnnotationsPanel(viewer) { if (cam || pointPos) { const info = document.createElement('div') info.className = 'annotation-info' - const fmt = (v) => (v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—') - const fmtPoint = fmt(pointPos) ? String(fmt(pointPos)).replace(/,\s*/g, ',
') : '—' - const fmtCam = fmt(cam) ? String(fmt(cam)).replace(/,\s*/g, ',
') : '—' + const fmt = (v) => + v ? v.map((c) => Number(c).toFixed(3)).join(', ') : '—' + const fmtPoint = fmt(pointPos) + ? String(fmt(pointPos)).replace(/,\s*/g, ',
') + : '—' + const fmtCam = fmt(cam) + ? String(fmt(cam)).replace(/,\s*/g, ',
') + : '—' info.innerHTML = `Saved Point coordinates:
${fmtPoint}

Camera coordinates:
${fmtCam}` body.appendChild(info) @@ -485,7 +530,9 @@ export function initAnnotationsPanel(viewer) { // Start inline editing for a given annotation UUID (opens an input in the sidebar) function startInlineEditForUUID(uuid) { if (!uuid) return - const labelEl = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) + const labelEl = targetContainer.querySelector( + `.annotation-label[data-uuid="${uuid}"]` + ) if (!labelEl) return // Compute the visible title text without the edit-hint glyph. // The label contains a text node (the title) and a child .annotation-edit-hint span. @@ -501,7 +548,8 @@ export function initAnnotationsPanel(viewer) { oldText = labelEl.textContent || '' } // Capture existing hint so we can re-attach it after editing - const existingHint = labelEl.querySelector && labelEl.querySelector('.annotation-edit-hint') + const existingHint = + labelEl.querySelector && labelEl.querySelector('.annotation-edit-hint') const existingHintText = existingHint ? existingHint.textContent : null const existingHintTitle = existingHint ? existingHint.title : null const input = document.createElement('input') @@ -538,13 +586,17 @@ export function initAnnotationsPanel(viewer) { if (input.isConnected) { input.replaceWith(newLabel) } else { - const existing = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) + const existing = targetContainer.querySelector( + `.annotation-label[data-uuid="${uuid}"]` + ) if (existing) existing.textContent = newText } } catch (e) { // ignore DOM replacement errors and try to update label if present try { - const existing = targetContainer.querySelector(`.annotation-label[data-uuid="${uuid}"]`) + const existing = targetContainer.querySelector( + `.annotation-label[data-uuid="${uuid}"]` + ) if (existing) existing.textContent = newText } catch (_) {} } @@ -565,9 +617,11 @@ export function initAnnotationsPanel(viewer) { // Start inline editing for an annotation description (multiline) function startInlineDescriptionEditForUUID(uuid) { if (!uuid) return - const descEl = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`) + const descEl = targetContainer.querySelector( + `.annotation-desc[data-uuid="${uuid}"]` + ) if (!descEl) return - let oldText = descEl.dataset.raw || descEl.textContent || '' + let oldText = descEl.dataset.raw || descEl.textContent || '' try { const live = _findLiveAnnotationByUUID(uuid) if (live) { @@ -580,7 +634,7 @@ export function initAnnotationsPanel(viewer) { } catch (e) {} const ta = document.createElement('textarea') - ta.className = 'annotation-edit-textarea' + ta.className = 'annotation-edit-textarea' // Prefill textarea with existing description ta.value = oldText ta.rows = 1 @@ -591,11 +645,11 @@ export function initAnnotationsPanel(viewer) { } catch (e) {} function finish(commit) { - const newText = commit ? (ta.value.trim() || '') : oldText - const displayText = newText ? newText : 'Annotation Description' + const newText = commit ? ta.value.trim() || '' : oldText + const displayText = newText ? newText : 'Annotation Description' const newDesc = document.createElement('div') - newDesc.className = 'annotation-desc' - newDesc.textContent = displayText + newDesc.className = 'annotation-desc' + newDesc.textContent = displayText newDesc.dataset.uuid = uuid newDesc.dataset.raw = newText newDesc.addEventListener('dblclick', (ev) => { @@ -607,12 +661,16 @@ export function initAnnotationsPanel(viewer) { if (ta.isConnected) { ta.replaceWith(newDesc) } else { - const existing = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`) + const existing = targetContainer.querySelector( + `.annotation-desc[data-uuid="${uuid}"]` + ) if (existing) existing.textContent = displayText } } catch (e) { try { - const existing = targetContainer.querySelector(`.annotation-desc[data-uuid="${uuid}"]`) + const existing = targetContainer.querySelector( + `.annotation-desc[data-uuid="${uuid}"]` + ) if (existing) existing.textContent = displayText } catch (_) {} } @@ -669,7 +727,8 @@ export function initAnnotationsPanel(viewer) { if (live) { live.data = live.data || {} live.data.description = description - if (typeof live.description !== 'undefined') live.description = description + if (typeof live.description !== 'undefined') + live.description = description if (typeof live.desc !== 'undefined') live.desc = description } } catch (e) {} @@ -685,7 +744,9 @@ export function initAnnotationsPanel(viewer) { if (tree) { const annotationsRoot = tree.get_json('annotations') if (annotationsRoot && annotationsRoot.children) { - const node = annotationsRoot.children.find((c) => (c.data && c.data.uuid) === uuid) + const node = annotationsRoot.children.find( + (c) => (c.data && c.data.uuid) === uuid + ) if (node) { _renameJSTreeNode(node.id, name) node.text = name @@ -710,7 +771,7 @@ export function initAnnotationsPanel(viewer) { // Add Annotation UI (mimics Potree toolbar/pinpoint-button logic) function createAddButton() { - const btn = document.createElement('button') + const btn = document.createElement('button') btn.className = 'annotation-add-button' btn.setAttribute('aria-label', 'Add a new saved location') btn.innerHTML = `Add a location` @@ -723,8 +784,8 @@ export function initAnnotationsPanel(viewer) { if (annotationHeader && annotationHeader.nextElementSibling) { $(annotationHeader.nextElementSibling).slideDown() } - // Capture current camera view (position) at the moment the user clicks Add - let camPos = null + // Capture current camera view (position) at the moment the user clicks Add + let camPos = null try { if (viewer && viewer.scene && viewer.scene.view) { camPos = diff --git a/src/AnnotationControl/persistence.js b/src/AnnotationControl/persistence.js index b9e1283..f991210 100644 --- a/src/AnnotationControl/persistence.js +++ b/src/AnnotationControl/persistence.js @@ -41,8 +41,12 @@ export function initAnnotationPersistence(viewer, options = {}) { return { title: ann.title || '', description: ann.description || '', - position: posToArray((ann?.marker && ann?.marker?.position) ? ann.marker.position : (ann.position || ann._position)), - cameraPosition: posToArray(ann.cameraPosition), + position: posToArray( + ann?.marker && ann?.marker?.position + ? ann.marker.position + : ann.position || ann._position + ), + cameraPosition: posToArray(ann.cameraPosition) } } @@ -93,10 +97,11 @@ export function initAnnotationPersistence(viewer, options = {}) { const posArr = Array.isArray(item.position) ? item.position : [0, 0, 0] const ann = new Potree.Annotation({ position: new THREE.Vector3(posArr[0], posArr[1], posArr[2]), - title: item.title || '', + title: item.title || '' }) ann.description = item.description || '' - if (Array.isArray(item.cameraPosition)) ann.cameraPosition = new THREE.Vector3(...item.cameraPosition) + if (Array.isArray(item.cameraPosition)) + ann.cameraPosition = new THREE.Vector3(...item.cameraPosition) if (folderSetter) folderSetter(ann, folderName) parent.add(ann) @@ -112,7 +117,8 @@ export function initAnnotationPersistence(viewer, options = {}) { } } catch {} }) - for (const child of item.children || []) addAnnotationRec(ann, child, folderName) + for (const child of item.children || []) + addAnnotationRec(ann, child, folderName) } function getAnnotationsJSON() { @@ -191,7 +197,7 @@ export function initAnnotationPersistence(viewer, options = {}) { } try { root.addEventListener('annotation_changed', onAnnotationChanged) - root.addEventListener('annotation_added', () => { + root.addEventListener('annotation_added', () => { requestAnimationFrame(() => requestAnimationFrame(debouncedSnapshot)) }) root.addEventListener('annotation_removed', onAnnotationChanged) @@ -203,10 +209,13 @@ export function initAnnotationPersistence(viewer, options = {}) { watchAnnotation(ann) // Watch vector changes to capture post-placement updates reliably watchVectorChanges(ann.position) - if (ann.marker && ann.marker.position) watchVectorChanges(ann.marker.position) + if (ann.marker && ann.marker.position) + watchVectorChanges(ann.marker.position) _watchedAnnotations.add(ann) // cache current title/desc - try { _titleDescCache.set(ann, { t: ann.title, d: ann.description }) } catch {} + try { + _titleDescCache.set(ann, { t: ann.title, d: ann.description }) + } catch {} ;(ann.children || []).forEach(watchAnnotationDeep) } From 294bc97779575e46b10cde2e8935cccb4b76d815 Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 22 Oct 2025 13:54:51 +0200 Subject: [PATCH 25/34] docs(#4): :memo: improved inline documentation in the code --- src/AnnotationControl/annotationPanel.js | 128 ++++++++++++++++++++--- 1 file changed, 116 insertions(+), 12 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 66ff909..280adc4 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -1,9 +1,21 @@ import { initAnnotationPersistence } from './persistence' /** - * Annotations Panel - * Injects a custom Annotations section for storing camera positions, - * that will (later) be used for letting users rapidly jump to saved views. + * Initialize and inject the Annotations sidebar panel, used for managing saved camera views. + * + * Creates (or re-uses) a container with id `annotations_list`, renders the + * saved annotation entries from the project's jsTree, wires UI controls + * (jump, delete, toggle, inline editing), and hooks into Potree's + * annotation events to keep the list in sync. + * + * Side effects: + * - Mutates DOM by injecting a panel and a "Add a location" button. + * - Registers event listeners on viewer.scene.annotations to refresh the list. + * - Attaches click/dblclick handlers and inline edit inputs that commit + * edits back to jsTree and the live annotation objects. + * + * @param {Object} viewer - Potree viewer instance (used to read camera/pivot, + * jump the view, and access viewer.scene.annotations). */ export function initAnnotationsPanel(viewer) { // Container management @@ -61,8 +73,19 @@ export function initAnnotationsPanel(viewer) { return } - // UI update - // Helper: normalize different vector shapes into [x,y,z] (necessary for handling both Three.js Vector3 and serialized data stored in the jsTree) + /** + * Normalize a vector-like input into an [x,y,z] array. (necessary for handling both Three.js Vector3 and serialized data stored in the jsTree) + * + * Accepts: + * - Array [x,y,z] + * - Three.js Vector3 with toArray() + * - Plain object {x,y,z} + * + * Returns null for invalid input. + * + * @param {*} v - value to normalize + * @returns {Array|null} an [x,y,z] array or null + */ function vecToArray(v) { if (!v) return null if (Array.isArray(v)) return v @@ -70,10 +93,20 @@ export function initAnnotationsPanel(viewer) { if (v.x != null && v.y != null && v.z != null) return [v.x, v.y, v.z] return null } - // Monotonic numbering: assign an increasing number to each UUID and never reuse numbers. + const _uuidToIndex = new Map() let _nextIndex = 1 + /** + * Return a monotonic index number for a UUID (Universally Unique Identifier). + * + * The function assigns an incrementing integer to each UUID the first time + * it is seen and never reuses numbers. Useful to produce human-readable + * default names (e.g. "Annotation #3") when nodes are missing titles. + * + * @param {string} uuid - UUID string for the annotation + * @returns {number|null} index assigned to the UUID, or null if uuid falsy + */ function _ensureIndexForUUID(uuid) { if (!uuid) return null if (!_uuidToIndex.has(uuid)) { @@ -107,6 +140,14 @@ export function initAnnotationsPanel(viewer) { function _getAnnotationsRoot() { const t = _getJSTree() + /** + * Find the live annotation object in the viewer.scene by UUID. + * + * Returns the live annotation or null. + * + * @param {string} uuid - annotation UUID + * @returns {Object|null} live annotation object or null if not found + */ try { return t ? t.get_json('annotations') : null } catch (e) { @@ -189,6 +230,23 @@ export function initAnnotationsPanel(viewer) { return null } function updateAnnotationsList() { + /** + * Rebuild the annotation list UI from the project's jsTree data. + * + * This function: + * - Clears and re-populates the `targetContainer` with a row per annotation. + * - Ensures default labels for unnamed annotations (using monotonic numbering). + * - Renders header, toggle triangle, edit label, jump/delete controls, description, + * and read-only camera/point info. + * - Wires events for inline editing, jump, delete, and header click-to-toggle. + * + * Side effects: + * - Mutates DOM inside `targetContainer`. + * - May call `_renameJSTreeNode` to rename nodes when needed. + * - Uses `_findLiveAnnotationByUUID` to prefer live object positions over serialized values. + * - Event listeners stop propagation where necessary so double-click and button + * behaviour are preserved. + */ // Implementation for listing annotations targetContainer.innerHTML = '' const annotationsTree = _getJSTree() @@ -280,7 +338,6 @@ export function initAnnotationsPanel(viewer) { toggle.title = 'Toggle details' // Jump button - const jumpBtn = document.createElement('button') jumpBtn.className = 'jump-btn' jumpBtn.title = 'Move to this position' @@ -310,7 +367,6 @@ export function initAnnotationsPanel(viewer) { try { jumpBtn.setAttribute('aria-pressed', 'true') jumpBtn.classList.add('recently-pressed') - // remove after short delay to allow CSS fade-out window.setTimeout(() => { try { jumpBtn.classList.remove('recently-pressed') @@ -332,7 +388,7 @@ export function initAnnotationsPanel(viewer) { const uuid = annData.uuid if (uuid && viewer && viewer.scene && viewer.scene.annotations) { - // Find the live annotation instance by UUID (Universally Unique Identifier) + // Find the live annotation instance by UUID let candidates = [] try { candidates = @@ -527,8 +583,21 @@ export function initAnnotationsPanel(viewer) { }) } - // Start inline editing for a given annotation UUID (opens an input in the sidebar) function startInlineEditForUUID(uuid) { + /** + * Start inline editing of an annotation title (opens an input in the sidebar). + * + * Replaces the `.annotation-label` with a text input and handles commit/abort: + * - On commit, updates the label in the sidebar, updates the jsTree node text, + * and updates the live annotation object's title (via `_commitEditedName`). + * - On abort, restores the original label without committing. + * + * Special handling: + * - If the label contains a decorative edit-hint span, the hint text is not + * included in the input value and is re-attached to the restored label. + * + * @param {string} uuid - annotation UUID to edit + */ if (!uuid) return const labelEl = targetContainer.querySelector( `.annotation-label[data-uuid="${uuid}"]` @@ -614,8 +683,16 @@ export function initAnnotationsPanel(viewer) { }) } - // Start inline editing for an annotation description (multiline) function startInlineDescriptionEditForUUID(uuid) { + /** + * Start inline editing of an annotation description (multiline). + * + * Replaces the `.annotation-desc` container with a textarea. Behavior mirrors + * `startInlineEditForUUID` but handles multiline input. Commit updates both + * the jsTree node data and the live annotation object's description. + * + * @param {string} uuid - annotation UUID to edit + */ if (!uuid) return const descEl = targetContainer.querySelector( `.annotation-desc[data-uuid="${uuid}"]` @@ -695,6 +772,14 @@ export function initAnnotationsPanel(viewer) { } function _commitEditedDescription(uuid, description) { + /** + * Commit a changed annotation description to jsTree and to the live annotation. + * + * Also updates any cached node data used by the sidebar. + * + * @param {string} uuid - annotation UUID + * @param {string} description - new description text + */ if (!uuid) return try { const tree = _getJSTree() @@ -737,6 +822,15 @@ export function initAnnotationsPanel(viewer) { } function _commitEditedName(uuid, name) { + /** + * Commit a changed annotation name to jsTree and to the live annotation object. + * + * This updates the jsTree node text (if accessible) and mutates the live + * annotation instance's title/name/data so the viewer label matches. + * + * @param {string} uuid - annotation UUID + * @param {string} name - new name to commit + */ if (!uuid) return // update jsTree node text try { @@ -769,8 +863,18 @@ export function initAnnotationsPanel(viewer) { setTimeout(updateAnnotationsList, 0) } - // Add Annotation UI (mimics Potree toolbar/pinpoint-button logic) function createAddButton() { + /** + * Create and insert the "Add a location" button for annotations. + * + * The button: + * - Captures the current camera view when clicked. + * - Starts Potree's annotation insertion mode. + * - Waits for placement completion and triggers `updateAnnotationsList`. + * + * Side effects: + * - Inserts a button into the DOM near the annotations list. + */ const btn = document.createElement('button') btn.className = 'annotation-add-button' btn.setAttribute('aria-label', 'Add a new saved location') From 681caeb40ebffa98be802fcce6b04013d70216ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Thu, 23 Oct 2025 10:43:22 +0200 Subject: [PATCH 26/34] docs(#4): :memo: Update readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 518effe..bf8c3f3 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,13 @@ npm install npm run dev ``` +Then in a new terminal run the server: + +```bash +npm run server +``` + + Open your browser at to view the app. ### Build for Production @@ -31,3 +38,12 @@ npm run build ``` The built files will be in the `dist/` folder. + + +## Structure + +### The Potree Build +This application is built using Potree as a package, meaning that the potree build is used as a base layer for UI and functionality and built onto by our own code. The add-ons may use, move or manipulate objects from the potree build, referencing the objects by classname or id. + +### Updating Potree version +To update the version of Potree that this application uses you must make a build from the official potree app and replace the build folder here with the new one. There is no guarantee that Molloy Explorer is compatible with other versions of potree. Make sure that everything works on the development server before applying this to the production server. From db4dc9a0485b383eea18191ea943c0782d64d517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Thu, 23 Oct 2025 10:45:06 +0200 Subject: [PATCH 27/34] ran prettier --- README.md | 8 ++++---- index.html | 5 +---- public/annotations/annotations.json | 5 ++--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bf8c3f3..625f104 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ Then in a new terminal run the server: npm run server ``` - Open your browser at to view the app. ### Build for Production @@ -39,11 +38,12 @@ npm run build The built files will be in the `dist/` folder. - ## Structure -### The Potree Build -This application is built using Potree as a package, meaning that the potree build is used as a base layer for UI and functionality and built onto by our own code. The add-ons may use, move or manipulate objects from the potree build, referencing the objects by classname or id. +### The Potree Build + +This application is built using Potree as a package, meaning that the potree build is used as a base layer for UI and functionality and built onto by our own code. The add-ons may use, move or manipulate objects from the potree build, referencing the objects by classname or id. ### Updating Potree version + To update the version of Potree that this application uses you must make a build from the official potree app and replace the build folder here with the new one. There is no guarantee that Molloy Explorer is compatible with other versions of potree. Make sure that everything works on the development server before applying this to the production server. diff --git a/index.html b/index.html index ea2aeb8..192e6ef 100644 --- a/index.html +++ b/index.html @@ -38,10 +38,7 @@ rel="stylesheet" href="/src/MeasurementControl/measurementsPanel.css" /> - + diff --git a/public/annotations/annotations.json b/public/annotations/annotations.json index e9b78ed..c58be02 100644 --- a/public/annotations/annotations.json +++ b/public/annotations/annotations.json @@ -1,5 +1,4 @@ { "version": 1, - "folders": { - } -} \ No newline at end of file + "folders": {} +} From 0b98a4599dea7485f9c4797ded5a05688453b881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Thu, 23 Oct 2025 11:01:45 +0200 Subject: [PATCH 28/34] style(#4): :lipstick: Update the UI for editing title and description of an annotation --- src/AnnotationControl/annotationPanel.css | 81 +++++++++++++++++++---- src/AnnotationControl/annotationPanel.js | 46 ++++++++++--- 2 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.css b/src/AnnotationControl/annotationPanel.css index b1f4131..3c9ecbe 100644 --- a/src/AnnotationControl/annotationPanel.css +++ b/src/AnnotationControl/annotationPanel.css @@ -201,6 +201,39 @@ img.button-icon[src$='/annotation.svg'] { color: #fff; } +.annotation-row .edit-btn { + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + color: #d0d6da; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(208, 214, 218, 0.28); + cursor: pointer; + transition: + transform 0.12s ease, + background 0.18s ease, + box-shadow 0.18s ease, + border-color 0.18s ease, + color 0.12s ease; + box-shadow: none; +} +.annotation-row .edit-btn:hover, +.annotation-row .edit-btn:focus { + color: #ffffff; + border-color: rgba(208, 214, 218, 0.44); + box-shadow: + 0 8px 18px rgba(140, 150, 160, 0.14), + 0 0 0 3px rgba(140, 150, 160, 0.06); + background: transparent; + outline: none; +} +.annotation-row .edit-btn:active { + transform: translateY(1px); +} + .annotation-row:hover { background: #354045; border-color: #425056; @@ -224,6 +257,40 @@ img.button-icon[src$='/annotation.svg'] { overflow-wrap: anywhere; } +/* make room for the pencil inside the description box */ +.annotation-row .annotation-desc { + position: relative; + padding-right: 36px; /* space for inner pencil */ +} + +.annotation-row .annotation-desc .edit-desc-btn { + position: absolute; + top: 6px; + right: 6px; + width: 22px; + height: 22px; + border-radius: 50%; + background: transparent; + color: #d0d6da; + border: 1px solid rgba(208, 214, 218, 0.28); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + line-height: 1; + padding: 0; +} +.annotation-row .annotation-desc .edit-desc-btn:hover, +.annotation-row .annotation-desc .edit-desc-btn:focus { + color: #ffffff; + border-color: rgba(208, 214, 218, 0.44); + box-shadow: + 0 6px 14px rgba(140, 150, 160, 0.16), + 0 0 0 2px rgba(140, 150, 160, 0.06); + outline: none; +} + .annotation-row .annotation-desc, .annotation-row .annotation-info { display: none; @@ -238,20 +305,6 @@ img.button-icon[src$='/annotation.svg'] { margin-right: 8px; } -/* tiny pencil hint showing that the title is editable via double-click */ -.annotation-header .annotation-label .annotation-edit-hint { - margin-left: 8px; - font-size: 0.85em; - color: rgba(223, 230, 233, 0.72); - opacity: 0.9; - cursor: default; - user-select: none; - vertical-align: middle; - margin-top: -1px; -} -.annotation-header .annotation-label:hover .annotation-edit-hint { - color: rgba(255, 255, 255, 0.95); -} .annotation-header .controls { margin-left: auto; display: flex; diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 280adc4..c8b0d60 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -324,13 +324,18 @@ export function initAnnotationsPanel(viewer) { if (uuid) startInlineEditForUUID(uuid) }) - // Small edit hint icon to indicate double-click to edit - const editHint = document.createElement('span') - editHint.className = 'annotation-edit-hint' - editHint.title = 'Double-click to edit title' - editHint.textContent = '🖉' - // keep the hint non-interactive (don't steal pointer events) - label.appendChild(editHint) + // Small pencil button to enter title edit mode + const editBtn = document.createElement('button') + editBtn.className = 'edit-btn' + editBtn.title = 'Edit title' + editBtn.setAttribute('aria-label', 'Edit title') + editBtn.textContent = '✎' + editBtn.addEventListener('click', (ev) => { + ev.stopPropagation() + const uuid = label.dataset.uuid + if (uuid) startInlineEditForUUID(uuid) + }) + // triangular toggle (collapsed/open) const toggle = document.createElement('span') @@ -441,7 +446,8 @@ export function initAnnotationsPanel(viewer) { // Append elements into header: toggle, label, then controls (jump/delete) const controls = document.createElement('div') controls.className = 'controls' - controls.appendChild(jumpBtn) + controls.appendChild(editBtn) + controls.appendChild(jumpBtn) controls.appendChild(delBtn) header.appendChild(toggle) @@ -511,6 +517,19 @@ export function initAnnotationsPanel(viewer) { const u = desc.dataset.uuid if (u) startInlineDescriptionEditForUUID(u) }) + // Add a pencil inside the description box + const innerDescBtn = document.createElement('button') + innerDescBtn.className = 'edit-desc-btn' + innerDescBtn.title = 'Edit description' + innerDescBtn.setAttribute('aria-label', 'Edit description') + innerDescBtn.textContent = '✎' + innerDescBtn.addEventListener('click', (ev) => { + ev.stopPropagation() + try { row.classList.add('open') } catch (e) {} + const u = desc.dataset.uuid + if (u) startInlineDescriptionEditForUUID(u) + }) + desc.appendChild(innerDescBtn) body.appendChild(desc) } catch (e) { // ignore description rendering errors @@ -733,6 +752,17 @@ export function initAnnotationsPanel(viewer) { ev.stopPropagation() startInlineDescriptionEditForUUID(uuid) }) + // Re-add inner edit pencil + const innerDescBtn = document.createElement('button') + innerDescBtn.className = 'edit-desc-btn' + innerDescBtn.title = 'Edit description' + innerDescBtn.setAttribute('aria-label', 'Edit description') + innerDescBtn.textContent = '✎' + innerDescBtn.addEventListener('click', (ev) => { + ev.stopPropagation() + startInlineDescriptionEditForUUID(uuid) + }) + newDesc.appendChild(innerDescBtn) try { if (ta.isConnected) { From 70256be937efeef6e3b4b8c4745b9eec4e7559b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Thu, 23 Oct 2025 11:02:15 +0200 Subject: [PATCH 29/34] ran prettier --- src/AnnotationControl/annotationPanel.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index c8b0d60..86706dc 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -336,7 +336,6 @@ export function initAnnotationsPanel(viewer) { if (uuid) startInlineEditForUUID(uuid) }) - // triangular toggle (collapsed/open) const toggle = document.createElement('span') toggle.className = 'toggle-triangle' @@ -446,8 +445,8 @@ export function initAnnotationsPanel(viewer) { // Append elements into header: toggle, label, then controls (jump/delete) const controls = document.createElement('div') controls.className = 'controls' - controls.appendChild(editBtn) - controls.appendChild(jumpBtn) + controls.appendChild(editBtn) + controls.appendChild(jumpBtn) controls.appendChild(delBtn) header.appendChild(toggle) @@ -525,7 +524,9 @@ export function initAnnotationsPanel(viewer) { innerDescBtn.textContent = '✎' innerDescBtn.addEventListener('click', (ev) => { ev.stopPropagation() - try { row.classList.add('open') } catch (e) {} + try { + row.classList.add('open') + } catch (e) {} const u = desc.dataset.uuid if (u) startInlineDescriptionEditForUUID(u) }) From 30a89bca6e034e750e941919f79422654dff93bd Mon Sep 17 00:00:00 2001 From: franmagn Date: Thu, 23 Oct 2025 15:00:24 +0200 Subject: [PATCH 30/34] docs(#4): :memo: fixed and moved inline documentation --- src/AnnotationControl/annotationPanel.js | 163 ++++++++++++----------- 1 file changed, 87 insertions(+), 76 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 86706dc..2bb5595 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -138,16 +138,17 @@ export function initAnnotationsPanel(viewer) { } } + + /** + * Find the live annotation object in the viewer.scene by UUID. + * + * Returns the live annotation or null. + * + * @param {string} uuid - annotation UUID + * @returns {Object|null} live annotation object or null if not found + */ function _getAnnotationsRoot() { const t = _getJSTree() - /** - * Find the live annotation object in the viewer.scene by UUID. - * - * Returns the live annotation or null. - * - * @param {string} uuid - annotation UUID - * @returns {Object|null} live annotation object or null if not found - */ try { return t ? t.get_json('annotations') : null } catch (e) { @@ -229,24 +230,25 @@ export function initAnnotationsPanel(viewer) { } return null } + + /** + * Rebuild the annotation list UI from the project's jsTree data. + * + * This function: + * - Clears and re-populates the `targetContainer` with a row per annotation. + * - Ensures default labels for unnamed annotations (using monotonic numbering). + * - Renders header, toggle triangle, edit label, jump/delete controls, description, + * and read-only camera/point info. + * - Wires events for inline editing, jump, delete, and header click-to-toggle. + * + * Side effects: + * - Mutates DOM inside `targetContainer`. + * - May call `_renameJSTreeNode` to rename nodes when needed. + * - Uses `_findLiveAnnotationByUUID` to prefer live object positions over serialized values. + * - Event listeners stop propagation where necessary so double-click and button + * behaviour are preserved. + */ function updateAnnotationsList() { - /** - * Rebuild the annotation list UI from the project's jsTree data. - * - * This function: - * - Clears and re-populates the `targetContainer` with a row per annotation. - * - Ensures default labels for unnamed annotations (using monotonic numbering). - * - Renders header, toggle triangle, edit label, jump/delete controls, description, - * and read-only camera/point info. - * - Wires events for inline editing, jump, delete, and header click-to-toggle. - * - * Side effects: - * - Mutates DOM inside `targetContainer`. - * - May call `_renameJSTreeNode` to rename nodes when needed. - * - Uses `_findLiveAnnotationByUUID` to prefer live object positions over serialized values. - * - Event listeners stop propagation where necessary so double-click and button - * behaviour are preserved. - */ // Implementation for listing annotations targetContainer.innerHTML = '' const annotationsTree = _getJSTree() @@ -603,21 +605,21 @@ export function initAnnotationsPanel(viewer) { }) } + /** + * Start inline editing of an annotation title (opens an input in the sidebar). + * + * Replaces the `.annotation-label` with a text input and handles commit/abort: + * - On commit, updates the label in the sidebar, updates the jsTree node text, + * and updates the live annotation object's title (via `_commitEditedName`). + * - On abort, restores the original label without committing. + * + * Special handling: + * - If the label contains a decorative edit-hint span, the hint text is not + * included in the input value and is re-attached to the restored label. + * + * @param {string} uuid - annotation UUID to edit + */ function startInlineEditForUUID(uuid) { - /** - * Start inline editing of an annotation title (opens an input in the sidebar). - * - * Replaces the `.annotation-label` with a text input and handles commit/abort: - * - On commit, updates the label in the sidebar, updates the jsTree node text, - * and updates the live annotation object's title (via `_commitEditedName`). - * - On abort, restores the original label without committing. - * - * Special handling: - * - If the label contains a decorative edit-hint span, the hint text is not - * included in the input value and is re-attached to the restored label. - * - * @param {string} uuid - annotation UUID to edit - */ if (!uuid) return const labelEl = targetContainer.querySelector( `.annotation-label[data-uuid="${uuid}"]` @@ -703,16 +705,16 @@ export function initAnnotationsPanel(viewer) { }) } + /** + * Start inline editing of an annotation description (multiline). + * + * Replaces the `.annotation-desc` container with a textarea. Behavior mirrors + * `startInlineEditForUUID` but handles multiline input. Commit updates both + * the jsTree node data and the live annotation object's description. + * + * @param {string} uuid - annotation UUID to edit + */ function startInlineDescriptionEditForUUID(uuid) { - /** - * Start inline editing of an annotation description (multiline). - * - * Replaces the `.annotation-desc` container with a textarea. Behavior mirrors - * `startInlineEditForUUID` but handles multiline input. Commit updates both - * the jsTree node data and the live annotation object's description. - * - * @param {string} uuid - annotation UUID to edit - */ if (!uuid) return const descEl = targetContainer.querySelector( `.annotation-desc[data-uuid="${uuid}"]` @@ -741,6 +743,15 @@ export function initAnnotationsPanel(viewer) { ta.select() } catch (e) {} + + /** + * Finish editing the description textarea. + * + * Replaces the textarea with a `.annotation-desc`, re-attaches handlers, + * and persists (or discards) the change via _commitEditedDescription. + * + * @param {boolean} commit - true to save, false to cancel + */ function finish(commit) { const newText = commit ? ta.value.trim() || '' : oldText const displayText = newText ? newText : 'Annotation Description' @@ -802,15 +813,15 @@ export function initAnnotationsPanel(viewer) { }) } + /** + * Commit a changed annotation description to jsTree and to the live annotation. + * + * Also updates any cached node data used by the sidebar. + * + * @param {string} uuid - annotation UUID + * @param {string} description - new description text + */ function _commitEditedDescription(uuid, description) { - /** - * Commit a changed annotation description to jsTree and to the live annotation. - * - * Also updates any cached node data used by the sidebar. - * - * @param {string} uuid - annotation UUID - * @param {string} description - new description text - */ if (!uuid) return try { const tree = _getJSTree() @@ -852,16 +863,16 @@ export function initAnnotationsPanel(viewer) { setTimeout(updateAnnotationsList, 0) } + /** + * Commit a changed annotation name to jsTree and to the live annotation object. + * + * This updates the jsTree node text (if accessible) and mutates the live + * annotation instance's title/name/data so the viewer label matches. + * + * @param {string} uuid - annotation UUID + * @param {string} name - new name to commit + */ function _commitEditedName(uuid, name) { - /** - * Commit a changed annotation name to jsTree and to the live annotation object. - * - * This updates the jsTree node text (if accessible) and mutates the live - * annotation instance's title/name/data so the viewer label matches. - * - * @param {string} uuid - annotation UUID - * @param {string} name - new name to commit - */ if (!uuid) return // update jsTree node text try { @@ -894,18 +905,18 @@ export function initAnnotationsPanel(viewer) { setTimeout(updateAnnotationsList, 0) } + /** + * Create and insert the "Add a location" button for annotations. + * + * The button: + * - Captures the current camera view when clicked. + * - Starts Potree's annotation insertion mode. + * - Waits for placement completion and triggers `updateAnnotationsList`. + * + * Side effects: + * - Inserts a button into the DOM near the annotations list. + */ function createAddButton() { - /** - * Create and insert the "Add a location" button for annotations. - * - * The button: - * - Captures the current camera view when clicked. - * - Starts Potree's annotation insertion mode. - * - Waits for placement completion and triggers `updateAnnotationsList`. - * - * Side effects: - * - Inserts a button into the DOM near the annotations list. - */ const btn = document.createElement('button') btn.className = 'annotation-add-button' btn.setAttribute('aria-label', 'Add a new saved location') From fa8725e990f338066e46d2aa59074a9cda74dfc9 Mon Sep 17 00:00:00 2001 From: franmagn Date: Thu, 23 Oct 2025 15:12:12 +0200 Subject: [PATCH 31/34] style(#4): fix annotation description edit box behaviour --- src/AnnotationControl/annotationPanel.css | 21 +++++++++++++++++++++ src/AnnotationControl/annotationPanel.js | 2 -- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.css b/src/AnnotationControl/annotationPanel.css index 3c9ecbe..13a3b2e 100644 --- a/src/AnnotationControl/annotationPanel.css +++ b/src/AnnotationControl/annotationPanel.css @@ -21,6 +21,27 @@ img.button-icon[src$='/annotation.svg'] { .annotation-edit-textarea { width: 100%; + box-sizing: border-box; + max-width: 100%; + display: block; + padding: 8px; + border-radius: 4px; + border: 1px solid #404a50; + background: #2f383d; + color: #cfd5d8; + font-family: inherit; + font-size: 12px; + line-height: 1.3; + white-space: pre-wrap; + overflow: auto; + /* Only allow vertical resizing so user can't drag the box horizontally + out of the gray container. */ + resize: vertical; + max-height: 40vh; +} + +.annotation-row .annotation-edit-textarea { + margin-top: 6px; } .annotation-add-button { diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index 2bb5595..cf12ae9 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -138,7 +138,6 @@ export function initAnnotationsPanel(viewer) { } } - /** * Find the live annotation object in the viewer.scene by UUID. * @@ -743,7 +742,6 @@ export function initAnnotationsPanel(viewer) { ta.select() } catch (e) {} - /** * Finish editing the description textarea. * From 6f45475ab076109caabfbabeb9f13e6a149c5d93 Mon Sep 17 00:00:00 2001 From: franmagn Date: Thu, 23 Oct 2025 18:59:12 +0200 Subject: [PATCH 32/34] docs(#4): :memo: change name of two functions --- src/AnnotationControl/annotationPanel.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index cf12ae9..a1d6b8f 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -650,7 +650,7 @@ export function initAnnotationsPanel(viewer) { input.focus() input.select() - function finish(commit) { + function finishInlineEditForUUID(commit) { const newText = commit ? input.value.trim() || oldText : oldText // restore label const newLabel = document.createElement('span') @@ -694,12 +694,12 @@ export function initAnnotationsPanel(viewer) { _commitEditedName(uuid, newText) } - input.addEventListener('blur', () => finish(true)) + input.addEventListener('blur', () => finishInlineEditForUUID(true)) input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { - finish(true) + finishInlineEditForUUID(true) } else if (e.key === 'Escape') { - finish(false) + finishInlineEditForUUID(false) } }) } @@ -750,7 +750,7 @@ export function initAnnotationsPanel(viewer) { * * @param {boolean} commit - true to save, false to cancel */ - function finish(commit) { + function finishInlineDescriptionEdit(commit) { const newText = commit ? ta.value.trim() || '' : oldText const displayText = newText ? newText : 'Annotation Description' const newDesc = document.createElement('div') @@ -795,7 +795,7 @@ export function initAnnotationsPanel(viewer) { _commitEditedDescription(uuid, newText) } - ta.addEventListener('blur', () => finish(true)) + ta.addEventListener('blur', () => finishInlineDescriptionEdit(true)) ta.addEventListener('keydown', (e) => { if (e.key === 'Enter') { // Shift+Enter: insert newline (allow default). Enter: finish and save. @@ -804,9 +804,9 @@ export function initAnnotationsPanel(viewer) { return } e.preventDefault() - finish(true) + finishInlineDescriptionEdit(true) } else if (e.key === 'Escape') { - finish(false) + finishInlineDescriptionEdit(false) } }) } From 4795405fe7870ccf5e467b97ab5f041181806bfe Mon Sep 17 00:00:00 2001 From: franmagn Date: Thu, 23 Oct 2025 19:30:55 +0200 Subject: [PATCH 33/34] refactor(#4): guard jQuery according to the scary rabbit --- src/AnnotationControl/annotationPanel.js | 25 +++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index a1d6b8f..eb9f429 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -48,9 +48,9 @@ export function initAnnotationsPanel(viewer) { menu.appendChild(panel) } - if ($(menu).accordion) { + if (window.jQuery && window.jQuery(menu).accordion) { try { - $(menu).accordion('refresh') + window.jQuery(menu).accordion('refresh') } catch (e) {} } @@ -118,6 +118,7 @@ export function initAnnotationsPanel(viewer) { // Helper: safe accessors and utilities for jsTree and annotation descriptions function _getJSTree() { try { + if (!window.jQuery) return null return $('#jstree_scene').jstree && $('#jstree_scene').jstree() } catch (e) { return null @@ -126,6 +127,7 @@ export function initAnnotationsPanel(viewer) { function _getJSTreeInstance() { try { + if (!window.jQuery || !window.jQuery.jstree) return null return ( ($('#jstree_scene').jstree && $('#jstree_scene').jstree(true)) || (typeof $.jstree !== 'undefined' && @@ -260,7 +262,7 @@ export function initAnnotationsPanel(viewer) { ) { const empty = document.createElement('div') empty.className = 'annotation-empty' - empty.textContent = 'No saved positions yet' + empty.textContent = 'No saved locations yet' targetContainer.appendChild(empty) return } @@ -434,10 +436,14 @@ export function initAnnotationsPanel(viewer) { // remove from sidebar/tree try { - $('#jstree_scene').jstree('delete_node', node.id) + if (window.jQuery && window.jQuery.fn.jstree) { + window.jQuery('#jstree_scene').jstree('delete_node', node.id) + } } catch (e) { try { - $.jstree.reference(node.id).delete_node(node.id) + if (window.jQuery && window.jQuery.jstree && window.jQuery.jstree.reference) { + window.jQuery.jstree.reference(node.id).delete_node(node.id) + } } catch (_) {} } updateAnnotationsList() @@ -874,7 +880,8 @@ export function initAnnotationsPanel(viewer) { if (!uuid) return // update jsTree node text try { - const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (!window.jQuery) return + const tree = window.jQuery('#jstree_scene').jstree && window.jQuery('#jstree_scene').jstree() if (tree) { const annotationsRoot = tree.get_json('annotations') if (annotationsRoot && annotationsRoot.children) { @@ -926,7 +933,11 @@ export function initAnnotationsPanel(viewer) { 'menu_camera_annotations' ) if (annotationHeader && annotationHeader.nextElementSibling) { - $(annotationHeader.nextElementSibling).slideDown() + if (window.jQuery) { + window.jQuery(annotationHeader.nextElementSibling).slideDown() + } else { + annotationHeader.nextElementSibling.style.display = '' + } } // Capture current camera view (position) at the moment the user clicks Add let camPos = null From b084e930ba1ab2754a9767568d2bbddabf71cc4d Mon Sep 17 00:00:00 2001 From: franmagn Date: Thu, 23 Oct 2025 19:33:55 +0200 Subject: [PATCH 34/34] style(#4): :art: ran format --- src/AnnotationControl/annotationPanel.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/AnnotationControl/annotationPanel.js b/src/AnnotationControl/annotationPanel.js index eb9f429..4bdd4ec 100644 --- a/src/AnnotationControl/annotationPanel.js +++ b/src/AnnotationControl/annotationPanel.js @@ -441,7 +441,11 @@ export function initAnnotationsPanel(viewer) { } } catch (e) { try { - if (window.jQuery && window.jQuery.jstree && window.jQuery.jstree.reference) { + if ( + window.jQuery && + window.jQuery.jstree && + window.jQuery.jstree.reference + ) { window.jQuery.jstree.reference(node.id).delete_node(node.id) } } catch (_) {} @@ -881,7 +885,9 @@ export function initAnnotationsPanel(viewer) { // update jsTree node text try { if (!window.jQuery) return - const tree = window.jQuery('#jstree_scene').jstree && window.jQuery('#jstree_scene').jstree() + const tree = + window.jQuery('#jstree_scene').jstree && + window.jQuery('#jstree_scene').jstree() if (tree) { const annotationsRoot = tree.get_json('annotations') if (annotationsRoot && annotationsRoot.children) {