From 087cefd49075041324c9bbb5f208079ed1865c74 Mon Sep 17 00:00:00 2001 From: franmagn Date: Thu, 30 Oct 2025 12:47:47 +0100 Subject: [PATCH] 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')