diff --git a/index.html b/index.html index 84536ed..f2e6d11 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,6 @@ type="text/css" href="/libs/jstree/themes/mixed/style.css" /> - @@ -75,18 +74,18 @@ $('#menu_scene').next().show() $('#menu_filters').next().show() viewer.toggleSidebar() - + // Initialize custom measurements panel (global function from measurementsPanel.js) - if(window.initMeasurementsPanel){ - window.initMeasurementsPanel(viewer); + if (window.initMeasurementsPanel) { + window.initMeasurementsPanel(viewer) } else { - console.warn('initMeasurementsPanel not found on window'); + console.warn('initMeasurementsPanel not found on window') } }) let url = './pointclouds/data_converted/metadata.json' - Potree.loadPointCloud(url).then(e => { + Potree.loadPointCloud(url).then((e) => { let pointcloud = e.pointcloud let material = pointcloud.material @@ -97,7 +96,7 @@ viewer.scene.addPointCloud(pointcloud) viewer.fitToScreen() - }) + }) 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) } }