Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
 into 47-improve-inspected-point-visualization
  • Loading branch information
franmagn committed Nov 5, 2025
2 parents 4ada576 + 0016d6d commit e382e64
Show file tree
Hide file tree
Showing 10 changed files with 421 additions and 93 deletions.
3 changes: 2 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
href="/src/MeasurementControl/measurementsPanel.css"
/>
<link rel="stylesheet" href="/src/AnnotationControl/annotationPanel.css" />
<link rel="stylesheet" href="src/AcceptedFiltering/threePanels.css" />
<link rel="stylesheet" href="src/Filter/filter.css" />
<link rel="stylesheet" href="src/2DProfileOverride/2DProfileOverride.css" />
</head>

<body>
Expand Down
12 changes: 12 additions & 0 deletions src/2DProfileOverride/2DProfileOverride.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* Keep profile SVG content (axes/labels) from being clipped */
#profileSVG {
overflow: visible !important;
}

/* Improve axis label readability against varying backgrounds */
#profile_window .axis text {
paint-order: stroke;
stroke: #000;
stroke-width: 3px;
stroke-linejoin: round;
}
224 changes: 224 additions & 0 deletions src/2DProfileOverride/2DProfileOverride.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { ecef, wgs84 } from '../config.js'

/**
* Initialize runtime overrides for Potree 2D profile behavior.
* - Patches ProfileWindow.addPoints: converts each point's Z to elevation instead.
* - Rewrites selection info table to show lon/lat/elevation.
*/
export function init2DProfileOverride(viewer) {
const tryPatchAddPoints = (target) => {
if (!target || target.__elevationPatchApplied) return false
const originalAddPoints = target.addPoints
if (typeof originalAddPoints !== 'function') return false

target.addPoints = function patchedAddPoints(pointcloud, points) {
try {
if (!points || !points.data || !points.data.position) {
return originalAddPoints.call(this, pointcloud, points)
}

const srcPos = points.data.position
// Divide by 3 because positions are [x0,y0,z0, x1,y1,z1, ...]
const count = points.numPoints || Math.floor(srcPos.length / 3)
const posArray = srcPos.constructor || Float32Array

// Clone without changing original buffers
const cloned = {
...points,
data: { ...points.data, position: new posArray(srcPos) }
}
const dstPos = cloned.data.position

const pcx = pointcloud?.position?.x || 0
const pcy = pointcloud?.position?.y || 0
const pcz = pointcloud?.position?.z || 0

// Preserve world ECEF Z per point (best-effort). Some Potree UIs attach
// selectedPoint from these attributes; we keep it on the cloned structure
// for later retrieval in the selection panel override.
const ecefZWorld = new Float64Array(count)

for (let i = 0; i < count; i++) {
const ix = 3 * i
const x = srcPos[ix + 0] + pcx
const y = srcPos[ix + 1] + pcy
const z = srcPos[ix + 2] + pcz

const [, , elevation] = proj4(ecef, wgs84, [x, y, z])
// Internally, Potree adds pointcloud.position.z back later.
dstPos[ix + 2] = elevation - pcz

ecefZWorld[i] = z
}

cloned.data.ecefZWorld = ecefZWorld

const result = originalAddPoints.call(this, pointcloud, cloned)

// Try to tag currently selectedPoint with the original ECEF Z if available
try {
if (
this &&
this.selectedPoint &&
Number.isFinite(this.selectedPoint.index)
) {
const idx = this.selectedPoint.index
if (ecefZWorld && idx >= 0 && idx < ecefZWorld.length) {
this.selectedPoint.ecefZWorld = ecefZWorld[idx]
}
}
} catch {}

return result
} catch (err) {
console.warn(
'2DProfileOverride: failed to apply elevation override',
err
)
return originalAddPoints.call(this, pointcloud, points)
}
}

target.__elevationPatchApplied = true
return true
}

let patched = false
if (viewer && viewer.profileWindow) {
patched = tryPatchAddPoints(viewer.profileWindow)
}
if (window.Potree?.ProfileWindow?.prototype) {
patched =
tryPatchAddPoints(window.Potree.ProfileWindow.prototype) || patched
}

const afterPatched = () => {
attachSelectionInfoOverride(viewer)
}

if (patched) {
afterPatched()
} else {
// Poll until the profile window/prototype is available
let tries = 0
const maxTries = 40
const timer = setInterval(() => {
tries += 1
if (
tryPatchAddPoints(window.Potree?.ProfileWindow?.prototype) ||
(viewer?.profileWindow && tryPatchAddPoints(viewer.profileWindow))
) {
clearInterval(timer)
afterPatched()
} else if (tries >= maxTries) {
clearInterval(timer)
}
}, 250)
}
}

// Rewrites the selection properties table inside the profile window
function attachSelectionInfoOverride(viewer) {
const getInfoEl = () => document.getElementById('profileSelectionProperties')
let infoEl = getInfoEl()
if (!infoEl) {
const obs = new MutationObserver(() => {
infoEl = getInfoEl()
if (infoEl) {
obs.disconnect()
monitorSelectionInfo(viewer, infoEl)
}
})
obs.observe(document.body, { childList: true, subtree: true })
} else {
monitorSelectionInfo(viewer, infoEl)
}
}

function monitorSelectionInfo(viewer, infoEl) {
let scheduled = false
let selfUpdating = false

const updateOnce = () => {
scheduled = false
selfUpdating = true
try {
const table = infoEl.querySelector('table')
if (!table) return
const rows = [...table.querySelectorAll('tr')]
if (rows.length < 3) return

const pw = viewer?.profileWindow
const pos = pw?.viewerPickSphere?.position
const sp = pw?.selectedPoint
if (!pos || !sp) return

// Use preserved ECEF Z if we have it; otherwise fall back to pos.z
const trueZ = Number(sp?.ecefZWorld ?? NaN)
let lon, lat
if (Number.isFinite(trueZ)) {
;[lon, lat] = proj4(ecef, wgs84, [pos.x, pos.y, trueZ])
} else {
;[lon, lat] = proj4(ecef, wgs84, [pos.x, pos.y, pos.z])
}
const elevation = pos.z

const setRow = (row, label, val) => {
const tds = row.querySelectorAll('td')
if (tds[0] && tds[0].textContent !== label) tds[0].textContent = label
if (tds[1]) {
const txt = Number.isFinite(val)
? val.toLocaleString(undefined, {
minimumFractionDigits: 4,
maximumFractionDigits: 4
})
: ''
if (tds[1].textContent !== txt) tds[1].textContent = txt
}
}

setRow(rows[0], 'lon', lon)
setRow(rows[1], 'lat', lat)
setRow(rows[2], 'elevation', elevation)

// Remove unwanted rows from the hover info table
const labelsToHide = new Set([
'intensity',
'return number',
'number of returns',
'classification flags',
'classification',
'user data',
'scan angle',
'gps time',
'gps-time',
'rgba'
])

// iterate over a snapshot to avoid issues while removing
const allRows = [...table.querySelectorAll('tr')]
for (let i = 3; i < allRows.length; i++) {
const row = allRows[i]
const labelCell = row.querySelector('td')
const label = (labelCell?.textContent || '').trim().toLowerCase()
if (labelsToHide.has(label)) {
row.remove()
}
}
} finally {
setTimeout(() => {
selfUpdating = false
}, 0)
}
}

const mo = new MutationObserver(() => {
if (selfUpdating) return
if (!scheduled) {
scheduled = true
requestAnimationFrame(updateOnce)
}
})
mo.observe(infoEl, { childList: true, subtree: true })
requestAnimationFrame(updateOnce)
}
87 changes: 57 additions & 30 deletions src/AnnotationControl/annotationPanel.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ img.button-icon[src$='/annotation.svg'] {
padding: 8px;
border-radius: 4px;
border: 1px solid #404a50;
background: #2f383d;
color: #cfd5d8;
background: #636262;
color: #636262;
font-family: inherit;
font-size: 12px;
line-height: 1.3;
Expand All @@ -44,10 +44,6 @@ img.button-icon[src$='/annotation.svg'] {
margin-top: 6px;
}

.annotation-add-button {
margin: 10px 0;
}

.annotation-empty {
opacity: 0.6;
padding: 10px;
Expand Down Expand Up @@ -350,37 +346,68 @@ img.button-icon[src$='/annotation.svg'] {
flex: 0 0 18px;
}

.pv-menu-list_annotations-panel {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
}

/* Add button */
.annotation-add-button {
background: linear-gradient(180deg, #f6f6f6 0%, #e9e9e9 100%);
color: #222;
padding: 8px 16px;
min-width: 140px;
height: 38px;
display: block;
margin: 12px auto;
border-radius: 6px;
width: 80%;
margin: 20px 0 10px;
padding: 10px 10px;
font-size: 13px;
font-weight: 700;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6) inset;
border: 1px solid #cfcfcf;
font-weight: 500;
background-color: #636262;
color: #ffffff;
border: 1px solid #555;
border-radius: 4px;
cursor: pointer;
text-align: center;
}
.annotation-add-button .add-label {
color: #222;
font-weight: 700;
transition:
background-color 0.2s ease,
transform 0.1s ease;
}

.annotation-add-button:hover {
background: linear-gradient(180deg, #f3f3f3 0%, #e2e2e2 100%);
border-color: #bfbfbf;
background-color: #8f8f8f;
}
.annotation-add-button:active {
transform: translateY(1px);
background: linear-gradient(180deg, #e9e9e9 0%, #dbdbdb 100%);
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.06);

#labelToggleContainer {
margin: 8px 0 6px;
padding-left: 4px;
}
.annotation-add-button:focus {
outline: 2px solid rgba(100, 100, 100, 0.12);
outline-offset: 2px;
#labelToggleContainer .labels-legend {
font-size: 13px;
color: #ddd;
margin-bottom: 4px;
}

.toggle-group {
display: flex;
width: 265px;
border: 1px solid black;
border-radius: 4px;
overflow: hidden;
}
.toggle-group button {
flex: 1;
padding: 6px 15px;
background: #a7a9aa;
color: #3d3c3c;
border: 0;
cursor: pointer;
font-weight: 300;
transition: background 0.2s;
}

.toggle-group button:not(:last-child) {
border-right: 1px solid #555;
}

.toggle-group button.active {
background: #c7c9ca;
color: #000;
}
Loading

0 comments on commit e382e64

Please sign in to comment.