diff --git a/src/MeasurementControl/measurementsPanel.css b/src/MeasurementControl/measurementsPanel.css index a848631..eba5091 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; @@ -300,3 +305,31 @@ color: #aaa; 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; +} + +/* 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 3021108..5be335e 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 @@ -6,7 +8,7 @@ */ 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 @@ -64,6 +66,18 @@ 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 + // set default display mode of the label buttons + let measurementDisplayMode = 'VALUES' + if (renderArea) { + overlay = document.createElement('div') + overlay.id = 'measurement_label_overlay' + overlay.classList.add('measurement-label-overlay') + renderArea.appendChild(overlay) + } const listDivider = document.createElement('div') listDivider.className = 'divider' const dividerSpan = document.createElement('span') @@ -101,6 +115,128 @@ export function initMeasurementsPanel(viewer) { return indexMap } + // 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]) + 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' + overlay.appendChild(lbl) + 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 + } + + // 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()) { + 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 { + // 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' + } + } catch (e) { + // ignore + } + } + } + const TYPE_ICONS = { Point: '●', Distance: '﹔', @@ -137,7 +273,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() { @@ -196,8 +334,19 @@ 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) + pruneMeasurementRows(originalPropertiesPanel) initCoordObserver() }) } @@ -208,6 +357,9 @@ export function initMeasurementsPanel(viewer) { originalPropertiesPanel, placeholder.nextSibling ) + try { + originalPropertiesPanel.removeAttribute('data-mp-mounted') + } catch (e) {} } if (targetContainer && targetContainer.children.length === 0) { targetContainer.innerHTML = '' @@ -231,7 +383,18 @@ export function initMeasurementsPanel(viewer) { function initCoordObserver() { if (!originalPropertiesPanel || coordRoundObserver) return coordRoundObserver = new MutationObserver(() => { - requestAnimationFrame(() => roundCoordinates(originalPropertiesPanel)) + 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) + }) }) coordRoundObserver.observe(originalPropertiesPanel, { childList: true, @@ -240,25 +403,54 @@ 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 @@ -283,6 +475,276 @@ export function initMeasurementsPanel(viewer) { }) }) } + + // Hide unwanted measurement attribute rows (keep only relevant ones) + 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 + const keep = new Set([ + 'point source id', + 'accepted', + 'tvu', + 'thu', + 'latitude', + 'longitude', + 'elevation' + ]) + tables.forEach((tbl) => { + // 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() + ) + if ( + ths.length >= 3 && + ths[0] === 'x' && + ths[1] === 'y' && + ths[2] === 'z' + ) { + 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')] + // 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)) { + 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') + }) + }) + } + + // Insert Latitude / Longitude / Elevation rows into the attribute table + 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 + + // 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 + } + // 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 + 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 @@ -306,10 +768,24 @@ 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 } } @@ -402,6 +878,16 @@ 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 }) @@ -429,6 +915,23 @@ 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 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 (!uuidsWithPoints.has(k)) { + const el = overlayMap.get(k) + if (el && el.parentElement) el.parentElement.removeChild(el) + overlayMap.delete(k) + } + } + updateOverlayPositions() + } catch (e) {} } rebuildMeasurementList() @@ -448,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() @@ -455,6 +968,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 @@ -550,6 +1081,18 @@ 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') @@ -583,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 } @@ -693,6 +1243,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') @@ -712,6 +1288,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 } }