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')