diff --git a/src/MeasurementControl/measurementsPanel.css b/src/MeasurementControl/measurementsPanel.css index 74f4e9c..e9c1aa8 100644 --- a/src/MeasurementControl/measurementsPanel.css +++ b/src/MeasurementControl/measurementsPanel.css @@ -282,7 +282,7 @@ flex-direction: row; align-items: center; margin-top: 10px; - cursor: pointer; + cursor: pointer; border-radius: 4px; } .tool-with-label:hover { @@ -290,7 +290,7 @@ } .tool-with-label:hover img { - filter: brightness(1.7); + filter: brightness(1.7); } .tool-with-label:hover .tool-label { @@ -302,7 +302,7 @@ margin-top: 2px; margin-left: 4px; color: #aaa; - pointer-events: none; + pointer-events: none; } /* On-canvas measurement label styling for improved contrast */ @@ -311,15 +311,13 @@ transform: translate(-50%, -100%); pointer-events: none; color: #000; - background: rgba(255,255,255,0.95); + background: rgba(255, 255, 255, 0.95); padding: 2px 6px; border-radius: 4px; - box-shadow: 0 1px 3px rgba(0,0,0,0.35); - border: 1px solid rgba(0,0,0,0.08); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35); + border: 1px solid rgba(0, 0, 0, 0.08); font-weight: 600; font-size: 12px; white-space: nowrap; z-index: 2100; } - - diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index cb77cba..bc47e2f 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -54,8 +54,8 @@ export function initMeasurementsPanel(viewer) { } }) targetContainer = panel.querySelector('#measurements_list') - } } + } if (!targetContainer) { console.warn( 'Measurements list container not found and dynamic injection failed' @@ -119,23 +119,37 @@ export function initMeasurementsPanel(viewer) { return indexMap } - // Project a THREE.Vector3 (or [x,y,z]) into screen coords using viewer + // Project a THREE.Vector3 (or [x,y,z]) into screen coords using viewer. Used later to display labels. function projectToScreen(pos) { try { const THREE = window.THREE || globalThis.THREE let vec3 = null if (!pos) return null - if (Array.isArray(pos) && pos.length >= 3) vec3 = new THREE.Vector3(pos[0], pos[1], pos[2]) + if (Array.isArray(pos) && pos.length >= 3) + vec3 = new THREE.Vector3(pos[0], pos[1], pos[2]) else if (pos.isVector3) vec3 = pos.clone() - else if (pos.position && pos.position.isVector3) vec3 = pos.position.clone() - else if (pos.x !== undefined && pos.y !== undefined && pos.z !== undefined) vec3 = new THREE.Vector3(pos.x, pos.y, pos.z) + else if (pos.position && pos.position.isVector3) + vec3 = pos.position.clone() + else if ( + pos.x !== undefined && + pos.y !== undefined && + pos.z !== undefined + ) + vec3 = new THREE.Vector3(pos.x, pos.y, pos.z) if (!vec3) return null // choose camera - const cam = (viewer.scene && typeof viewer.scene.getActiveCamera === 'function') ? viewer.scene.getActiveCamera() : (viewer.scene && viewer.scene.camera) || viewer.scene.cameraP || null + const cam = + viewer.scene && typeof viewer.scene.getActiveCamera === 'function' + ? viewer.scene.getActiveCamera() + : (viewer.scene && viewer.scene.camera) || + viewer.scene.cameraP || + null if (!cam) return null vec3.project(cam) // renderer canvas - const canvas = (viewer && viewer.renderer && viewer.renderer.domElement) || document.querySelector('#potree_render_area canvas') + const canvas = + (viewer && viewer.renderer && viewer.renderer.domElement) || + document.querySelector('#potree_render_area canvas') if (!canvas) return null const w = canvas.clientWidth || canvas.width const h = canvas.clientHeight || canvas.height @@ -193,6 +207,8 @@ export function initMeasurementsPanel(viewer) { return lbl } + // Recompute screen positions for all overlay labels and update their + // left/top styles. Hide labels when the measurement or position is not visible. function updateOverlayPositions() { if (!overlay) return for (const [uuid, el] of overlayMap.entries()) { @@ -338,7 +354,9 @@ export function initMeasurementsPanel(viewer) { originalPropertiesPanel, placeholder.nextSibling ) - try { originalPropertiesPanel.removeAttribute('data-mp-mounted') } catch (e) {} + try { + originalPropertiesPanel.removeAttribute('data-mp-mounted') + } catch (e) {} } if (targetContainer && targetContainer.children.length === 0) { targetContainer.innerHTML = '' @@ -366,7 +384,9 @@ export function initMeasurementsPanel(viewer) { // Only run post-processing when the properties panel is mounted into // our measurements area. if (!originalPropertiesPanel) return - const mounted = originalPropertiesPanel.getAttribute && originalPropertiesPanel.getAttribute('data-mp-mounted') === '1' + const mounted = + originalPropertiesPanel.getAttribute && + originalPropertiesPanel.getAttribute('data-mp-mounted') === '1' if (!mounted) return roundCoordinates(originalPropertiesPanel) pruneMeasurementRows(originalPropertiesPanel) @@ -384,7 +404,10 @@ export function initMeasurementsPanel(viewer) { if (!rootEl) return // Only run post-processing when the properties panel is mounted into // our measurements area. - const mounted = originalPropertiesPanel && originalPropertiesPanel.getAttribute && originalPropertiesPanel.getAttribute('data-mp-mounted') === '1' + const mounted = + originalPropertiesPanel && + originalPropertiesPanel.getAttribute && + originalPropertiesPanel.getAttribute('data-mp-mounted') === '1' if (!mounted) return // Find first table that has a header row with th: x y z const tables = rootEl.querySelectorAll('table.measurement_value_table') @@ -401,7 +424,12 @@ export function initMeasurementsPanel(viewer) { const ths = [...headerRow.querySelectorAll('th')].map((th) => (th.textContent || '').trim().toLowerCase() ) - if (ths.length >= 3 && ths[0] === 'x' && ths[1] === 'y' && ths[2] === 'z') { + if ( + ths.length >= 3 && + ths[0] === 'x' && + ths[1] === 'y' && + ths[2] === 'z' + ) { coordTable = tbl break } @@ -410,7 +438,10 @@ export function initMeasurementsPanel(viewer) { const rows = Array.from(tbl.querySelectorAll('tr')) for (const r of rows) { const tds = Array.from(r.querySelectorAll('td')) - if (tds.length >= 3 && tds.every((td) => looksLikeNumber(td.textContent))) { + if ( + tds.length >= 3 && + tds.every((td) => looksLikeNumber(td.textContent)) + ) { coordTable = tbl found = true break @@ -448,7 +479,10 @@ export function initMeasurementsPanel(viewer) { if (!rootEl) return // Only run post-processing when the properties panel is mounted into // our measurements area. - const mounted = originalPropertiesPanel && originalPropertiesPanel.getAttribute && originalPropertiesPanel.getAttribute('data-mp-mounted') === '1' + const mounted = + originalPropertiesPanel && + originalPropertiesPanel.getAttribute && + originalPropertiesPanel.getAttribute('data-mp-mounted') === '1' if (!mounted) return const tables = rootEl.querySelectorAll('table.measurement_value_table') if (!tables || tables.length === 0) return @@ -468,14 +502,21 @@ export function initMeasurementsPanel(viewer) { let isCoordTable = false const looksLikeNumber = (s) => { if (!s) return false - const cleaned = (s + '').replace(/[^0-9+\-.,eE]/g, '').replace(/,+/g, '') + const cleaned = (s + '') + .replace(/[^0-9+\-.,eE]/g, '') + .replace(/,+/g, '') return /[-+]?\d*\.?\d+(e[-+]?\d+)?/.test(cleaned) } if (headerRow) { const ths = [...headerRow.querySelectorAll('th')].map((th) => (th.textContent || '').trim().toLowerCase() ) - if (ths.length >= 3 && ths[0] === 'x' && ths[1] === 'y' && ths[2] === 'z') { + if ( + ths.length >= 3 && + ths[0] === 'x' && + ths[1] === 'y' && + ths[2] === 'z' + ) { isCoordTable = true } } @@ -483,7 +524,10 @@ export function initMeasurementsPanel(viewer) { const rows = Array.from(tbl.querySelectorAll('tr')) for (const r of rows) { const tds = Array.from(r.querySelectorAll('td')) - if (tds.length >= 3 && tds.every((td) => looksLikeNumber(td.textContent))) { + if ( + tds.length >= 3 && + tds.every((td) => looksLikeNumber(td.textContent)) + ) { isCoordTable = true break } @@ -521,9 +565,14 @@ export function initMeasurementsPanel(viewer) { if (!rootEl) rootEl = originalPropertiesPanel if (!rootEl) return // Only insert Lat/Lon/Elev when the properties panel is explicitly mounted - const mounted = originalPropertiesPanel && originalPropertiesPanel.getAttribute && originalPropertiesPanel.getAttribute('data-mp-mounted') === '1' + const mounted = + originalPropertiesPanel && + originalPropertiesPanel.getAttribute && + originalPropertiesPanel.getAttribute('data-mp-mounted') === '1' if (!mounted) return - const tables = Array.from(rootEl.querySelectorAll('table.measurement_value_table')) + const tables = Array.from( + rootEl.querySelectorAll('table.measurement_value_table') + ) if (!tables.length) return // helper to parse numeric strings @@ -539,8 +588,15 @@ export function initMeasurementsPanel(viewer) { for (const tbl of tables) { const header = tbl.querySelector('tr') if (!header) continue - const ths = [...header.querySelectorAll('th')].map((t) => (t.textContent || '').trim().toLowerCase()) - if (ths.length >= 3 && ths[0] === 'x' && ths[1] === 'y' && ths[2] === 'z') { + const ths = [...header.querySelectorAll('th')].map((t) => + (t.textContent || '').trim().toLowerCase() + ) + if ( + ths.length >= 3 && + ths[0] === 'x' && + ths[1] === 'y' && + ths[2] === 'z' + ) { coordTable = tbl const rows = Array.from(tbl.querySelectorAll('tr')) for (let i = 0; i < rows.length; i++) { @@ -580,7 +636,9 @@ export function initMeasurementsPanel(viewer) { if (coordinateRow) break } - let x = null, y = null, z = null + let x = null, + y = null, + z = null if (coordinateRow) { const tds = Array.from(coordinateRow.querySelectorAll('td')) x = parseNum(tds[0].textContent) @@ -589,10 +647,24 @@ export function initMeasurementsPanel(viewer) { } // compute lat/lon/elev - let lat = null, lon = null, elev = null - const hasProj4 = (typeof proj4 !== 'undefined') || (typeof window !== 'undefined' && typeof window.proj4 !== 'undefined') - const proj = typeof proj4 !== 'undefined' ? proj4 : (typeof window !== 'undefined' ? window.proj4 : undefined) - if (hasProj4 && typeof ecef !== 'undefined' && typeof wgs84 !== 'undefined' && x != null) { + let lat = null, + lon = null, + elev = null + const hasProj4 = + typeof proj4 !== 'undefined' || + (typeof window !== 'undefined' && typeof window.proj4 !== 'undefined') + const proj = + typeof proj4 !== 'undefined' + ? proj4 + : typeof window !== 'undefined' + ? window.proj4 + : undefined + if ( + hasProj4 && + typeof ecef !== 'undefined' && + typeof wgs84 !== 'undefined' && + x != null + ) { try { const res = proj(ecef, wgs84, [x, y, z]) lon = res[0] @@ -602,57 +674,73 @@ export function initMeasurementsPanel(viewer) { } // find first attribute-style table - let attrTable = tables.find((t) => Array.from(t.querySelectorAll('tr')).some((r) => r.querySelectorAll('td').length === 2)) || coordTable || tables[0] + let attrTable = + tables.find((t) => + Array.from(t.querySelectorAll('tr')).some( + (r) => r.querySelectorAll('td').length === 2 + ) + ) || + coordTable || + tables[0] - const ids = { lat: 'mp_coord_latitude', lon: 'mp_coord_longitude', elev: 'mp_coord_elevation' } + const ids = { + lat: 'mp_coord_latitude', + lon: 'mp_coord_longitude', + elev: 'mp_coord_elevation' + } const fmt = (v, decimals) => (v == null ? '' : Number(v).toFixed(decimals)) const latStr = lat != null ? fmt(lat, 4) + '˚' : '' const lonStr = lon != null ? fmt(lon, 4) + '˚' : '' const elevStr = elev != null ? fmt(elev, 4) + 'm' : '' - // Prepare rows (create new ones and update existing ones). We collect - // newly-created rows and insert them as a fragment before the first - // child so they appear at the top in the desired order. - const createRowNode = (id, label, value) => { - const row = document.createElement('tr') - row.id = id - row.className = 'attr-row' - const tdLabel = document.createElement('td') - tdLabel.className = 'property-name' - tdLabel.textContent = label - const tdValue = document.createElement('td') - tdValue.className = 'property-value' - tdValue.textContent = value - row.appendChild(tdLabel) - row.appendChild(tdValue) - return row - } + // Prepare rows (create new ones and update existing ones). We collect + // newly-created rows and insert them as a fragment before the first + // child so they appear at the top in the desired order. + const createRowNode = (id, label, value) => { + const row = document.createElement('tr') + row.id = id + row.className = 'attr-row' + const tdLabel = document.createElement('td') + tdLabel.className = 'property-name' + tdLabel.textContent = label + const tdValue = document.createElement('td') + tdValue.className = 'property-value' + tdValue.textContent = value + row.appendChild(tdLabel) + row.appendChild(tdValue) + return row + } - const newRows = [] - const entries = [ - { id: ids.lat, label: 'Latitude', value: latStr }, - { id: ids.lon, label: 'Longitude', value: lonStr }, - { id: ids.elev, label: 'Elevation', value: elevStr } - ] + const newRows = [] + const entries = [ + { id: ids.lat, label: 'Latitude', value: latStr }, + { id: ids.lon, label: 'Longitude', value: lonStr }, + { id: ids.elev, label: 'Elevation', value: elevStr } + ] - for (const e of entries) { - const existing = rootEl.querySelector(`#${e.id}`) - if (existing) { - const valTd = existing.querySelector('td:last-child') || existing.querySelectorAll('td')[1] - if (valTd) valTd.textContent = e.value - } else { - newRows.push(createRowNode(e.id, e.label, e.value)) - } + for (const e of entries) { + const existing = rootEl.querySelector(`#${e.id}`) + if (existing) { + const valTd = + existing.querySelector('td:last-child') || + existing.querySelectorAll('td')[1] + if (valTd) valTd.textContent = e.value + } else { + newRows.push(createRowNode(e.id, e.label, e.value)) } + } - if (newRows.length > 0) { - const tbody = (attrTable.tBodies && attrTable.tBodies[0]) ? attrTable.tBodies[0] : attrTable - const first = tbody.firstElementChild - const frag = document.createDocumentFragment() - for (const r of newRows) frag.appendChild(r) - tbody.insertBefore(frag, first) - } + if (newRows.length > 0) { + const tbody = + attrTable.tBodies && attrTable.tBodies[0] + ? attrTable.tBodies[0] + : attrTable + const first = tbody.firstElementChild + const frag = document.createDocumentFragment() + for (const r of newRows) frag.appendChild(r) + tbody.insertBefore(frag, first) + } } // Helper to decide if a uuid is a measurement-like object function isMeasurementUUID(uuid) { @@ -680,7 +768,11 @@ export function initMeasurementsPanel(viewer) { // Also store the measurement type (Point/Profile/Area/Height/etc.) try { const scene = viewer.scene - const all = [...scene.measurements, ...scene.profiles, ...scene.volumes] + const all = [ + ...scene.measurements, + ...scene.profiles, + ...scene.volumes + ] const obj = all.find((o) => o.uuid === uuid) lastSelection.type = obj ? resolveType(obj) : null } catch (_e) { @@ -788,7 +880,10 @@ export function initMeasurementsPanel(viewer) { // label so the measurement name is visible on the canvas. We use the // sidebar label so the on-canvas label matches the list. if (overlay && m.points && m.points.length > 0) { - createOrUpdateMeasurementCanvasLabel(m, labelSpan.textContent || baseName) + createOrUpdateMeasurementCanvasLabel( + m, + labelSpan.textContent || baseName + ) } }) countSpan.textContent = groups.get(type).length @@ -972,10 +1067,14 @@ export function initMeasurementsPanel(viewer) { // Update overlay positions when camera moves or window resizes try { if (viewer && typeof viewer.addEventListener === 'function') { - viewer.addEventListener('camera_changed', () => requestAnimationFrame(updateOverlayPositions)) + viewer.addEventListener('camera_changed', () => + requestAnimationFrame(updateOverlayPositions) + ) } } catch (e) {} - window.addEventListener('resize', () => requestAnimationFrame(updateOverlayPositions)) + window.addEventListener('resize', () => + requestAnimationFrame(updateOverlayPositions) + ) // Click handling for selection, focus and delete listRoot.addEventListener('click', (e) => { @@ -1090,32 +1189,31 @@ export function initMeasurementsPanel(viewer) { 'sphere_distances.svg': 'Sphere volume', 'profile.svg': '2D height profile', 'reset_tools.svg': 'Remove all' - }; + } - const toolIcons = existingTools.querySelectorAll('img'); - toolIcons.forEach(img => { - const src = img.getAttribute('src'); - const file = src.split('/').pop(); // extract icon name - const baseName = file.replace(/\.[^/.]+$/, ''); + const toolIcons = existingTools.querySelectorAll('img') + toolIcons.forEach((img) => { + const src = img.getAttribute('src') + const file = src.split('/').pop() // extract icon name + const baseName = file.replace(/\.[^/.]+$/, '') if (toolDescriptions[file]) { - const wrapper = document.createElement('div'); - wrapper.className = 'tool-with-label'; - wrapper.id = `tool-wrapper-${baseName}`; + const wrapper = document.createElement('div') + wrapper.className = 'tool-with-label' + wrapper.id = `tool-wrapper-${baseName}` - wrapper.addEventListener('click', () => img.click()); + wrapper.addEventListener('click', () => img.click()) - img.parentNode.insertBefore(wrapper, img); - wrapper.appendChild(img); + img.parentNode.insertBefore(wrapper, img) + wrapper.appendChild(img) - const label = document.createElement('span'); - label.className = 'tool-label'; - label.textContent = toolDescriptions[file]; - label.id = `label-${file.replace(/\.[^/.]+$/, '')}`; - wrapper.appendChild(label); + const label = document.createElement('span') + label.className = 'tool-label' + label.textContent = toolDescriptions[file] + label.id = `label-${file.replace(/\.[^/.]+$/, '')}` + wrapper.appendChild(label) } - }); - + }) // Move measurement options UI into our tools host if (toolsHost) {