From f5ce5f5461e8430916f9a8fca26201a42bec20a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 24 Sep 2025 12:53:27 +0200 Subject: [PATCH 01/18] feat(#5): :sparkles: Add menu sections for measurements This new menu section will display measurements of points and distances, and display extra attributes --- index.html | 181 ++++++++++++++++++++++++++++++- public/build/potree/sidebar.html | 11 ++ src/measurementsPanel.css | 10 ++ src/measurementsPanel.js | 126 +++++++++++++++++++++ 4 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 src/measurementsPanel.css create mode 100644 src/measurementsPanel.js diff --git a/index.html b/index.html index c77bfb1..3f8c243 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,176 @@ type="text/css" href="/libs/jstree/themes/mixed/style.css" /> + + + @@ -53,7 +223,9 @@
+ diff --git a/public/build/potree/sidebar.html b/public/build/potree/sidebar.html index 1552427..93b6ca7 100644 --- a/public/build/potree/sidebar.html +++ b/public/build/potree/sidebar.html @@ -116,6 +116,17 @@
+ + +
+ +
Point Measurements
+ +
+ +
+
+
diff --git a/src/measurementsPanel.css b/src/measurementsPanel.css new file mode 100644 index 0000000..bbc1863 --- /dev/null +++ b/src/measurementsPanel.css @@ -0,0 +1,10 @@ +#measurements_list .mcard{background:#2d373c;border:1px solid #1c2428;border-radius:4px;margin:6px 4px;padding:6px 8px;font-family:inherit;font-size:12px;color:#eee;} +#measurements_list .mheader{display:flex;align-items:center;margin-bottom:4px;font-weight:bold;} +#measurements_list .mstatus{width:8px;height:8px;border-radius:50%;background:#68d96e;margin-right:6px;} +#measurements_list .mdelete{margin-left:auto;cursor:pointer;color:#d55;font-weight:bold;background:transparent;border:none;font-size:14px;} +#measurements_list table{width:100%;border-collapse:collapse;margin-top:4px;} +#measurements_list td{padding:2px 4px;} +#measurements_list tr:nth-child(even){background:#3a454b;} +#measurements_list .valueRow td{font-weight:bold;text-align:center;background:#4a555b;} +#measurements_list .attrRow td{font-size:11px;color:#cfd5d8;} +#measurements_list .empty{opacity:.6;padding:8px;text-align:center;} diff --git a/src/measurementsPanel.js b/src/measurementsPanel.js new file mode 100644 index 0000000..5c76af7 --- /dev/null +++ b/src/measurementsPanel.js @@ -0,0 +1,126 @@ +export function initMeasurementsPanel(viewer){ + const container = document.getElementById('measurements_list'); + if(!container){ + console.warn('Measurements list container not found'); + return; + } + + const state = new Map(); + let needsRender = true; + + function formatNumber(n, p=2){ + if(n===undefined||n===null||isNaN(n)) return '-'; + return Number(n).toFixed(p); + } + + function distance(a,b){ + const A = a.position || a; const B = b.position || b; + return Math.sqrt((A.x-B.x)**2 + (A.y-B.y)**2 + (A.z-B.z)**2); + } + + function inferType(m){ + if(m.points && m.points.length===1) return 'potCoordinate'; + if(m.points && m.points.length>1) return 'potDistance'; + return 'Measurement'; + } + + function addMeasurement(m){ + state.set(m.uuid, {measurement: m, dirty:true}); + needsRender = true; + } + + function removeMeasurement(m){ + state.delete(m.uuid); + needsRender = true; + } + + function markDirty(m){ + const entry = state.get(m.uuid); + if(entry){ entry.dirty = true; needsRender = true; } + } + + function collectAttributes(point){ + if(!point) return []; + const attrs = []; + const accepted = point.accepted ?? point.ACCEPTED ?? point.Accepted; + const tvu = point.TVU ?? point.tvu; + const thu = point.THU ?? point.thu; + if(accepted !== undefined) attrs.push(['accepted', accepted]); + if(tvu !== undefined) attrs.push(['TVU', tvu]); + if(thu !== undefined) attrs.push(['THU', thu]); + return attrs; + } + + function render(){ + if(!needsRender) return; + needsRender = false; + container.innerHTML = ''; + if(state.size===0){ + container.innerHTML = '
No measurements
'; + return; + } + + for(const {measurement:m} of state.values()){ + const points = m.points || []; + const card = document.createElement('div'); + card.className='mcard'; + + let body = ''; + for(const pt of points){ + const vec = pt.position || pt; + body += ``; + const attrs = collectAttributes(pt); + for(const [k,v] of attrs){ + body += ``; + } + } + if(points.length>1){ + let total=0; + for(let i=0;i`; + } + body += ``; + } + body += '
${formatNumber(vec.x)}, ${formatNumber(vec.y)}, ${formatNumber(vec.z)}
${k}: ${v}
${formatNumber(seg)}
${formatNumber(total)}
'; + card.innerHTML = ` + + ${body}`; + container.appendChild(card); + } + } + + viewer.scene.addEventListener('measurement_added', e=> addMeasurement(e.measurement)); + viewer.scene.addEventListener('measurement_removed', e=> removeMeasurement(e.measurement)); + + viewer.addEventListener('update', ()=>{ + for(const entry of state.values()){ + const m = entry.measurement; + if(!m.points) continue; + // If any point still at (0,0,0) while others not, mark dirty; or if distances change + if(m.points.some(pt => (pt.position||pt).x!==0 || (pt.position||pt).y!==0 || (pt.position||pt).z!==0)){ + if(entry.dirty){ + } else if(Math.random()<0.02){ // light periodic refresh safety + entry.dirty=true; + } + } + } + if([...state.values()].some(e=>e.dirty)){ + for(const e of state.values()) e.dirty=false; + needsRender=true; + } + render(); + }); + + // Initial existing measurements + for(const m of viewer.scene.measurements){ addMeasurement(m); } + render(); + + container.addEventListener('click', e=>{ + const btn = e.target.closest('button.mdelete'); + if(!btn) return; + const m = state.get(btn.getAttribute('data-uuid'))?.measurement; + if(m){ viewer.scene.removeMeasurement(m); } + }); +} From 6f4ed05f5c6ae1fe6d629bfbcb279548fe7f10ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 24 Sep 2025 13:34:20 +0200 Subject: [PATCH 02/18] fix(#5): :bug: better display of distance measurement --- src/measurementsPanel.js | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/measurementsPanel.js b/src/measurementsPanel.js index 5c76af7..88a55ea 100644 --- a/src/measurementsPanel.js +++ b/src/measurementsPanel.js @@ -34,11 +34,6 @@ export function initMeasurementsPanel(viewer){ needsRender = true; } - function markDirty(m){ - const entry = state.get(m.uuid); - if(entry){ entry.dirty = true; needsRender = true; } - } - function collectAttributes(point){ if(!point) return []; const attrs = []; @@ -66,22 +61,28 @@ export function initMeasurementsPanel(viewer){ card.className='mcard'; let body = ''; - for(const pt of points){ - const vec = pt.position || pt; - body += ``; - const attrs = collectAttributes(pt); - for(const [k,v] of attrs){ - body += ``; - } - } - if(points.length>1){ + if(points.length>0){ let total=0; - for(let i=0;i`; + const isDistance = points.length>1; + for(let i=0;i`; + if(!isDistance){ + const attrs = collectAttributes(pt); + for(const [k,v] of attrs){ + body += ``; + } + } + if(i < points.length -1){ + const seg = distance(points[i], points[i+1]); + total += seg; + body += ``; + } + } + if(points.length>1){ + body += ``; } - body += ``; } body += '
${formatNumber(vec.x)}, ${formatNumber(vec.y)}, ${formatNumber(vec.z)}
${k}: ${v}
${formatNumber(seg)}
${formatNumber(vec.x)}, ${formatNumber(vec.y)}, ${formatNumber(vec.z)}
${k}: ${v}
${formatNumber(seg)} m
${formatNumber(total)} m
${formatNumber(total)}
'; card.innerHTML = ` From acd5f4b2164d34eb63e953cf3d123f3c651b7429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 24 Sep 2025 13:35:14 +0200 Subject: [PATCH 03/18] refactor(#5): :recycle: move styling from index file to measurements css file --- index.html | 169 -------------------------------------- src/measurementsPanel.css | 12 ++- 2 files changed, 11 insertions(+), 170 deletions(-) diff --git a/index.html b/index.html index 3f8c243..29196d7 100644 --- a/index.html +++ b/index.html @@ -24,175 +24,6 @@ href="/libs/jstree/themes/mixed/style.css" /> - - diff --git a/src/measurementsPanel.css b/src/measurementsPanel.css index bbc1863..af89261 100644 --- a/src/measurementsPanel.css +++ b/src/measurementsPanel.css @@ -5,6 +5,16 @@ #measurements_list table{width:100%;border-collapse:collapse;margin-top:4px;} #measurements_list td{padding:2px 4px;} #measurements_list tr:nth-child(even){background:#3a454b;} -#measurements_list .valueRow td{font-weight:bold;text-align:center;background:#4a555b;} +#measurements_list .coordRow td{background:#2f383d;font-family:monospace;font-size:11px;padding:4px 6px;border:1px solid #404a50;border-radius:4px;margin:2px 0;} +#measurements_list .segmentRow td{background:transparent;padding:0 0 2px 0;} +#measurements_list .segmentConnector{width:1px;height:12px;background:#505b61;margin:2px auto 0;} +#measurements_list .segmentPill{display:inline-block;background:#3d474d;border:1px solid #566067;padding:2px 10px;margin:2px auto 4px;border-radius:6px;font-size:11px;font-family:monospace;} +#measurements_list .totalRow td{background:#424d53;font-weight:600;font-size:12px;text-align:center;border:1px solid #59646a;border-radius:6px;margin-top:4px;} +#measurements_list .separator td{padding:0;height:2px;background:transparent;} #measurements_list .attrRow td{font-size:11px;color:#cfd5d8;} #measurements_list .empty{opacity:.6;padding:8px;text-align:center;} +#measurements_list{max-height:400px;overflow-y:auto;margin:10px 0;} +#measurements_list::-webkit-scrollbar{width:8px;} +#measurements_list::-webkit-scrollbar-track{background:var(--bg-dark-color);} +#measurements_list::-webkit-scrollbar-thumb{background:var(--bg-color-2);border-radius:4px;} +#measurements_list::-webkit-scrollbar-thumb:hover{background:var(--color-1);} From 75c5b4370198174a1fd42699d1bedff5b8b38a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 1 Oct 2025 12:45:04 +0200 Subject: [PATCH 04/18] feat(#5): :sparkles: Display measurements in a sorted list, and move the tools for measurements to the same tab Closes #5 --- index.html | 13 +- public/build/potree/sidebar.html | 11 - src/measurementsPanel.css | 20 ++ src/measurementsPanel.js | 386 +++++++++++++++++++++++-------- 4 files changed, 318 insertions(+), 112 deletions(-) diff --git a/index.html b/index.html index 29196d7..84536ed 100644 --- a/index.html +++ b/index.html @@ -55,8 +55,8 @@
- + diff --git a/src/measurementsPanel.css b/src/measurementsPanel.css index 67d7d1f..9e05476 100644 --- a/src/measurementsPanel.css +++ b/src/measurementsPanel.css @@ -1,40 +1,228 @@ -#measurements_list .mcard{background:#2d373c;border:1px solid #1c2428;border-radius:4px;margin:6px 4px;padding:6px 8px;font-family:inherit;font-size:12px;color:#eee;} -#measurements_list .mheader{display:flex;align-items:center;margin-bottom:4px;font-weight:bold;} -#measurements_list .mstatus{width:8px;height:8px;border-radius:50%;background:#68d96e;margin-right:6px;} -#measurements_list .mdelete{margin-left:auto;cursor:pointer;color:#d55;font-weight:bold;background:transparent;border:none;font-size:14px;} -#measurements_list table{width:100%;border-collapse:collapse;margin-top:4px;} -#measurements_list td{padding:2px 4px;} -#measurements_list tr:nth-child(even){background:#3a454b;} -#measurements_list .coordRow td{background:#2f383d;font-family:monospace;font-size:11px;padding:4px 6px;border:1px solid #404a50;border-radius:4px;margin:2px 0;} -#measurements_list .segmentRow td{background:transparent;padding:0 0 2px 0;} -#measurements_list .segmentConnector{width:1px;height:12px;background:#505b61;margin:2px auto 0;} -#measurements_list .segmentPill{display:inline-block;background:#3d474d;border:1px solid #566067;padding:2px 10px;margin:2px auto 4px;border-radius:6px;font-size:11px;font-family:monospace;} -#measurements_list .totalRow td{background:#424d53;font-weight:600;font-size:12px;text-align:center;border:1px solid #59646a;border-radius:6px;margin-top:4px;} -#measurements_list .separator td{padding:0;height:2px;background:transparent;} -#measurements_list .attrRow td{font-size:11px;color:#cfd5d8;} -#measurements_list .empty{opacity:.6;padding:8px;text-align:center;} -#measurements_list{max-height:400px;overflow-y:auto;margin:10px 0;} -#measurements_list::-webkit-scrollbar{width:8px;} -#measurements_list::-webkit-scrollbar-track{background:var(--bg-dark-color);} -#measurements_list::-webkit-scrollbar-thumb{background:var(--bg-color-2);border-radius:4px;} -#measurements_list::-webkit-scrollbar-thumb:hover{background:var(--color-1);} +#measurements_list .mcard { + background: #2d373c; + border: 1px solid #1c2428; + border-radius: 4px; + margin: 6px 4px; + padding: 6px 8px; + font-family: inherit; + font-size: 12px; + color: #eee; +} +#measurements_list .mheader { + display: flex; + align-items: center; + margin-bottom: 4px; + font-weight: bold; +} +#measurements_list .mstatus { + width: 8px; + height: 8px; + border-radius: 50%; + background: #68d96e; + margin-right: 6px; +} +#measurements_list .mdelete { + margin-left: auto; + cursor: pointer; + color: #d55; + font-weight: bold; + background: transparent; + border: none; + font-size: 14px; +} +#measurements_list table { + width: 100%; + border-collapse: collapse; + margin-top: 4px; +} +#measurements_list td { + padding: 2px 4px; +} +#measurements_list tr:nth-child(even) { + background: #3a454b; +} +#measurements_list .coordRow td { + background: #2f383d; + font-family: monospace; + font-size: 11px; + padding: 4px 6px; + border: 1px solid #404a50; + border-radius: 4px; + margin: 2px 0; +} +#measurements_list .segmentRow td { + background: transparent; + padding: 0 0 2px 0; +} +#measurements_list .segmentConnector { + width: 1px; + height: 12px; + background: #505b61; + margin: 2px auto 0; +} +#measurements_list .segmentPill { + display: inline-block; + background: #3d474d; + border: 1px solid #566067; + padding: 2px 10px; + margin: 2px auto 4px; + border-radius: 6px; + font-size: 11px; + font-family: monospace; +} +#measurements_list .totalRow td { + background: #424d53; + font-weight: 600; + font-size: 12px; + text-align: center; + border: 1px solid #59646a; + border-radius: 6px; + margin-top: 4px; +} +#measurements_list .separator td { + padding: 0; + height: 2px; + background: transparent; +} +#measurements_list .attrRow td { + font-size: 11px; + color: #cfd5d8; +} +#measurements_list .empty { + opacity: 0.6; + padding: 8px; + text-align: center; +} +#measurements_list { + max-height: 400px; + overflow-y: auto; + margin: 10px 0; +} +#measurements_list::-webkit-scrollbar { + width: 8px; +} +#measurements_list::-webkit-scrollbar-track { + background: var(--bg-dark-color); +} +#measurements_list::-webkit-scrollbar-thumb { + background: var(--bg-color-2); + border-radius: 4px; +} +#measurements_list::-webkit-scrollbar-thumb:hover { + background: var(--color-1); +} /* Modern grouped measurement list */ -#measurement_items{background:#252d31;border:1px solid #1b2326;border-radius:6px;font-size:12px;color:#d9e2e6;} -#measurement_items .m-empty{padding:10px;text-align:center;opacity:.6;} -#measurement_items .m-group{border-top:1px solid #303a3f;} -#measurement_items .m-group:first-child{border-top:none;} -#measurement_items .m-group-header{display:flex;align-items:center;gap:6px;padding:6px 10px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;font-size:11px;background:linear-gradient(#2f383d,#2b3438);cursor:pointer;user-select:none;position:sticky;top:0;z-index:1;} -#measurement_items .m-group-header:hover{background:#374247;} -#measurement_items .m-group-caret{font-size:10px;width:14px;text-align:center;color:#8fb9c9;} -#measurement_items .m-group-title{flex:1;color:#c7d4d9;} -#measurement_items .m-group-count{background:#3d4850;color:#9fb7c2;font-size:10px;padding:2px 6px;border-radius:10px;line-height:1;} -#measurement_items .m-group-body{padding:4px 4px 6px;} -#measurement_items .m-row{display:flex;align-items:center;gap:6px;padding:6px 8px;margin:2px 2px;border:1px solid transparent;border-radius:4px;background:#2c3539;transition:background .15s,border-color .15s;cursor:pointer;} -#measurement_items .m-row-icon{width:16px;flex:0 0 16px;text-align:center;font-size:12px;color:#8fb9c9;filter:drop-shadow(0 0 2px #0a0f12);} -#measurement_items .m-row:hover{background:#354045;border-color:#425056;} -#measurement_items .m-row.active{background:#1f4b63;border-color:#2f6b8c;box-shadow:0 0 0 1px #2f6b8c66;} -#measurement_items .m-row-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} -#measurement_items .m-row-delete{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 .15s, color .15s;} -#measurement_items .m-row-delete:hover{background:#5a2d2d;color:#fff;} - +#measurement_items { + background: #252d31; + border: 1px solid #1b2326; + border-radius: 6px; + font-size: 12px; + color: #d9e2e6; +} +#measurement_items .m-empty { + padding: 10px; + text-align: center; + opacity: 0.6; +} +#measurement_items .m-group { + border-top: 1px solid #303a3f; +} +#measurement_items .m-group:first-child { + border-top: none; +} +#measurement_items .m-group-header { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + font-size: 11px; + background: linear-gradient(#2f383d, #2b3438); + cursor: pointer; + user-select: none; + position: sticky; + top: 0; + z-index: 1; +} +#measurement_items .m-group-header:hover { + background: #374247; +} +#measurement_items .m-group-caret { + font-size: 10px; + width: 14px; + text-align: center; + color: #8fb9c9; +} +#measurement_items .m-group-title { + flex: 1; + color: #c7d4d9; +} +#measurement_items .m-group-count { + background: #3d4850; + color: #9fb7c2; + font-size: 10px; + padding: 2px 6px; + border-radius: 10px; + line-height: 1; +} +#measurement_items .m-group-body { + padding: 4px 4px 6px; +} +#measurement_items .m-row { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + margin: 2px 2px; + border: 1px solid transparent; + border-radius: 4px; + background: #2c3539; + transition: + background 0.15s, + border-color 0.15s; + cursor: pointer; +} +#measurement_items .m-row-icon { + width: 16px; + flex: 0 0 16px; + text-align: center; + font-size: 12px; + color: #8fb9c9; + filter: drop-shadow(0 0 2px #0a0f12); +} +#measurement_items .m-row:hover { + background: #354045; + border-color: #425056; +} +#measurement_items .m-row.active { + background: #1f4b63; + border-color: #2f6b8c; + box-shadow: 0 0 0 1px #2f6b8c66; +} +#measurement_items .m-row-label { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#measurement_items .m-row-delete { + 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; +} +#measurement_items .m-row-delete:hover { + background: #5a2d2d; + color: #fff; +} diff --git a/src/measurementsPanel.js b/src/measurementsPanel.js index 7af504d..e2052ea 100644 --- a/src/measurementsPanel.js +++ b/src/measurementsPanel.js @@ -1,321 +1,368 @@ /** * Measurements Panel - * Injects a custom Measurements accordion section, shows grouped measurement + * Injects a custom Measurements tab section, shows grouped measurement * entries with per-type numbering, syncs selection with Potree's jsTree, and * dynamically mounts the native properties panel when a measurement is active. */ -window.initMeasurementsPanel = function initMeasurementsPanel(viewer){ +window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { // Resolve or create measurements container in Potree menu - const existingListContainer = document.getElementById('measurements_list'); - let targetContainer = existingListContainer; - if(!targetContainer){ - const menu = document.getElementById('potree_menu'); - if(menu){ - const header = document.createElement('h3'); - header.id = 'menu_point_measurements'; - header.innerHTML = 'Measurements'; - const panel = document.createElement('div'); - panel.className = 'pv-menu-list'; - panel.innerHTML = '
'; + const existingListContainer = document.getElementById('measurements_list') + let targetContainer = existingListContainer + if (!targetContainer) { + const menu = document.getElementById('potree_menu') + if (menu) { + const header = document.createElement('h3') + header.id = 'menu_point_measurements' + header.innerHTML = 'Measurements' + const panel = document.createElement('div') + panel.className = 'pv-menu-list' + panel.innerHTML = + '
' // Insert before filters/about if possible, else append at end - const about = document.getElementById('menu_about'); - if(about){ - menu.insertBefore(panel, about); - menu.insertBefore(header, panel); + const about = document.getElementById('menu_about') + if (about) { + menu.insertBefore(panel, about) + menu.insertBefore(header, panel) } else { - menu.appendChild(header); - menu.appendChild(panel); + menu.appendChild(header) + menu.appendChild(panel) } // Activate tab behavior if jQuery UI accordion already initialized - if($(menu).accordion){ - try { $(menu).accordion('refresh'); } catch(e) {} + if ($(menu).accordion) { + try { + $(menu).accordion('refresh') + } catch (e) {} } // Toggle on header click if not managed by accordion refresh - header.addEventListener('click', ()=> panel.style.display = panel.style.display==='none'?'':'none'); - targetContainer = panel.querySelector('#measurements_list'); + header.addEventListener( + 'click', + () => + (panel.style.display = panel.style.display === 'none' ? '' : 'none') + ) + targetContainer = panel.querySelector('#measurements_list') } } - if(!targetContainer){ - console.warn('Measurements list container not found and dynamic injection failed'); - return; + if (!targetContainer) { + console.warn( + 'Measurements list container not found and dynamic injection failed' + ) + return } - let listRoot = document.createElement('div'); - listRoot.id = 'measurement_items'; - listRoot.style.cssText = 'max-height:260px; overflow:auto; margin-bottom:6px; border:1px solid #333; border-radius:4px;'; - targetContainer.parentElement && targetContainer.parentElement.insertBefore(listRoot, targetContainer); + let listRoot = document.createElement('div') + listRoot.id = 'measurement_items' + listRoot.style.cssText = + 'max-height:260px; overflow:auto; margin-bottom:6px; border:1px solid #333; border-radius:4px;' + targetContainer.parentElement && + targetContainer.parentElement.insertBefore(listRoot, targetContainer) // Creating an incremental number for each measurment type - const creationOrder = []; - const uuidRef = new Map(); + const creationOrder = [] + const uuidRef = new Map() - function trackCreation(obj){ - if(!obj || !obj.uuid) return; - if(!uuidRef.has(obj.uuid)){ - uuidRef.set(obj.uuid, obj); - creationOrder.push(obj.uuid); + function trackCreation(obj) { + if (!obj || !obj.uuid) return + if (!uuidRef.has(obj.uuid)) { + uuidRef.set(obj.uuid, obj) + creationOrder.push(obj.uuid) } else { - uuidRef.set(obj.uuid, obj); + uuidRef.set(obj.uuid, obj) } } - function buildTypeIndices(items){ - const indexMap = new Map(); - for(const uuid of creationOrder){ - const entry = items.find(it=>it.obj.uuid===uuid); - if(!entry) continue; - const t = entry.type; - if(!indexMap.has(t)) indexMap.set(t, new Map()); - const map = indexMap.get(t); - if(!map.has(uuid)) map.set(uuid, map.size + 1); + function buildTypeIndices(items) { + const indexMap = new Map() + for (const uuid of creationOrder) { + const entry = items.find((it) => it.obj.uuid === uuid) + if (!entry) continue + const t = entry.type + if (!indexMap.has(t)) indexMap.set(t, new Map()) + const map = indexMap.get(t) + if (!map.has(uuid)) map.set(uuid, map.size + 1) } - return indexMap; + return indexMap } const TYPE_ICONS = { - 'Point':'●', - 'Distance':'﹔', - 'Height':'↕', - 'Area':'▧', - 'Angle':'∠', - 'Circle':'◯', - 'Azimuth':'N', - 'Volume':'▣', - 'Profile':'≋', - 'Measurement':'●' - }; + Point: '●', + Distance: '﹔', + Height: '↕', + Area: '▧', + Angle: '∠', + Circle: '◯', + Azimuth: 'N', + Volume: '▣', + Profile: '≋', + Measurement: '●' + } - function resolveType(m){ - const ctor = m.constructor?.name || ''; - if(/Volume/i.test(ctor)) return 'Volume'; - if(/Profile/i.test(ctor)) return 'Profile'; - if(/Circle/i.test(ctor) || m.showCircle) return 'Circle'; - if(/Angle/i.test(ctor) || m.showAngle) return 'Angle'; - if(/Height/i.test(ctor) || m.showHeight && m.points?.length >= 2) return 'Height'; - if(/Azimuth/i.test(ctor) || m.showAzimuth) return 'Azimuth'; - if(/Area/i.test(ctor) || (m.showDistances && m.showArea)) return 'Area'; - if(m.className) return m.className; + function resolveType(m) { + const ctor = m.constructor?.name || '' + if (/Volume/i.test(ctor)) return 'Volume' + if (/Profile/i.test(ctor)) return 'Profile' + if (/Circle/i.test(ctor) || m.showCircle) return 'Circle' + if (/Angle/i.test(ctor) || m.showAngle) return 'Angle' + if (/Height/i.test(ctor) || (m.showHeight && m.points?.length >= 2)) + return 'Height' + if (/Azimuth/i.test(ctor) || m.showAzimuth) return 'Azimuth' + if (/Area/i.test(ctor) || (m.showDistances && m.showArea)) return 'Area' + if (m.className) return m.className // Fallback heuristics on point count - if(m.points){ - if(m.points.length === 1) return 'Point'; - if(m.points.length === 2 && m.showHeight) return 'Height'; - return 'Distance'; + if (m.points) { + if (m.points.length === 1) return 'Point' + if (m.points.length === 2 && m.showHeight) return 'Height' + return 'Distance' } - return 'Measurement'; + return 'Measurement' } - const originalPropertiesPanel = document.querySelector('#scene_object_properties'); + const originalPropertiesPanel = document.querySelector( + '#scene_object_properties' + ) // This section is for moving the "scene" object to the meaasurements tab, only if the measurements section is selected, so that all other folders (like camera) stays where it should be - let placeholder = null; - let originalParent = null; - function ensurePlaceholder(){ - if(originalPropertiesPanel && !placeholder){ - originalParent = originalPropertiesPanel.parentElement; - placeholder = document.createElement('div'); - placeholder.id = 'scene_object_properties_placeholder'; - placeholder.style.display='none'; - originalParent.insertBefore(placeholder, originalPropertiesPanel); + let placeholder = null + let originalParent = null + function ensurePlaceholder() { + if (originalPropertiesPanel && !placeholder) { + originalParent = originalPropertiesPanel.parentElement + placeholder = document.createElement('div') + placeholder.id = 'scene_object_properties_placeholder' + placeholder.style.display = 'none' + originalParent.insertBefore(placeholder, originalPropertiesPanel) } } - function showPanelInMeasurements(){ - ensurePlaceholder(); - if(!originalPropertiesPanel) return; - if(originalPropertiesPanel.parentElement !== targetContainer){ - targetContainer.innerHTML=''; - targetContainer.appendChild(originalPropertiesPanel); + function showPanelInMeasurements() { + ensurePlaceholder() + if (!originalPropertiesPanel) return + if (originalPropertiesPanel.parentElement !== targetContainer) { + targetContainer.innerHTML = '' + targetContainer.appendChild(originalPropertiesPanel) } } - function restorePanelToOriginal(){ - if(!originalPropertiesPanel || !placeholder || !originalParent) return; - if(originalPropertiesPanel.parentElement !== originalParent){ - originalParent.insertBefore(originalPropertiesPanel, placeholder.nextSibling); + function restorePanelToOriginal() { + if (!originalPropertiesPanel || !placeholder || !originalParent) return + if (originalPropertiesPanel.parentElement !== originalParent) { + originalParent.insertBefore( + originalPropertiesPanel, + placeholder.nextSibling + ) } - if(targetContainer && targetContainer.children.length === 0){ - targetContainer.innerHTML = '
Select a measurement to view its properties here
'; + if (targetContainer && targetContainer.children.length === 0) { + targetContainer.innerHTML = + '
Select a measurement to view its properties here
' } } - if(targetContainer){ - targetContainer.innerHTML = '
Select a measurement to view its properties here
'; + if (targetContainer) { + targetContainer.innerHTML = + '
Select a measurement to view its properties here
' } // Helper to decide if a uuid is a measurement-like object - function isMeasurementUUID(uuid){ - if(!uuid) return false; - const s = viewer.scene; - return s.measurements.some(m=>m.uuid===uuid) || s.volumes.some(v=>v.uuid===uuid) || s.profiles.some(p=>p.uuid===uuid); + function isMeasurementUUID(uuid) { + if (!uuid) return false + const s = viewer.scene + return ( + s.measurements.some((m) => m.uuid === uuid) || + s.volumes.some((v) => v.uuid === uuid) || + s.profiles.some((p) => p.uuid === uuid) + ) } // If on load there's already a selected measurement, move panel immediately - setTimeout(()=>{ + setTimeout(() => { try { - const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); - if(tree){ - const sel = tree.get_selected(true)[0]; - if(sel && sel.data && isMeasurementUUID(sel.data.uuid)){ - showPanelInMeasurements(); + const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (tree) { + const sel = tree.get_selected(true)[0] + if (sel && sel.data && isMeasurementUUID(sel.data.uuid)) { + showPanelInMeasurements() } } - } catch(_e) {} - },0); + } catch (_e) {} + }, 0) // Build/update the measurement list - function rebuildMeasurementList(){ - if(!listRoot) return; - const scene = viewer.scene; + function rebuildMeasurementList() { + if (!listRoot) return + const scene = viewer.scene const itemsRaw = [ - ...scene.measurements.map(m=>({type: resolveType(m), obj:m})), - ...scene.profiles.map(p=>({type:'Profile', obj:p})), - ...scene.volumes.map(v=>({type:'Volume', obj:v})) - ]; - itemsRaw.forEach(entry=> trackCreation(entry.obj)); - const perTypeNumbers = buildTypeIndices(itemsRaw); + ...scene.measurements.map((m) => ({ type: resolveType(m), obj: m })), + ...scene.profiles.map((p) => ({ type: 'Profile', obj: p })), + ...scene.volumes.map((v) => ({ type: 'Volume', obj: v })) + ] + itemsRaw.forEach((entry) => trackCreation(entry.obj)) + const perTypeNumbers = buildTypeIndices(itemsRaw) // Group by type while preserving overall creation order within each type - const groups = new Map(); - for(const entry of itemsRaw){ - if(!groups.has(entry.type)) groups.set(entry.type, []); - groups.get(entry.type).push(entry); + const groups = new Map() + for (const entry of itemsRaw) { + if (!groups.has(entry.type)) groups.set(entry.type, []) + groups.get(entry.type).push(entry) } - const order = Array.from(groups.keys()).sort(); - listRoot.innerHTML = ''; - if(itemsRaw.length===0){ - listRoot.innerHTML = '
No measurements
'; - return; + const order = Array.from(groups.keys()).sort() + listRoot.innerHTML = '' + if (itemsRaw.length === 0) { + listRoot.innerHTML = '
No measurements
' + return } - order.forEach(type=>{ - const section = document.createElement('div'); - section.className = 'm-group'; - const gid = 'g_'+type.toLowerCase(); - section.innerHTML = `\n
\n \n ${type}\n \n
\n
`; - listRoot.appendChild(section); - const body = section.querySelector('.m-group-body'); - groups.get(type).forEach(entry=>{ - const m = entry.obj; - const num = perTypeNumbers.get(type)?.get(m.uuid) || 0; - const baseName = `${type} #${num}`; - const row = document.createElement('div'); - row.className = 'm-row'; - row.dataset.uuid = m.uuid; - const icon = TYPE_ICONS[type] || TYPE_ICONS['Measurement']; - row.innerHTML = `${icon}${baseName}`+ - ''; - body.appendChild(row); - }); - section.querySelector('#'+gid+'_count').textContent = groups.get(type).length; - }); + order.forEach((type) => { + const section = document.createElement('div') + section.className = 'm-group' + const gid = 'g_' + type.toLowerCase() + section.innerHTML = `\n
\n \n ${type}\n \n
\n
` + listRoot.appendChild(section) + const body = section.querySelector('.m-group-body') + groups.get(type).forEach((entry) => { + const m = entry.obj + const num = perTypeNumbers.get(type)?.get(m.uuid) || 0 + const baseName = `${type} #${num}` + const row = document.createElement('div') + row.className = 'm-row' + row.dataset.uuid = m.uuid + const icon = TYPE_ICONS[type] || TYPE_ICONS['Measurement'] + row.innerHTML = + `${icon}${baseName}` + + '' + body.appendChild(row) + }) + section.querySelector('#' + gid + '_count').textContent = + groups.get(type).length + }) } - rebuildMeasurementList(); + rebuildMeasurementList() // Hook into scene add/remove events to refresh list - viewer.scene.addEventListener('measurement_added', (e)=>{ - const obj = e.measurement || e.object || e.detail || null; + viewer.scene.addEventListener('measurement_added', (e) => { + const obj = e.measurement || e.object || e.detail || null // Some measurements start as a point, then become distance or height when adding more points to it. This is a listener for that. - if(obj && obj.addEventListener && !obj._mp_listenersAttached){ - obj._mp_listenersAttached = true; + if (obj && obj.addEventListener && !obj._mp_listenersAttached) { + obj._mp_listenersAttached = true // Potree measurement objects usually fire their own events; fallback to polling if needed - ['marker_added','marker_removed','point_added','point_removed'].forEach(ev=>{ - try { obj.addEventListener(ev, ()=> rebuildMeasurementList()); } catch(_e) {} - }); + ;[ + 'marker_added', + 'marker_removed', + 'point_added', + 'point_removed' + ].forEach((ev) => { + try { + obj.addEventListener(ev, () => rebuildMeasurementList()) + } catch (_e) {} + }) } - rebuildMeasurementList(); - if(obj && isMeasurementUUID(obj.uuid)){ - showPanelInMeasurements(); + rebuildMeasurementList() + if (obj && isMeasurementUUID(obj.uuid)) { + showPanelInMeasurements() } else { - const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); - if(tree){ - const sel = tree.get_selected(true)[0]; - if(sel && sel.data && isMeasurementUUID(sel.data.uuid)){ - showPanelInMeasurements(); + const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (tree) { + const sel = tree.get_selected(true)[0] + if (sel && sel.data && isMeasurementUUID(sel.data.uuid)) { + showPanelInMeasurements() } } } - }); + }) //volume and profile have their own event handlers and wont be included in the basic "measurement_added" - viewer.scene.addEventListener('measurement_removed', rebuildMeasurementList); - viewer.scene.addEventListener('volume_added', rebuildMeasurementList); - viewer.scene.addEventListener('volume_removed', rebuildMeasurementList); - viewer.scene.addEventListener('profile_added', rebuildMeasurementList); - viewer.scene.addEventListener('profile_removed', rebuildMeasurementList); + viewer.scene.addEventListener('measurement_removed', rebuildMeasurementList) + viewer.scene.addEventListener('volume_added', rebuildMeasurementList) + viewer.scene.addEventListener('volume_removed', rebuildMeasurementList) + viewer.scene.addEventListener('profile_added', rebuildMeasurementList) + viewer.scene.addEventListener('profile_removed', rebuildMeasurementList) // Click handling for selection, focus and delete - listRoot.addEventListener('click', (e)=>{ - const header = e.target.closest('.m-group-header'); - if(header){ - const open = header.getAttribute('data-open') === 'true'; - header.setAttribute('data-open', String(!open)); - const caret = header.querySelector('.m-group-caret'); - const body = header.parentElement.querySelector('.m-group-body'); - if(body){ body.style.display = open ? 'none':'block'; } - if(caret){ caret.textContent = open ? '▸':'▾'; } - return; + listRoot.addEventListener('click', (e) => { + const header = e.target.closest('.m-group-header') + if (header) { + const open = header.getAttribute('data-open') === 'true' + header.setAttribute('data-open', String(!open)) + const caret = header.querySelector('.m-group-caret') + const body = header.parentElement.querySelector('.m-group-body') + if (body) { + body.style.display = open ? 'none' : 'block' + } + if (caret) { + caret.textContent = open ? '▸' : '▾' + } + return } - const btn = e.target.closest('button'); - const row = e.target.closest('.m-row[data-uuid]'); - if(!row) return; - const uuid = row.dataset.uuid; - const scene = viewer.scene; - const all = [...scene.measurements, ...scene.profiles, ...scene.volumes]; - const obj = all.find(o=>o.uuid===uuid); - if(!obj) return; - if(btn){ - const act = btn.dataset.act; - if(act==='delete'){ - if(scene.removeMeasurement && scene.measurements.includes(obj)) scene.removeMeasurement(obj); - else if(scene.removeVolume && scene.volumes.includes(obj)) scene.removeVolume(obj); - else if(scene.removeProfile && scene.profiles.includes(obj)) scene.removeProfile(obj); - rebuildMeasurementList(); - return; + const btn = e.target.closest('button') + const row = e.target.closest('.m-row[data-uuid]') + if (!row) return + const uuid = row.dataset.uuid + const scene = viewer.scene + const all = [...scene.measurements, ...scene.profiles, ...scene.volumes] + const obj = all.find((o) => o.uuid === uuid) + if (!obj) return + if (btn) { + const act = btn.dataset.act + if (act === 'delete') { + if (scene.removeMeasurement && scene.measurements.includes(obj)) + scene.removeMeasurement(obj) + else if (scene.removeVolume && scene.volumes.includes(obj)) + scene.removeVolume(obj) + else if (scene.removeProfile && scene.profiles.includes(obj)) + scene.removeProfile(obj) + rebuildMeasurementList() + return } } else { - const treeElement = document.getElementById('jstree_scene'); - if(treeElement && $(treeElement).jstree){ - let measurementsRoot = $('#jstree_scene').jstree().get_json('measurements'); - const findNode = (root) => root.children.find(ch => ch.data && ch.data.uuid === uuid); - let node = measurementsRoot && findNode(measurementsRoot); - if(!node){ - node = measurementsRoot && measurementsRoot.children.find(ch => ch.data && ch.data.uuid === uuid); + const treeElement = document.getElementById('jstree_scene') + if (treeElement && $(treeElement).jstree) { + let measurementsRoot = $('#jstree_scene') + .jstree() + .get_json('measurements') + const findNode = (root) => + root.children.find((ch) => ch.data && ch.data.uuid === uuid) + let node = measurementsRoot && findNode(measurementsRoot) + if (!node) { + node = + measurementsRoot && + measurementsRoot.children.find( + (ch) => ch.data && ch.data.uuid === uuid + ) } - if(node){ - $.jstree.reference(node.id).deselect_all(); - $.jstree.reference(node.id).select_node(node.id); + if (node) { + $.jstree.reference(node.id).deselect_all() + $.jstree.reference(node.id).select_node(node.id) } } - [...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach(el=>{ - el.classList.toggle('active', el.dataset.uuid===uuid); - }); - showPanelInMeasurements(); + ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { + el.classList.toggle('active', el.dataset.uuid === uuid) + }) + showPanelInMeasurements() } - }); + }) // Sync highlight if selection changes via original tree - document.addEventListener('click', (e)=>{ - if(!e.target.closest('#jstree_scene')) return; - setTimeout(()=>{ - const tree = $('#jstree_scene').jstree(); - if(!tree) return; - const sel = tree.get_selected(true)[0]; - if(sel && sel.data && sel.data.uuid){ - const uuid = sel.data.uuid; - [...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach(el=>{ - el.classList.toggle('active', el.dataset.uuid===uuid); - }); + document.addEventListener('click', (e) => { + if (!e.target.closest('#jstree_scene')) return + setTimeout(() => { + const tree = $('#jstree_scene').jstree() + if (!tree) return + const sel = tree.get_selected(true)[0] + if (sel && sel.data && sel.data.uuid) { + const uuid = sel.data.uuid + ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { + el.classList.toggle('active', el.dataset.uuid === uuid) + }) // Determine if selected node is a measurement-like object; if not, restore. - const isMeasurement = sel && sel.data && isMeasurementUUID(sel.data.uuid); - if(isMeasurement){ - showPanelInMeasurements(); + const isMeasurement = + sel && sel.data && isMeasurementUUID(sel.data.uuid) + if (isMeasurement) { + showPanelInMeasurements() } else { - restorePanelToOriginal(); + restorePanelToOriginal() } } - },0); - }); + }, 0) + }) // Move existing tools UI into this section - const toolsHost = document.getElementById('measurement_tools_host'); - const existingTools = document.getElementById('tools'); - if(toolsHost && existingTools){ - toolsHost.appendChild(existingTools); + const toolsHost = document.getElementById('measurement_tools_host') + const existingTools = document.getElementById('tools') + if (toolsHost && existingTools) { + toolsHost.appendChild(existingTools) } } From d028b7eb0914321fa14fa503982396ede1a18568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 1 Oct 2025 13:18:34 +0200 Subject: [PATCH 06/18] refactor(#5): :recycle: Small cleanup --- index.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index e29d5c8..64e8991 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,7 @@ href="/libs/jstree/themes/mixed/style.css" /> + @@ -55,9 +56,9 @@ - - - + - - - diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index c4f8183..507a73e 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -4,7 +4,7 @@ * entries with per-type numbering, syncs selection with Potree's jsTree, and * dynamically mounts the native properties panel when a measurement is active. */ -window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { +window.createMeasurementsPanel = function createMeasurementsPanel(viewer) { // Track last selected measurement label for dynamic data title const lastSelection = { uuid: null, label: '' } // Resolve or create measurements container in Potree menu @@ -676,3 +676,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { } } } + +export function initMeasurementsPanel(viewer) { + return window.createMeasurementsPanel(viewer) +} diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 4644388..fb709f0 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,4 +1,5 @@ import { initElevationControls } from './ElevationControl/elevationControl.js' +import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js' import { ecef } from './config.js' /** @@ -35,6 +36,7 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { viewer.toggleSidebar() initElevationControls(viewer) + initMeasurementsPanel(viewer) }) const e = await Potree.loadPointCloud(pointcloudUrl) From f04fabb50c33d3151a60f211f1a9faac46667712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 6 Oct 2025 13:13:18 +0200 Subject: [PATCH 17/18] ran format --- index.html | 3 +-- src/config.js | 2 +- src/coordinateShowing/coordinateShowing.css | 1 - src/coordinateShowing/coordinateShowing.js | 4 ++-- src/main.js | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index e915c48..9aee3f9 100644 --- a/index.html +++ b/index.html @@ -57,7 +57,6 @@ -
-
+
diff --git a/src/config.js b/src/config.js index 16ac6ec..09c17aa 100644 --- a/src/config.js +++ b/src/config.js @@ -7,4 +7,4 @@ export const POTREE_SETTINGS = { } export const ecef = '+proj=geocent +datum=WGS84 +units=m +no_defs' // EPSG:4978 (geocentric coordinates) -export const wgs84 = '+proj=longlat +datum=WGS84 +no_defs' // EPSG:4326 (geographic coordinates) \ No newline at end of file +export const wgs84 = '+proj=longlat +datum=WGS84 +no_defs' // EPSG:4326 (geographic coordinates) diff --git a/src/coordinateShowing/coordinateShowing.css b/src/coordinateShowing/coordinateShowing.css index dd46e72..0a8ac75 100644 --- a/src/coordinateShowing/coordinateShowing.css +++ b/src/coordinateShowing/coordinateShowing.css @@ -24,4 +24,3 @@ margin-bottom: 10px; border-radius: 5px; } - diff --git a/src/coordinateShowing/coordinateShowing.js b/src/coordinateShowing/coordinateShowing.js index bb7f360..8045cd0 100644 --- a/src/coordinateShowing/coordinateShowing.js +++ b/src/coordinateShowing/coordinateShowing.js @@ -1,5 +1,5 @@ -import { ecef } from "../config.js"; -import { wgs84 } from "../config.js"; +import { ecef } from '../config.js' +import { wgs84 } from '../config.js' const posCanvas = document.getElementById('posCanvas') // lat/lon const elevationCanvas = document.getElementById('elevationCanvas') diff --git a/src/main.js b/src/main.js index af6efff..aaa415a 100644 --- a/src/main.js +++ b/src/main.js @@ -6,7 +6,7 @@ import { initCoordinateCanvases, updateCoordinateText, updateTargetElevation -} from "./CoordinateShowing/coordinateShowing.js"; +} from './CoordinateShowing/coordinateShowing.js' async function init() { window.cesiumViewer = createCesiumViewer('cesiumContainer') From 7af180794d1d17e79d834b60dd9331fcb99e4916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 6 Oct 2025 13:23:42 +0200 Subject: [PATCH 18/18] refactor(#5): :recycle: Small cleanup --- src/MeasurementControl/measurementsPanel.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 507a73e..6b70ac2 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -4,7 +4,7 @@ * entries with per-type numbering, syncs selection with Potree's jsTree, and * dynamically mounts the native properties panel when a measurement is active. */ -window.createMeasurementsPanel = function createMeasurementsPanel(viewer) { +export function initMeasurementsPanel(viewer) { // Track last selected measurement label for dynamic data title const lastSelection = { uuid: null, label: '' } // Resolve or create measurements container in Potree menu @@ -676,7 +676,3 @@ window.createMeasurementsPanel = function createMeasurementsPanel(viewer) { } } } - -export function initMeasurementsPanel(viewer) { - return window.createMeasurementsPanel(viewer) -}