Skip to content

Commit

Permalink
feat(#5): ✨ Add menu sections for measurements
Browse files Browse the repository at this point in the history
This new menu section will display measurements of points and distances, and display extra attributes
  • Loading branch information
gautegf committed Sep 24, 2025
1 parent 7ee1333 commit f5ce5f5
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 2 deletions.
181 changes: 179 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,176 @@
type="text/css"
href="/libs/jstree/themes/mixed/style.css"
/>

<!-- Custom styles for measurements -->
<style>
.measurement-item {
background-color: var(--bg-dark-color);
border: 1px solid #4a4a4a;
border-radius: 6px;
margin: 8px 0;
padding: 0;
color: var(--font-color);
position: relative;
}

.measurement-header {
background-color: var(--bg-color-2);
padding: 8px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #4a4a4a;
border-radius: 6px 6px 0 0;
}

.measurement-name {
font-weight: bold;
font-size: 14px;
flex-grow: 1;
margin-right: 8px;
color: var(--font-color-2);
}

.measurement-toggle,
.measurement-delete {
background: transparent;
border: 1px solid #666;
border-radius: 3px;
color: #ff6b6b;
cursor: pointer;
font-size: 14px;
margin-left: 4px;
padding: 3px 6px;
transition: background-color 0.2s;
font-weight: bold;
}

.measurement-toggle {
color: var(--font-color);
}

.measurement-toggle:hover,
.measurement-delete:hover {
background-color: rgba(255, 255, 255, 0.1);
}

.measurement-content {
padding: 12px;
}

.measurement-coordinate {
background-color: var(--color-1);
border: 1px solid #666;
border-radius: 4px;
padding: 8px 12px;
margin: 4px 0;
font-family: monospace;
font-size: 13px;
text-align: center;
color: var(--font-color-2);
}

.measurement-value {
background-color: var(--color-1);
border: 1px solid #666;
border-radius: 4px;
padding: 6px 12px;
margin: 4px 0;
font-family: monospace;
font-size: 14px;
text-align: center;
color: var(--font-color-2);
font-weight: bold;
}

.measurement-details-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 0;
font-size: 13px;
}

.measurement-label {
color: var(--font-color);
font-weight: normal;
}

.measurement-data {
color: var(--font-color-2);
font-family: monospace;
}

.no-measurements {
text-align: center;
color: var(--font-color);
opacity: 0.6;
font-style: italic;
padding: 20px;
}

#measurements_clear_all {
background-color: var(--bg-color-2);
border: 1px solid black;
color: var(--font-color-2);
cursor: pointer;
padding: 6px 12px;
border-radius: 4px;
width: 100%;
font-size: 90%;
font-weight: bold;
text-shadow: 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000;
box-shadow: 0px 2px 2px #111;
transition: filter 0.2s;
}

#measurements_clear_all:hover {
filter: brightness(125%);
}

#measurements_clear_all:active {
box-shadow: inset 0px 2px 2px #111;
}

#measurements_list {
max-height: 400px;
overflow-y: auto;
margin: 10px 0;
}

/* Scrollbar styling for measurements list */
#measurements_list::-webkit-scrollbar {
width: 8px;
}

#measurements_list::-webkit-scrollbar-track {
background: var(--bg-dark-color);
}

#measurements_list::-webkit-scrollbar-thumb {
background: var(--bg-color-2);
border-radius: 4px;
}

#measurements_list::-webkit-scrollbar-thumb:hover {
background: var(--color-1);
}

.measurement-status-indicator {
position: absolute;
top: 8px;
left: 8px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #4CAF50;
}

.measurement-status-indicator.hidden {
background-color: #ff6b6b;
}
</style>
</head>

<body>
Expand Down Expand Up @@ -53,7 +223,9 @@
<div id="potree_sidebar_container"></div>
</div>

<link rel="stylesheet" href="/src/measurementsPanel.css" />
<script type="module">
import { initMeasurementsPanel } from './src/measurementsPanel.js';
window.viewer = new Potree.Viewer(
document.getElementById('potree_render_area')
)
Expand All @@ -70,12 +242,17 @@
$('#menu_appearance').next().show()
$('#menu_tools').next().show()
$('#menu_scene').next().show()
$('#menu_measurements').next().show()
$('#menu_filters').next().show()
viewer.toggleSidebar()

// Initialize custom measurements panel
initMeasurementsPanel(viewer)
})

let url = './pointclouds/data_converted/metadata.json'
Potree.loadPointCloud(url).then((e) => {

Potree.loadPointCloud(url).then(e => {
let pointcloud = e.pointcloud
let material = pointcloud.material

Expand All @@ -86,7 +263,7 @@

viewer.scene.addPointCloud(pointcloud)
viewer.fitToScreen()
})
})
</script>
</body>
</html>
11 changes: 11 additions & 0 deletions public/build/potree/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ <h3 id="menu_scene"><span data-i18n="tb.scene_opt"></span></h3>
<div id="scene_object_properties"></div>
</div>

<!-- MEASUREMENTS -->
<h3 id="menu_measurements"><span data-i18n="tb.measurements_opt">Measurements</span></h3>
<div class="pv-menu-list">

<div class="divider"><span>Point Measurements</span></div>

<div id="measurements_list">
<!-- Point measurements will be dynamically populated here -->
</div>
</div>

<!-- Classification -->
<h3 id="menu_filters"><span data-i18n="tb.filters_opt"></span></h3>
<div>
Expand Down
10 changes: 10 additions & 0 deletions src/measurementsPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#measurements_list .mcard{background:#2d373c;border:1px solid #1c2428;border-radius:4px;margin:6px 4px;padding:6px 8px;font-family:inherit;font-size:12px;color:#eee;}
#measurements_list .mheader{display:flex;align-items:center;margin-bottom:4px;font-weight:bold;}
#measurements_list .mstatus{width:8px;height:8px;border-radius:50%;background:#68d96e;margin-right:6px;}
#measurements_list .mdelete{margin-left:auto;cursor:pointer;color:#d55;font-weight:bold;background:transparent;border:none;font-size:14px;}
#measurements_list table{width:100%;border-collapse:collapse;margin-top:4px;}
#measurements_list td{padding:2px 4px;}
#measurements_list tr:nth-child(even){background:#3a454b;}
#measurements_list .valueRow td{font-weight:bold;text-align:center;background:#4a555b;}
#measurements_list .attrRow td{font-size:11px;color:#cfd5d8;}
#measurements_list .empty{opacity:.6;padding:8px;text-align:center;}
126 changes: 126 additions & 0 deletions src/measurementsPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
export function initMeasurementsPanel(viewer){
const container = document.getElementById('measurements_list');
if(!container){
console.warn('Measurements list container not found');
return;
}

const state = new Map();
let needsRender = true;

function formatNumber(n, p=2){
if(n===undefined||n===null||isNaN(n)) return '-';
return Number(n).toFixed(p);
}

function distance(a,b){
const A = a.position || a; const B = b.position || b;
return Math.sqrt((A.x-B.x)**2 + (A.y-B.y)**2 + (A.z-B.z)**2);
}

function inferType(m){
if(m.points && m.points.length===1) return 'potCoordinate';
if(m.points && m.points.length>1) return 'potDistance';
return 'Measurement';
}

function addMeasurement(m){
state.set(m.uuid, {measurement: m, dirty:true});
needsRender = true;
}

function removeMeasurement(m){
state.delete(m.uuid);
needsRender = true;
}

function markDirty(m){
const entry = state.get(m.uuid);
if(entry){ entry.dirty = true; needsRender = true; }
}

function collectAttributes(point){
if(!point) return [];
const attrs = [];
const accepted = point.accepted ?? point.ACCEPTED ?? point.Accepted;
const tvu = point.TVU ?? point.tvu;
const thu = point.THU ?? point.thu;
if(accepted !== undefined) attrs.push(['accepted', accepted]);
if(tvu !== undefined) attrs.push(['TVU', tvu]);
if(thu !== undefined) attrs.push(['THU', thu]);
return attrs;
}

function render(){
if(!needsRender) return;
needsRender = false;
container.innerHTML = '';
if(state.size===0){
container.innerHTML = '<div class="empty">No measurements</div>';
return;
}

for(const {measurement:m} of state.values()){
const points = m.points || [];
const card = document.createElement('div');
card.className='mcard';

let body = '<table>';
for(const pt of points){
const vec = pt.position || pt;
body += `<tr><td colspan="3">${formatNumber(vec.x)}, ${formatNumber(vec.y)}, ${formatNumber(vec.z)}</td></tr>`;
const attrs = collectAttributes(pt);
for(const [k,v] of attrs){
body += `<tr class="attrRow"><td colspan="3">${k}: ${v}</td></tr>`;
}
}
if(points.length>1){
let total=0;
for(let i=0;i<points.length-1;i++){
const seg = distance(points[i], points[i+1]);
total+=seg;
body += `<tr class="valueRow"><td colspan="3">${formatNumber(seg)}</td></tr>`;
}
body += `<tr class="valueRow"><td colspan="3">${formatNumber(total)}</td></tr>`;
}
body += '</table>';
card.innerHTML = `
<div class="mheader"><div class="mstatus"></div><span>${m.name || inferType(m)}</span><button class="mdelete" data-uuid="${m.uuid}" title="Delete">×</button></div>
${body}`;
container.appendChild(card);
}
}

viewer.scene.addEventListener('measurement_added', e=> addMeasurement(e.measurement));
viewer.scene.addEventListener('measurement_removed', e=> removeMeasurement(e.measurement));

viewer.addEventListener('update', ()=>{
for(const entry of state.values()){
const m = entry.measurement;
if(!m.points) continue;
// If any point still at (0,0,0) while others not, mark dirty; or if distances change
if(m.points.some(pt => (pt.position||pt).x!==0 || (pt.position||pt).y!==0 || (pt.position||pt).z!==0)){
if(entry.dirty){
} else if(Math.random()<0.02){ // light periodic refresh safety
entry.dirty=true;
}
}
}
if([...state.values()].some(e=>e.dirty)){
for(const e of state.values()) e.dirty=false;
needsRender=true;
}
render();
});

// Initial existing measurements
for(const m of viewer.scene.measurements){ addMeasurement(m); }
render();

container.addEventListener('click', e=>{
const btn = e.target.closest('button.mdelete');
if(!btn) return;
const m = state.get(btn.getAttribute('data-uuid'))?.measurement;
if(m){ viewer.scene.removeMeasurement(m); }
});
}

0 comments on commit f5ce5f5

Please sign in to comment.