From 1514c98e1a3937709a0d70876fa56ca4f825d6f7 Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 28 Oct 2025 18:19:06 +0100 Subject: [PATCH 01/10] refactor(47): :lipstick: remove all unused values from measurement panel pruned the table with all the values related to the point selected, in order to delete all unused and useless attributes while keeping origianl behaviour --- src/MeasurementControl/measurementsPanel.css | 7 ++- src/MeasurementControl/measurementsPanel.js | 46 +++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.css b/src/MeasurementControl/measurementsPanel.css index 96f735f..d5fa999 100644 --- a/src/MeasurementControl/measurementsPanel.css +++ b/src/MeasurementControl/measurementsPanel.css @@ -38,9 +38,14 @@ #measurements_list td { padding: 2px 4px; } -#measurements_list tr:nth-child(even) { +#measurements_list table.measurement_value_table tr.alt-even td { background: #3a454b; } + +/* Fallback: ensure odd rows are neutral unless other row-type rules apply */ +#measurements_list table.measurement_value_table tr.alt-odd td { + background: transparent; +} #measurements_list .coordRow td { background: #2f383d; font-family: monospace; diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 6b70ac2..28dc3ac 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -231,7 +231,10 @@ export function initMeasurementsPanel(viewer) { function initCoordObserver() { if (!originalPropertiesPanel || coordRoundObserver) return coordRoundObserver = new MutationObserver(() => { - requestAnimationFrame(() => roundCoordinates(originalPropertiesPanel)) + requestAnimationFrame(() => { + roundCoordinates(originalPropertiesPanel) + pruneMeasurementRows(originalPropertiesPanel) + }) }) coordRoundObserver.observe(originalPropertiesPanel, { childList: true, @@ -283,6 +286,47 @@ export function initMeasurementsPanel(viewer) { }) }) } + + // Hide unwanted measurement attribute rows (keep only relevant ones) + function pruneMeasurementRows(rootEl) { + if (!rootEl) rootEl = originalPropertiesPanel + if (!rootEl) return + const tables = rootEl.querySelectorAll('table.measurement_value_table') + if (!tables || tables.length === 0) return + // Labels we want to keep + const keep = new Set(['point source id', 'accepted', 'tvu', 'thu']) + tables.forEach((tbl) => { + // Detect coordinate table (header with th: x y z) and skip pruning for it + const headerRow = tbl.querySelector('tr') + 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') { + return + } + } + ;[...tbl.querySelectorAll('tr')].forEach((row) => { + const firstTd = row.querySelector('td') + if (!firstTd) return + const txt = (firstTd.textContent || '').trim().toLowerCase() + if (!keep.has(txt)) { + row.style.display = 'none' + } else { + row.style.display = '' + } + }) + // After hiding rows, reapply alternating classes to visible rows so + // zebra striping remains correct even when some rows are display:none + const visibleRows = [...tbl.querySelectorAll('tr')].filter( + (r) => r.style.display !== 'none' + ) + visibleRows.forEach((r, i) => { + r.classList.remove('alt-even', 'alt-odd') + r.classList.add(i % 2 === 0 ? 'alt-odd' : 'alt-even') + }) + }) + } // Helper to decide if a uuid is a measurement-like object function isMeasurementUUID(uuid) { if (!uuid) return false From 370755161ef1d9f4551016c84ac96897236c75c8 Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 29 Oct 2025 12:21:32 +0100 Subject: [PATCH 02/10] refactor(#47): :lipstick: add Latitude, Longitude and Elevation to the measurement panel --- src/MeasurementControl/measurementsPanel.js | 147 +++++++++++++++++++- 1 file changed, 143 insertions(+), 4 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 28dc3ac..1152df6 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -1,3 +1,5 @@ +import { ecef, wgs84 } from '../config.js' + /** * Measurements Panel * Injects a custom Measurements tab section, shows grouped measurement @@ -198,6 +200,8 @@ export function initMeasurementsPanel(viewer) { } requestAnimationFrame(() => { roundCoordinates(originalPropertiesPanel) + insertLatLonRows(originalPropertiesPanel) + pruneMeasurementRows(originalPropertiesPanel) initCoordObserver() }) } @@ -234,6 +238,7 @@ export function initMeasurementsPanel(viewer) { requestAnimationFrame(() => { roundCoordinates(originalPropertiesPanel) pruneMeasurementRows(originalPropertiesPanel) + insertLatLonRows(originalPropertiesPanel) }) }) coordRoundObserver.observe(originalPropertiesPanel, { @@ -294,20 +299,34 @@ export function initMeasurementsPanel(viewer) { const tables = rootEl.querySelectorAll('table.measurement_value_table') if (!tables || tables.length === 0) return // Labels we want to keep - const keep = new Set(['point source id', 'accepted', 'tvu', 'thu']) + const keep = new Set([ + 'point source id', + 'accepted', + 'tvu', + 'thu', + 'latitude', + 'longitude', + 'elevation' + ]) tables.forEach((tbl) => { - // Detect coordinate table (header with th: x y z) and skip pruning for it + // Detect if this table is the coordinates table (header with th: x y z) const headerRow = tbl.querySelector('tr') + let isCoordTable = false 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') { - return + isCoordTable = true } } + ;[...tbl.querySelectorAll('tr')].forEach((row) => { - const firstTd = row.querySelector('td') + const tds = [...row.querySelectorAll('td')] + // If this is the coordinates table, skip pruning rows that look like + // coordinate rows (they have 3 or more td columns). + if (isCoordTable && tds.length >= 3) return + const firstTd = tds[0] if (!firstTd) return const txt = (firstTd.textContent || '').trim().toLowerCase() if (!keep.has(txt)) { @@ -327,6 +346,126 @@ export function initMeasurementsPanel(viewer) { }) }) } + + // Insert Latitude / Longitude / Elevation rows into the attribute table + function insertLatLonRows(rootEl) { + if (!rootEl) rootEl = originalPropertiesPanel + if (!rootEl) return + const tables = Array.from(rootEl.querySelectorAll('table.measurement_value_table')) + if (!tables.length) return + + // helper to parse numeric strings + const parseNum = (s) => { + if (!s) return null + const cleaned = (s + '').replace(/[^0-9+\-.,]/g, '').replace(/,+/g, '') + return /[-+]?\d*\.?\d+/.test(cleaned) ? Number(cleaned) : null + } + + // Find coord table (header x,y,z) and first data row + let coordTable = null + let coordinateRow = null + 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') { + coordTable = tbl + const rows = Array.from(tbl.querySelectorAll('tr')) + for (let i = 0; i < rows.length; i++) { + if (rows[i] === header) { + for (let j = i + 1; j < rows.length; j++) { + const tds = Array.from(rows[j].querySelectorAll('td')) + if (tds.length >= 3) { + const nx = parseNum(tds[0].textContent) + const ny = parseNum(tds[1].textContent) + const nz = parseNum(tds[2].textContent) + if (nx != null && ny != null && nz != null) { + coordinateRow = rows[j] + break + } + } + } + break + } + } + break + } + } + + let x = null, y = null, z = null + if (coordinateRow) { + const tds = Array.from(coordinateRow.querySelectorAll('td')) + x = parseNum(tds[0].textContent) + y = parseNum(tds[1].textContent) + z = parseNum(tds[2].textContent) + } + + // 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) { + try { + const res = proj(ecef, wgs84, [x, y, z]) + lon = res[0] + lat = res[1] + elev = res[2] + } catch (_e) {} + } + + // 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] + + 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 + } + + 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)) + } + } + + 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) { if (!uuid) return false From 9c0c73d10712f093dff9aa18cfc9d78da5c80235 Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 29 Oct 2025 12:55:44 +0100 Subject: [PATCH 03/10] fix(#47): :bug: fix conflict bug with other measurement tools visualization fixed a graphic bug that made not possible to see x,y and z coordinates on other measurement tools sections in the sidebar, and that showed Latitude, Longitude and Elevation in those sections --- src/MeasurementControl/measurementsPanel.js | 107 +++++++++++++++++--- 1 file changed, 94 insertions(+), 13 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 1152df6..a64202f 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -8,7 +8,7 @@ import { ecef, wgs84 } from '../config.js' */ export function initMeasurementsPanel(viewer) { // Track last selected measurement label for dynamic data title - const lastSelection = { uuid: null, label: '' } + const lastSelection = { uuid: null, label: '', type: null } // Resolve or create measurements container in Potree menu const existingListContainer = document.getElementById('measurements_list') let targetContainer = existingListContainer @@ -198,6 +198,15 @@ export function initMeasurementsPanel(viewer) { } } } + // Set or remove the mounted marker to run only for + // single-point measurements. + try { + if (lastSelection && lastSelection.type === 'Point') { + originalPropertiesPanel.setAttribute('data-mp-mounted', '1') + } else { + originalPropertiesPanel.removeAttribute('data-mp-mounted') + } + } catch (e) {} requestAnimationFrame(() => { roundCoordinates(originalPropertiesPanel) insertLatLonRows(originalPropertiesPanel) @@ -212,6 +221,7 @@ export function initMeasurementsPanel(viewer) { originalPropertiesPanel, placeholder.nextSibling ) + try { originalPropertiesPanel.removeAttribute('data-mp-mounted') } catch (e) {} } if (targetContainer && targetContainer.children.length === 0) { targetContainer.innerHTML = '' @@ -236,6 +246,11 @@ export function initMeasurementsPanel(viewer) { if (!originalPropertiesPanel || coordRoundObserver) return coordRoundObserver = new MutationObserver(() => { requestAnimationFrame(() => { + // 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' + if (!mounted) return roundCoordinates(originalPropertiesPanel) pruneMeasurementRows(originalPropertiesPanel) insertLatLonRows(originalPropertiesPanel) @@ -248,25 +263,43 @@ export function initMeasurementsPanel(viewer) { } function roundCoordinates(rootEl) { + if (!rootEl) rootEl = originalPropertiesPanel 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' + if (!mounted) return // Find first table that has a header row with th: x y z const tables = rootEl.querySelectorAll('table.measurement_value_table') let coordTable = null + const looksLikeNumber = (s) => { + if (!s) return false + const cleaned = (s + '').replace(/[^0-9+\-.,eE]/g, '').replace(/,+/g, '') + return /[-+]?\d*\.?\d+(e[-+]?\d+)?/.test(cleaned) + } for (const tbl of tables) { const headerRow = tbl.querySelector('tr') - if (!headerRow) continue - const ths = [...headerRow.querySelectorAll('th')].map((th) => - (th.textContent || '').trim().toLowerCase() - ) - if ( - ths.length >= 3 && - ths[0] === 'x' && - ths[1] === 'y' && - ths[2] === 'z' - ) { - coordTable = tbl - break + let found = false + 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') { + coordTable = tbl + break + } + } + // Fallback: detect a row with 3 numeric td cells (some panels use td headers) + 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))) { + coordTable = tbl + found = true + break + } } + if (found) break } if (!coordTable) return @@ -296,6 +329,10 @@ export function initMeasurementsPanel(viewer) { function pruneMeasurementRows(rootEl) { if (!rootEl) rootEl = originalPropertiesPanel 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' + if (!mounted) return const tables = rootEl.querySelectorAll('table.measurement_value_table') if (!tables || tables.length === 0) return // Labels we want to keep @@ -312,6 +349,11 @@ export function initMeasurementsPanel(viewer) { // Detect if this table is the coordinates table (header with th: x y z) const headerRow = tbl.querySelector('tr') let isCoordTable = false + const looksLikeNumber = (s) => { + if (!s) return false + 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() @@ -320,6 +362,16 @@ export function initMeasurementsPanel(viewer) { isCoordTable = true } } + if (!isCoordTable) { + 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))) { + isCoordTable = true + break + } + } + } ;[...tbl.querySelectorAll('tr')].forEach((row) => { const tds = [...row.querySelectorAll('td')] @@ -351,6 +403,9 @@ export function initMeasurementsPanel(viewer) { function insertLatLonRows(rootEl) { 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' + if (!mounted) return const tables = Array.from(rootEl.querySelectorAll('table.measurement_value_table')) if (!tables.length) return @@ -390,6 +445,22 @@ export function initMeasurementsPanel(viewer) { } break } + // Fallback: look for a row with 3 numeric tds anywhere in the table + const rows = Array.from(tbl.querySelectorAll('tr')) + for (let j = 0; j < rows.length; j++) { + const tds = Array.from(rows[j].querySelectorAll('td')) + if (tds.length >= 3) { + const nx = parseNum(tds[0].textContent) + const ny = parseNum(tds[1].textContent) + const nz = parseNum(tds[2].textContent) + if (nx != null && ny != null && nz != null) { + coordTable = tbl + coordinateRow = rows[j] + break + } + } + } + if (coordinateRow) break } let x = null, y = null, z = null @@ -489,10 +560,20 @@ export function initMeasurementsPanel(viewer) { if (labelEl) { lastSelection.uuid = uuid lastSelection.label = labelEl.textContent.trim() + // Also store the measurement type (Point/Profile/Area/Height/etc.) + try { + const scene = viewer.scene + const all = [...scene.measurements, ...scene.profiles, ...scene.volumes] + const obj = all.find((o) => o.uuid === uuid) + lastSelection.type = obj ? resolveType(obj) : null + } catch (_e) { + lastSelection.type = null + } } } else { lastSelection.uuid = null lastSelection.label = '' + lastSelection.type = null } } From 087cefd49075041324c9bbb5f208079ed1865c74 Mon Sep 17 00:00:00 2001 From: franmagn Date: Thu, 30 Oct 2025 12:47:47 +0100 Subject: [PATCH 04/10] feat(#47): add label for each measurement tool placed on the pointcloud on the globe add a simple label with the name of the measured object, taken from the sidebar measurement list. It automatically gets removed once the measured object is removed, and it moves with the moving of the points. --- src/MeasurementControl/measurementsPanel.js | 151 ++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index d8c59c0..e8766bd 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -66,6 +66,22 @@ export function initMeasurementsPanel(viewer) { let listRoot = document.createElement('div') listRoot.id = 'measurement_items' listRoot.className = 'measurement-items-root' + // Overlay container for on-canvas measurement labels (Point #N) + const renderArea = document.getElementById('potree_render_area') + let overlay = null + const overlayMap = new Map() // uuid -> label element + if (renderArea) { + overlay = document.createElement('div') + overlay.id = 'measurement_label_overlay' + overlay.style.position = 'absolute' + overlay.style.left = '0' + overlay.style.top = '0' + overlay.style.width = '100%' + overlay.style.height = '100%' + overlay.style.pointerEvents = 'none' + overlay.style.zIndex = '2000' + renderArea.appendChild(overlay) + } const listDivider = document.createElement('div') listDivider.className = 'divider' const dividerSpan = document.createElement('span') @@ -103,6 +119,113 @@ export function initMeasurementsPanel(viewer) { return indexMap } + // Project a THREE.Vector3 (or [x,y,z]) into screen coords using viewer + 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]) + 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) + 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 + if (!cam) return null + vec3.project(cam) + // renderer 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 + const x = (vec3.x * 0.5 + 0.5) * w + const y = (-vec3.y * 0.5 + 0.5) * h + // check if behind camera + const visible = vec3.z < 1 + return { x, y, visible } + } catch (e) { + return null + } + } + + // Return a representative 3D position for a measurement-like object. + // For single-point measurements we return that point; for multi-point + // measurements we return the centroid of all point positions. + function getMeasurementRepresentativePosition(o) { + try { + const THREE = window.THREE || globalThis.THREE + if (!o || !o.points || o.points.length === 0) return null + if (o.points.length === 1) { + return o.points[0].position || o.points[0] + } + const v = new THREE.Vector3(0, 0, 0) + let count = 0 + for (const pt of o.points) { + const p = pt.position || pt + if (!p) continue + v.x += p.x + v.y += p.y + v.z += p.z + count++ + } + if (count === 0) return null + v.x /= count + v.y /= count + v.z /= count + return v + } catch (e) { + return null + } + } + + // Create or update an on-canvas label for a measurement object + function createOrUpdateMeasurementCanvasLabel(measurement, labelText) { + if (!overlay || !measurement || !measurement.uuid) return null + let lbl = overlayMap.get(measurement.uuid) + if (!lbl) { + lbl = document.createElement('div') + lbl.className = 'measurement-canvas-label' + lbl.style.position = 'absolute' + lbl.style.transform = 'translate(-50%, -100%)' + lbl.style.pointerEvents = 'none' + lbl.style.color = 'white' + lbl.style.fontWeight = 'bold' + lbl.style.textShadow = '0 0 4px rgba(0,0,0,0.9)' + lbl.style.fontSize = '12px' + overlay.appendChild(lbl) + overlayMap.set(measurement.uuid, lbl) + } + lbl.textContent = labelText || '' + return lbl + } + + function updateOverlayPositions() { + if (!overlay) return + for (const [uuid, el] of overlayMap.entries()) { + try { + const scene = viewer.scene + const all = [...scene.measurements, ...scene.profiles, ...scene.volumes] + const obj = all.find((o) => o.uuid === uuid) + if (!obj) { + el.style.display = 'none' + continue + } + // Get a representative position (centroid or first point) + const rep = getMeasurementRepresentativePosition(obj) + const pos = projectToScreen(rep) + if (!pos || !pos.visible) { + el.style.display = 'none' + } else { + el.style.display = '' + el.style.transform = `translate(${Math.round(pos.x)}px, ${Math.round(pos.y)}px)` + } + } catch (e) { + // ignore + } + } + } + const TYPE_ICONS = { Point: '●', Distance: '﹔', @@ -666,6 +789,13 @@ export function initMeasurementsPanel(viewer) { row.appendChild(labelSpan) row.appendChild(delBtn) body.appendChild(row) + + // If this measurement has one or more points, create/update an overlay + // 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) + } }) countSpan.textContent = groups.get(type).length }) @@ -693,6 +823,19 @@ export function initMeasurementsPanel(viewer) { showPanelInMeasurements() } } + // Cleanup overlay labels for removed measurements and update positions + try { + // remove overlay labels for uuids that no longer exist + const known = new Set(itemsRaw.map((it) => it.obj.uuid)) + for (const k of Array.from(overlayMap.keys())) { + if (!known.has(k)) { + const el = overlayMap.get(k) + if (el && el.parentElement) el.parentElement.removeChild(el) + overlayMap.delete(k) + } + } + updateOverlayPositions() + } catch (e) {} } rebuildMeasurementList() @@ -814,6 +957,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)) + } + } catch (e) {} + window.addEventListener('resize', () => requestAnimationFrame(updateOverlayPositions)) + // Click handling for selection, focus and delete listRoot.addEventListener('click', (e) => { const header = e.target.closest('.m-group-header') From df109d3c8b33d9cfa93239b6ef306fd42df40f17 Mon Sep 17 00:00:00 2001 From: franmagn Date: Thu, 30 Oct 2025 13:03:32 +0100 Subject: [PATCH 05/10] refactor(#47): :zap: improve updating of labels on the UI made labels come up faster after a movement, not requiring a camera update anymore --- src/MeasurementControl/measurementsPanel.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index e8766bd..16e4152 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -862,6 +862,24 @@ export function initMeasurementsPanel(viewer) { }) } catch (_e) {} }) + + // Listen to movement events separately so we can update overlay label + // positions immediately when a marker/point is moved + ;['marker_moved', 'marker_dropped', 'point_moved'].forEach((ev) => { + try { + obj.addEventListener(ev, () => { + // schedule a position update on next frame + requestAnimationFrame(() => updateOverlayPositions()) + // If this measurement is currently selected, refresh the panel too + if (lastSelection.uuid === obj.uuid) { + try { + updateActiveSelection(obj.uuid) + showPanelInMeasurements() + } catch (_e) {} + } + }) + } catch (_e) {} + }) } // Distance/Height/Angle measurements can change type based on point count From 39ac3f27502f6afe0a4252740647c0671054d563 Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 4 Nov 2025 17:35:56 +0100 Subject: [PATCH 06/10] style(#47): :lipstick: add background to the labels add background to make the labels visible even while displaying black or white datasets --- src/MeasurementControl/measurementsPanel.css | 17 +++++++++++++++++ src/MeasurementControl/measurementsPanel.js | 12 +++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.css b/src/MeasurementControl/measurementsPanel.css index e767c65..74f4e9c 100644 --- a/src/MeasurementControl/measurementsPanel.css +++ b/src/MeasurementControl/measurementsPanel.css @@ -305,4 +305,21 @@ pointer-events: none; } +/* On-canvas measurement label styling for improved contrast */ +.measurement-canvas-label { + position: absolute; + transform: translate(-50%, -100%); + pointer-events: none; + color: #000; + 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); + 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 16e4152..cb77cba 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' @@ -186,13 +186,6 @@ export function initMeasurementsPanel(viewer) { if (!lbl) { lbl = document.createElement('div') lbl.className = 'measurement-canvas-label' - lbl.style.position = 'absolute' - lbl.style.transform = 'translate(-50%, -100%)' - lbl.style.pointerEvents = 'none' - lbl.style.color = 'white' - lbl.style.fontWeight = 'bold' - lbl.style.textShadow = '0 0 4px rgba(0,0,0,0.9)' - lbl.style.fontSize = '12px' overlay.appendChild(lbl) overlayMap.set(measurement.uuid, lbl) } @@ -218,7 +211,8 @@ export function initMeasurementsPanel(viewer) { el.style.display = 'none' } else { el.style.display = '' - el.style.transform = `translate(${Math.round(pos.x)}px, ${Math.round(pos.y)}px)` + el.style.left = Math.round(pos.x) + 'px' + el.style.top = Math.round(pos.y) + 'px' } } catch (e) { // ignore From c00de21339414ef1bfeabcce1f87f806e9291503 Mon Sep 17 00:00:00 2001 From: franmagn Date: Tue, 4 Nov 2025 18:14:44 +0100 Subject: [PATCH 07/10] style(#47): ran prettier --- src/MeasurementControl/measurementsPanel.css | 14 +- src/MeasurementControl/measurementsPanel.js | 274 +++++++++++++------ 2 files changed, 192 insertions(+), 96 deletions(-) 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) { From 4ada5761e69093405fe85a0801006b83340df02a Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 5 Nov 2025 14:21:06 +0100 Subject: [PATCH 08/10] feat(#47): visualize name labels OR value labels, or hide both now it is not possible to see both name labels and value labels of the measurements, and it's possible to switch between these visualization thanks to two new buttons. There is also the possibility to hide both with a third button, all in the same section of the sidebar --- src/MeasurementControl/measurementsPanel.js | 99 ++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index bc47e2f..aef16fd 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -70,6 +70,8 @@ export function initMeasurementsPanel(viewer) { const renderArea = document.getElementById('potree_render_area') let overlay = null const overlayMap = new Map() // uuid -> label element + // set default display mode of the label buttons + let measurementDisplayMode = 'VALUES' if (renderArea) { overlay = document.createElement('div') overlay.id = 'measurement_label_overlay' @@ -204,6 +206,10 @@ export function initMeasurementsPanel(viewer) { overlayMap.set(measurement.uuid, lbl) } lbl.textContent = labelText || '' + try { + // only show overlay name labels when in NAMES mode + lbl.style.display = measurementDisplayMode === 'NAMES' ? '' : 'none' + } catch (e) {} return lbl } @@ -226,7 +232,8 @@ export function initMeasurementsPanel(viewer) { if (!pos || !pos.visible) { el.style.display = 'none' } else { - el.style.display = '' + // Only show overlay name labels when NAMES mode is active + el.style.display = measurementDisplayMode === 'NAMES' ? '' : 'none' el.style.left = Math.round(pos.x) + 'px' el.style.top = Math.round(pos.y) + 'px' } @@ -272,7 +279,9 @@ export function initMeasurementsPanel(viewer) { 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 + // 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() { @@ -1219,6 +1228,32 @@ export function initMeasurementsPanel(viewer) { if (toolsHost) { const measOptions = document.getElementById('measurement_options_show') if (measOptions) { + // Replace the built two-option control with a three-option label handling + try { + const hasValues = + !!measOptions.querySelector('#measurement_options_show_values') || + !!measOptions.querySelector('option[value="VALUES"]') + if (!hasValues) { + measOptions.innerHTML = `\n + \n + \n + \n + ` + } + + if ( + window && + window.jQuery && + typeof $(measOptions).selectgroup === 'function' + ) { + try { + $(measOptions).selectgroup({ title: 'Show/Hide labels' }) + // trigger the VALUES option so UI appears selected and handlers run + const $val = $(measOptions).find('input[value="VALUES"]') + if ($val.length) $val.trigger('click') + } catch (e) {} + } + } catch (e) {} const measLi = measOptions.closest('li') || measOptions if (measLi && !measLi.classList.contains('measurement-options-block')) { measLi.classList.add('measurement-options-block') @@ -1238,6 +1273,66 @@ export function initMeasurementsPanel(viewer) { if (measLi && measLi.parentElement !== toolsHost) { toolsHost.appendChild(measLi) } + // Force initial VALUES mode here + try { + if (window && window.jQuery) { + $(document).trigger('measurement_display_mode', ['VALUES']) + } + // also set Potree measuring tool state directly if present + if (viewer && viewer.measuringTool) { + try { + viewer.measuringTool.showLabels = true // show numeric/value labels + } catch (e) {} + } + } catch (e) {} + } + } + + // Listen for measurement display mode events (VALUES / NAMES / HIDE) + // and toggle the overlay name labels we create in this panel. + try { + const applyDisplayMode = (mode) => { + // remember the chosen mode so new overlays follow it + measurementDisplayMode = mode || measurementDisplayMode + if (!overlay || !overlayMap) return + for (const el of overlayMap.values()) { + if (!el) continue + if (measurementDisplayMode === 'NAMES') { + // show name overlays + el.style.display = '' + } else { + // hide name overlays in both VALUES and HIDE modes + el.style.display = 'none' + } + } + // Also ensure the measuringTool's numeric/value labels follow the mode. + try { + if (viewer && viewer.measuringTool) { + // show numeric/value labels only in VALUES mode + viewer.measuringTool.showLabels = measurementDisplayMode === 'VALUES' + } + } catch (e) {} + } + + // If jQuery triggers the custom event from potree toolbar, handle it + if (window && window.jQuery) { + $(document).on('measurement_display_mode', (e, mode) => { + applyDisplayMode(mode) + }) + } + + // Also attach a direct listener to input clicks inside the control in case + // the selectgroup implementation creates native inputs. + const measEl = document.getElementById('measurement_options_show') + if (measEl) { + measEl.addEventListener('click', (ev) => { + const input = + ev.target && ev.target.closest && ev.target.closest('input') + const value = input ? input.value : null + if (value) applyDisplayMode(value) + }) } + } catch (e) { + // non-fatal if event hooks cannot be installed } } From 7d2fb71b01ef530d6de8a35f0758eb1d1506c989 Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 5 Nov 2025 15:12:38 +0100 Subject: [PATCH 09/10] style(#47): :art: moved stylings to .css --- src/MeasurementControl/measurementsPanel.css | 11 +++++++++++ src/MeasurementControl/measurementsPanel.js | 8 +------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.css b/src/MeasurementControl/measurementsPanel.css index cf5405e..eba5091 100644 --- a/src/MeasurementControl/measurementsPanel.css +++ b/src/MeasurementControl/measurementsPanel.css @@ -322,3 +322,14 @@ white-space: nowrap; z-index: 2100; } + +/* Container overlay for on-canvas measurement labels. */ +.measurement-label-overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 2000; +} diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 00f3517..e01442f 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -75,13 +75,7 @@ export function initMeasurementsPanel(viewer) { if (renderArea) { overlay = document.createElement('div') overlay.id = 'measurement_label_overlay' - overlay.style.position = 'absolute' - overlay.style.left = '0' - overlay.style.top = '0' - overlay.style.width = '100%' - overlay.style.height = '100%' - overlay.style.pointerEvents = 'none' - overlay.style.zIndex = '2000' + overlay.classList.add('measurement-label-overlay') renderArea.appendChild(overlay) } const listDivider = document.createElement('div') From e269d394e77b92429b013728fb95b1b9a468eff5 Mon Sep 17 00:00:00 2001 From: franmagn Date: Wed, 5 Nov 2025 15:27:03 +0100 Subject: [PATCH 10/10] fix(#47): :bug: fix deletion of name labels in UI fixed behaviour after deleting measurements. Now UI is immediately refreshed, not needing anymore to move the camera position to see the labels of deleted points disappear --- src/MeasurementControl/measurementsPanel.js | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index e01442f..5be335e 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -918,9 +918,13 @@ export function initMeasurementsPanel(viewer) { // Cleanup overlay labels for removed measurements and update positions try { // remove overlay labels for uuids that no longer exist - const known = new Set(itemsRaw.map((it) => it.obj.uuid)) + const uuidsWithPoints = new Set( + itemsRaw + .filter((it) => it.obj && it.obj.points && it.obj.points.length > 0) + .map((it) => it.obj.uuid) + ) for (const k of Array.from(overlayMap.keys())) { - if (!known.has(k)) { + if (!uuidsWithPoints.has(k)) { const el = overlayMap.get(k) if (el && el.parentElement) el.parentElement.removeChild(el) overlayMap.delete(k) @@ -947,6 +951,16 @@ export function initMeasurementsPanel(viewer) { try { obj.addEventListener(ev, () => { rebuildMeasurementList() + // If this object no longer has any points, remove any overlay + // immediately so the on-canvas label doesn't linger. + try { + if (!obj.points || obj.points.length === 0) { + const ol = overlayMap.get(obj.uuid) + if (ol && ol.parentElement) ol.parentElement.removeChild(ol) + overlayMap.delete(obj.uuid) + } + } catch (_e) {} + if (lastSelection.uuid === obj.uuid) { updateActiveSelection(obj.uuid) showPanelInMeasurements() @@ -1112,6 +1126,13 @@ export function initMeasurementsPanel(viewer) { scene.removeVolume(obj) else if (scene.removeProfile && scene.profiles.includes(obj)) scene.removeProfile(obj) + // Remove any on-canvas overlay immediately for this uuid so the + // label doesn't linger while other async updates occur. + try { + const ol = overlayMap.get(obj.uuid) + if (ol && ol.parentElement) ol.parentElement.removeChild(ol) + overlayMap.delete(obj.uuid) + } catch (_e) {} rebuildMeasurementList() return }