Skip to content

Commit

Permalink
feat(#47): add label for each measurement tool placed on the pointclo…
Browse files Browse the repository at this point in the history
…ud 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.
  • Loading branch information
franmagn committed Oct 30, 2025
1 parent f8b22b0 commit 087cefd
Showing 1 changed file with 151 additions and 0 deletions.
151 changes: 151 additions & 0 deletions src/MeasurementControl/measurementsPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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: '﹔',
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand Down

0 comments on commit 087cefd

Please sign in to comment.