From 5f45df7b9f30f28d00057d9f0c1b6651abc46adb Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Sat, 20 Sep 2025 15:52:47 +0200 Subject: [PATCH 01/46] feat(#9): add cesium globe --- index.html | 165 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 152 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index c77bfb1..b2a119c 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,11 @@ type="text/css" href="/libs/jstree/themes/mixed/style.css" /> + @@ -39,6 +44,7 @@ +
+ > +
+
From 305ec8b468b492584b8d940134bd980c53aa5e42 Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Sat, 20 Sep 2025 17:12:27 +0200 Subject: [PATCH 02/46] refactor(#9): restructure index.html for cleaner project setup --- index.html | 160 +------------------------------------------- src/cameraSync.js | 58 ++++++++++++++++ src/cesiumViewer.js | 32 +++++++++ src/config.js | 16 +++++ src/main.js | 24 ++++++- src/potreeViewer.js | 41 ++++++++++++ 6 files changed, 172 insertions(+), 159 deletions(-) create mode 100644 src/cameraSync.js create mode 100644 src/cesiumViewer.js create mode 100644 src/config.js create mode 100644 src/potreeViewer.js diff --git a/index.html b/index.html index b2a119c..5c5e49a 100644 --- a/index.html +++ b/index.html @@ -69,163 +69,7 @@
- + + diff --git a/src/cameraSync.js b/src/cameraSync.js new file mode 100644 index 0000000..b2b9d9a --- /dev/null +++ b/src/cameraSync.js @@ -0,0 +1,58 @@ +export function syncCameras(potreeViewer, cesiumViewer) { + let camera = potreeViewer.scene.getActiveCamera() + + let pPos = new THREE.Vector3(0, 0, 0).applyMatrix4( + camera.matrixWorld + ) + let pUp = new THREE.Vector3(0, 600, 0).applyMatrix4( + camera.matrixWorld + ) + let pTarget = potreeViewer.scene.view.getPivot() + + let toCes = (pos) => { + let xy = [pos.x, pos.y] + let height = pos.z + let deg = toMap.forward(xy) + let cPos = Cesium.Cartesian3.fromDegrees(...deg, height) + + return cPos + } + + let cPos = toCes(pPos) + let cUpTarget = toCes(pUp) + let cTarget = toCes(pTarget) + + let cDir = Cesium.Cartesian3.subtract( + cTarget, + cPos, + new Cesium.Cartesian3() + ) + let cUp = Cesium.Cartesian3.subtract( + cUpTarget, + cPos, + new Cesium.Cartesian3() + ) + + cDir = Cesium.Cartesian3.normalize(cDir, new Cesium.Cartesian3()) + cUp = Cesium.Cartesian3.normalize(cUp, new Cesium.Cartesian3()) + + cesiumViewer.camera.setView({ + destination: cPos, + orientation: { + direction: cDir, + up: cUp + } + }) + + let aspect = potreeViewer.scene.getActiveCamera().aspect + if (aspect < 1) { + let fovy = + Math.PI * (potreeViewer.scene.getActiveCamera().fov / 180) + cesiumViewer.camera.frustum.fov = fovy + } else { + let fovy = + Math.PI * (potreeViewer.scene.getActiveCamera().fov / 180) + let fovx = Math.atan(Math.tan(0.5 * fovy) * aspect) * 2 + cesiumViewer.camera.frustum.fov = fovx + } +} \ No newline at end of file diff --git a/src/cesiumViewer.js b/src/cesiumViewer.js new file mode 100644 index 0000000..2b49481 --- /dev/null +++ b/src/cesiumViewer.js @@ -0,0 +1,32 @@ +export function createCesiumViewer(containerId) { + const viewer = new Cesium.Viewer(containerId, { + useDefaultRenderLoop: false, + animation: false, + baseLayerPicker: false, + fullscreenButton: false, + geocoder: false, + homeButton: false, + infoBox: false, + sceneModePicker: false, + selectionIndicator: false, + timeline: false, + navigationHelpButton: false, + imageryProvider: Cesium.createOpenStreetMapImageryProvider({ + url: 'https://a.tile.openstreetmap.org/' + }), + terrainShadows: Cesium.ShadowMode.DISABLED + }) + return viewer; +} + +export function setCesiumView(viewer, pos) { + const cp = new Cesium.Cartesian3(pos.x, pos.y, pos.z); + viewer.camera.setView({ + destination: cp, + orientation: { + heading: pos.heading, + pitch: pos.pitch, + roll: pos.roll + } + }); +} \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..ca47cff --- /dev/null +++ b/src/config.js @@ -0,0 +1,16 @@ +export const POTREE_POINTCLOUD_URL = "../pointclouds/data_converted/metadata.json"; + +export const INITIAL_CESIUM_POS = { + x: 4303414.154026048, + y: 552161.235598733, + z: 4660771.704035539, + heading: 10, + pitch: -Cesium.Math.PI_OVER_TWO * 0.5, + roll: 0.0 +}; + +export const POTREE_SETTINGS = { + edl: true, + fov: 60, + pointBudget: 1_000_000 +}; \ No newline at end of file diff --git a/src/main.js b/src/main.js index 79eee5f..6c918a2 100644 --- a/src/main.js +++ b/src/main.js @@ -1 +1,23 @@ -/* Empty for now, add logic later */ +import { POTREE_POINTCLOUD_URL, INITIAL_CESIUM_POS, POTREE_SETTINGS } from './config.js'; +import { createCesiumViewer, setCesiumView } from './cesiumViewer.js'; +import { createPotreeViewer } from './potreeViewer.js'; +import { syncCameras } from './cameraSync.js'; + +async function init() { + window.cesiumViewer = createCesiumViewer('cesiumContainer'); + setCesiumView(window.cesiumViewer, INITIAL_CESIUM_POS); + + window.potreeViewer = await createPotreeViewer('potree_render_area', POTREE_POINTCLOUD_URL, POTREE_SETTINGS); + + function loop(timestamp) { + requestAnimationFrame(loop); + potreeViewer.update(potreeViewer.clock.getDelta(), timestamp); + potreeViewer.render(); + if(window.toMap) syncCameras(potreeViewer, cesiumViewer); + cesiumViewer.render(); + } + + requestAnimationFrame(loop); +} + +init(); \ No newline at end of file diff --git a/src/potreeViewer.js b/src/potreeViewer.js new file mode 100644 index 0000000..751e8df --- /dev/null +++ b/src/potreeViewer.js @@ -0,0 +1,41 @@ +export async function createPotreeViewer(containerId, pointcloudUrl, settings) { + const viewer = new Potree.Viewer(document.getElementById(containerId), { + useDefaultRenderLoop: false + }); + + if(settings.edl) viewer.setEDLEnabled(true); + if(settings.fov) viewer.setFOV(settings.fov); + if(settings.pointBudget) viewer.setPointBudget(settings.pointBudget); + + viewer.loadSettingsFromURL(); + viewer.setBackground(null); + viewer.setDescription('Molloy Explorer') + + viewer.loadGUI(() => { + viewer.setLanguage('en') + $('#menu_appearance').next().show() + $('#menu_tools').next().show() + $('#menu_scene').next().show() + $('#menu_filters').next().show() + viewer.toggleSidebar() + }) + + const e = await Potree.loadPointCloud(pointcloudUrl); + const pc = e.pointcloud; + viewer.scene.addPointCloud(pc); + + pc.material.pointSizeType = Potree.PointSizeType.ADAPTIVE; + pc.material.shape = Potree.PointShape.CIRCLE + pc.material.activeAttributeName = "elevation"; + pc.material.gradient = Potree.Gradients["RAINBOW"]; + + e.pointcloud.projection = "+proj=utm +zone=32 +ellps=GRS80 +datum=ETRS89 +units=m +no_defs"; + const pointcloudProjection = e.pointcloud.projection; + const mapProjection = proj4.defs("WGS84"); + window.toMap = proj4(pointcloudProjection, mapProjection); + window.toScene = proj4(mapProjection, pointcloudProjection); + + viewer.scene.view.setView([401603.85, 8860821.54, 1000000], [401603.85, 8860821.54, -2650.47]); + + return viewer; +} \ No newline at end of file From bd4c0cc3c5406cf237baa9b0b452cc0d0d764b8d Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Sat, 20 Sep 2025 17:34:40 +0200 Subject: [PATCH 03/46] style(#9): clean up and format code --- src/cameraSync.js | 98 +++++++++++++++++++-------------------------- src/cesiumViewer.js | 56 +++++++++++++------------- src/config.js | 25 ++++++------ src/main.js | 40 ++++++++++-------- src/potreeViewer.js | 84 ++++++++++++++++++++------------------ 5 files changed, 151 insertions(+), 152 deletions(-) diff --git a/src/cameraSync.js b/src/cameraSync.js index b2b9d9a..c2ca8b6 100644 --- a/src/cameraSync.js +++ b/src/cameraSync.js @@ -1,58 +1,44 @@ export function syncCameras(potreeViewer, cesiumViewer) { - let camera = potreeViewer.scene.getActiveCamera() - - let pPos = new THREE.Vector3(0, 0, 0).applyMatrix4( - camera.matrixWorld - ) - let pUp = new THREE.Vector3(0, 600, 0).applyMatrix4( - camera.matrixWorld - ) - let pTarget = potreeViewer.scene.view.getPivot() - - let toCes = (pos) => { - let xy = [pos.x, pos.y] - let height = pos.z - let deg = toMap.forward(xy) - let cPos = Cesium.Cartesian3.fromDegrees(...deg, height) - - return cPos - } - - let cPos = toCes(pPos) - let cUpTarget = toCes(pUp) - let cTarget = toCes(pTarget) - - let cDir = Cesium.Cartesian3.subtract( - cTarget, - cPos, - new Cesium.Cartesian3() - ) - let cUp = Cesium.Cartesian3.subtract( - cUpTarget, - cPos, - new Cesium.Cartesian3() - ) - - cDir = Cesium.Cartesian3.normalize(cDir, new Cesium.Cartesian3()) - cUp = Cesium.Cartesian3.normalize(cUp, new Cesium.Cartesian3()) - - cesiumViewer.camera.setView({ - destination: cPos, - orientation: { - direction: cDir, - up: cUp - } - }) - - let aspect = potreeViewer.scene.getActiveCamera().aspect - if (aspect < 1) { - let fovy = - Math.PI * (potreeViewer.scene.getActiveCamera().fov / 180) - cesiumViewer.camera.frustum.fov = fovy - } else { - let fovy = - Math.PI * (potreeViewer.scene.getActiveCamera().fov / 180) - let fovx = Math.atan(Math.tan(0.5 * fovy) * aspect) * 2 - cesiumViewer.camera.frustum.fov = fovx + const camera = potreeViewer.scene.getActiveCamera() + + const pPos = new THREE.Vector3(0, 0, 0).applyMatrix4(camera.matrixWorld) + const pUp = new THREE.Vector3(0, 600, 0).applyMatrix4(camera.matrixWorld) + const pTarget = potreeViewer.scene.view.getPivot() + + const toCes = (pos) => { + const xy = [pos.x, pos.y] + const height = pos.z + const deg = toMap.forward(xy) + return Cesium.Cartesian3.fromDegrees(...deg, height) + } + + const cPos = toCes(pPos) + const cUpTarget = toCes(pUp) + const cTarget = toCes(pTarget) + + const cDir = Cesium.Cartesian3.normalize( + Cesium.Cartesian3.subtract(cTarget, cPos, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ) + const cUp = Cesium.Cartesian3.normalize( + Cesium.Cartesian3.subtract(cUpTarget, cPos, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ) + + cesiumViewer.camera.setView({ + destination: cPos, + orientation: { + direction: cDir, + up: cUp } -} \ No newline at end of file + }) + + const aspect = camera.aspect + const fovy = Math.PI * (camera.fov / 180) + if (aspect < 1) { + cesiumViewer.camera.frustum.fov = fovy + } else { + const fovx = Math.atan(Math.tan(0.5 * fovy) * aspect) * 2 + cesiumViewer.camera.frustum.fov = fovx + } +} diff --git a/src/cesiumViewer.js b/src/cesiumViewer.js index 2b49481..3fdbbbd 100644 --- a/src/cesiumViewer.js +++ b/src/cesiumViewer.js @@ -1,32 +1,32 @@ export function createCesiumViewer(containerId) { - const viewer = new Cesium.Viewer(containerId, { - useDefaultRenderLoop: false, - animation: false, - baseLayerPicker: false, - fullscreenButton: false, - geocoder: false, - homeButton: false, - infoBox: false, - sceneModePicker: false, - selectionIndicator: false, - timeline: false, - navigationHelpButton: false, - imageryProvider: Cesium.createOpenStreetMapImageryProvider({ - url: 'https://a.tile.openstreetmap.org/' - }), - terrainShadows: Cesium.ShadowMode.DISABLED - }) - return viewer; + const viewer = new Cesium.Viewer(containerId, { + useDefaultRenderLoop: false, + animation: false, + baseLayerPicker: false, + fullscreenButton: false, + geocoder: false, + homeButton: false, + infoBox: false, + sceneModePicker: false, + selectionIndicator: false, + timeline: false, + navigationHelpButton: false, + imageryProvider: Cesium.createOpenStreetMapImageryProvider({ + url: 'https://a.tile.openstreetmap.org/' + }), + terrainShadows: Cesium.ShadowMode.DISABLED + }) + return viewer } export function setCesiumView(viewer, pos) { - const cp = new Cesium.Cartesian3(pos.x, pos.y, pos.z); - viewer.camera.setView({ - destination: cp, - orientation: { - heading: pos.heading, - pitch: pos.pitch, - roll: pos.roll - } - }); -} \ No newline at end of file + const cp = new Cesium.Cartesian3(pos.x, pos.y, pos.z) + viewer.camera.setView({ + destination: cp, + orientation: { + heading: pos.heading, + pitch: pos.pitch, + roll: pos.roll + } + }) +} diff --git a/src/config.js b/src/config.js index ca47cff..8245b56 100644 --- a/src/config.js +++ b/src/config.js @@ -1,16 +1,17 @@ -export const POTREE_POINTCLOUD_URL = "../pointclouds/data_converted/metadata.json"; +export const POTREE_POINTCLOUD_URL = + '../pointclouds/data_converted/metadata.json' export const INITIAL_CESIUM_POS = { - x: 4303414.154026048, - y: 552161.235598733, - z: 4660771.704035539, - heading: 10, - pitch: -Cesium.Math.PI_OVER_TWO * 0.5, - roll: 0.0 -}; + x: 4303414.154026048, + y: 552161.235598733, + z: 4660771.704035539, + heading: 10, + pitch: -Cesium.Math.PI_PVER_FOUR, + roll: 0.0 +} export const POTREE_SETTINGS = { - edl: true, - fov: 60, - pointBudget: 1_000_000 -}; \ No newline at end of file + edl: true, + fov: 60, + pointBudget: 1_000_000 +} diff --git a/src/main.js b/src/main.js index 6c918a2..8ab5b84 100644 --- a/src/main.js +++ b/src/main.js @@ -1,23 +1,31 @@ -import { POTREE_POINTCLOUD_URL, INITIAL_CESIUM_POS, POTREE_SETTINGS } from './config.js'; -import { createCesiumViewer, setCesiumView } from './cesiumViewer.js'; -import { createPotreeViewer } from './potreeViewer.js'; -import { syncCameras } from './cameraSync.js'; +import { + POTREE_POINTCLOUD_URL, + INITIAL_CESIUM_POS, + POTREE_SETTINGS +} from './config.js' +import { createCesiumViewer, setCesiumView } from './cesiumViewer.js' +import { createPotreeViewer } from './potreeViewer.js' +import { syncCameras } from './cameraSync.js' async function init() { - window.cesiumViewer = createCesiumViewer('cesiumContainer'); - setCesiumView(window.cesiumViewer, INITIAL_CESIUM_POS); + window.cesiumViewer = createCesiumViewer('cesiumContainer') + setCesiumView(window.cesiumViewer, INITIAL_CESIUM_POS) - window.potreeViewer = await createPotreeViewer('potree_render_area', POTREE_POINTCLOUD_URL, POTREE_SETTINGS); + window.potreeViewer = await createPotreeViewer( + 'potree_render_area', + POTREE_POINTCLOUD_URL, + POTREE_SETTINGS + ) - function loop(timestamp) { - requestAnimationFrame(loop); - potreeViewer.update(potreeViewer.clock.getDelta(), timestamp); - potreeViewer.render(); - if(window.toMap) syncCameras(potreeViewer, cesiumViewer); - cesiumViewer.render(); - } + function loop(timestamp) { + requestAnimationFrame(loop) + potreeViewer.update(potreeViewer.clock.getDelta(), timestamp) + potreeViewer.render() + if (window.toMap) syncCameras(potreeViewer, cesiumViewer) + cesiumViewer.render() + } - requestAnimationFrame(loop); + requestAnimationFrame(loop) } -init(); \ No newline at end of file +init() diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 751e8df..2e7272e 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,41 +1,45 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { - const viewer = new Potree.Viewer(document.getElementById(containerId), { - useDefaultRenderLoop: false - }); - - if(settings.edl) viewer.setEDLEnabled(true); - if(settings.fov) viewer.setFOV(settings.fov); - if(settings.pointBudget) viewer.setPointBudget(settings.pointBudget); - - viewer.loadSettingsFromURL(); - viewer.setBackground(null); - viewer.setDescription('Molloy Explorer') - - viewer.loadGUI(() => { - viewer.setLanguage('en') - $('#menu_appearance').next().show() - $('#menu_tools').next().show() - $('#menu_scene').next().show() - $('#menu_filters').next().show() - viewer.toggleSidebar() - }) - - const e = await Potree.loadPointCloud(pointcloudUrl); - const pc = e.pointcloud; - viewer.scene.addPointCloud(pc); - - pc.material.pointSizeType = Potree.PointSizeType.ADAPTIVE; - pc.material.shape = Potree.PointShape.CIRCLE - pc.material.activeAttributeName = "elevation"; - pc.material.gradient = Potree.Gradients["RAINBOW"]; - - e.pointcloud.projection = "+proj=utm +zone=32 +ellps=GRS80 +datum=ETRS89 +units=m +no_defs"; - const pointcloudProjection = e.pointcloud.projection; - const mapProjection = proj4.defs("WGS84"); - window.toMap = proj4(pointcloudProjection, mapProjection); - window.toScene = proj4(mapProjection, pointcloudProjection); - - viewer.scene.view.setView([401603.85, 8860821.54, 1000000], [401603.85, 8860821.54, -2650.47]); - - return viewer; -} \ No newline at end of file + const viewer = new Potree.Viewer(document.getElementById(containerId), { + useDefaultRenderLoop: false + }) + + if (settings.edl) viewer.setEDLEnabled(true) + if (settings.fov) viewer.setFOV(settings.fov) + if (settings.pointBudget) viewer.setPointBudget(settings.pointBudget) + + viewer.loadSettingsFromURL() + viewer.setBackground(null) + viewer.setDescription('Molloy Explorer') + + viewer.loadGUI(() => { + viewer.setLanguage('en') + $('#menu_appearance').next().show() + $('#menu_tools').next().show() + $('#menu_scene').next().show() + $('#menu_filters').next().show() + viewer.toggleSidebar() + }) + + const e = await Potree.loadPointCloud(pointcloudUrl) + const pc = e.pointcloud + viewer.scene.addPointCloud(pc) + + pc.material.pointSizeType = Potree.PointSizeType.ADAPTIVE + pc.material.shape = Potree.PointShape.CIRCLE + pc.material.activeAttributeName = 'elevation' + pc.material.gradient = Potree.Gradients['RAINBOW'] + + e.pointcloud.projection = + '+proj=utm +zone=32 +ellps=GRS80 +datum=ETRS89 +units=m +no_defs' + const pointcloudProjection = e.pointcloud.projection + const mapProjection = proj4.defs('WGS84') + window.toMap = proj4(pointcloudProjection, mapProjection) + window.toScene = proj4(mapProjection, pointcloudProjection) + + viewer.scene.view.setView( + [401603.85, 8860821.54, 1000000], + [401603.85, 8860821.54, -2650.47] + ) + + return viewer +} From 7372c1305b064cecb86eb1b7e026f9393f877792 Mon Sep 17 00:00:00 2001 From: Kleinc Date: Mon, 22 Sep 2025 11:24:10 +0200 Subject: [PATCH 04/46] feat(#10): :sparkles: coordinates possible to see on scene --- index.html | 7 ++++++- src/main.js | 18 +++++++++++++++++- src/style.css | 11 ++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index c77bfb1..3370c31 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,7 @@ type="text/css" href="/libs/jstree/themes/mixed/style.css" /> + @@ -39,6 +40,7 @@ +
+ > + +
@@ -88,5 +92,6 @@ viewer.fitToScreen() }) + diff --git a/src/main.js b/src/main.js index 79eee5f..a3de3e7 100644 --- a/src/main.js +++ b/src/main.js @@ -1 +1,17 @@ -/* Empty for now, add logic later */ +// EPSG:32633 (WGS84 / UTM zone 33N) → WGS84 (lon/lat) +const utm33 = "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs"; //UTM zone 33N +const wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; //Current standard for GPS coordinates + +const posCanvas = document.getElementById('camera-pos'); +const context = posCanvas.getContext('2d'); + +function updateCameraOverlay() { + const cam = window.viewer.scene.view.position; + const [lon, lat] = proj4(utm33, wgs84, [cam.x, cam.y]); // Conversion using proj4js library + + context.clearRect(0, 0, posCanvas.width, posCanvas.height); + context.fillStyle = 'white'; + context.font = '20px Times New Roman'; + context.fillText(`lat=${lat.toFixed(2)}˚ lon=${lon.toFixed(2)}˚`, 10, 40); +} +viewer.addEventListener("update", updateCameraOverlay); diff --git a/src/style.css b/src/style.css index 5bc7676..9cdf607 100644 --- a/src/style.css +++ b/src/style.css @@ -1 +1,10 @@ -/* Empty for now, add styles later */ + +#camera-pos { + position: absolute; + left: 10px; + bottom: 10px; + width: 300px; + height: 60px; + pointer-events: none; + z-index: 10; +} \ No newline at end of file From f5ce5f5461e8430916f9a8fca26201a42bec20a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 24 Sep 2025 12:53:27 +0200 Subject: [PATCH 05/46] feat(#5): :sparkles: Add menu sections for measurements This new menu section will display measurements of points and distances, and display extra attributes --- index.html | 181 ++++++++++++++++++++++++++++++- public/build/potree/sidebar.html | 11 ++ src/measurementsPanel.css | 10 ++ src/measurementsPanel.js | 126 +++++++++++++++++++++ 4 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 src/measurementsPanel.css create mode 100644 src/measurementsPanel.js diff --git a/index.html b/index.html index c77bfb1..3f8c243 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,176 @@ type="text/css" href="/libs/jstree/themes/mixed/style.css" /> + + + @@ -53,7 +223,9 @@
+ diff --git a/public/build/potree/sidebar.html b/public/build/potree/sidebar.html index 1552427..93b6ca7 100644 --- a/public/build/potree/sidebar.html +++ b/public/build/potree/sidebar.html @@ -116,6 +116,17 @@
+ + +
+ +
Point Measurements
+ +
+ +
+
+
diff --git a/src/measurementsPanel.css b/src/measurementsPanel.css new file mode 100644 index 0000000..bbc1863 --- /dev/null +++ b/src/measurementsPanel.css @@ -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;} diff --git a/src/measurementsPanel.js b/src/measurementsPanel.js new file mode 100644 index 0000000..5c76af7 --- /dev/null +++ b/src/measurementsPanel.js @@ -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 = '
No measurements
'; + return; + } + + for(const {measurement:m} of state.values()){ + const points = m.points || []; + const card = document.createElement('div'); + card.className='mcard'; + + let body = ''; + for(const pt of points){ + const vec = pt.position || pt; + body += ``; + const attrs = collectAttributes(pt); + for(const [k,v] of attrs){ + body += ``; + } + } + if(points.length>1){ + let total=0; + for(let i=0;i`; + } + body += ``; + } + body += '
${formatNumber(vec.x)}, ${formatNumber(vec.y)}, ${formatNumber(vec.z)}
${k}: ${v}
${formatNumber(seg)}
${formatNumber(total)}
'; + card.innerHTML = ` + + ${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); } + }); +} From 6f4ed05f5c6ae1fe6d629bfbcb279548fe7f10ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 24 Sep 2025 13:34:20 +0200 Subject: [PATCH 06/46] fix(#5): :bug: better display of distance measurement --- src/measurementsPanel.js | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/measurementsPanel.js b/src/measurementsPanel.js index 5c76af7..88a55ea 100644 --- a/src/measurementsPanel.js +++ b/src/measurementsPanel.js @@ -34,11 +34,6 @@ export function initMeasurementsPanel(viewer){ 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 = []; @@ -66,22 +61,28 @@ export function initMeasurementsPanel(viewer){ card.className='mcard'; let body = ''; - for(const pt of points){ - const vec = pt.position || pt; - body += ``; - const attrs = collectAttributes(pt); - for(const [k,v] of attrs){ - body += ``; - } - } - if(points.length>1){ + if(points.length>0){ let total=0; - for(let i=0;i`; + const isDistance = points.length>1; + for(let i=0;i`; + if(!isDistance){ + const attrs = collectAttributes(pt); + for(const [k,v] of attrs){ + body += ``; + } + } + if(i < points.length -1){ + const seg = distance(points[i], points[i+1]); + total += seg; + body += ``; + } + } + if(points.length>1){ + body += ``; } - body += ``; } body += '
${formatNumber(vec.x)}, ${formatNumber(vec.y)}, ${formatNumber(vec.z)}
${k}: ${v}
${formatNumber(seg)}
${formatNumber(vec.x)}, ${formatNumber(vec.y)}, ${formatNumber(vec.z)}
${k}: ${v}
${formatNumber(seg)} m
${formatNumber(total)} m
${formatNumber(total)}
'; card.innerHTML = ` From acd5f4b2164d34eb63e953cf3d123f3c651b7429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 24 Sep 2025 13:35:14 +0200 Subject: [PATCH 07/46] refactor(#5): :recycle: move styling from index file to measurements css file --- index.html | 169 -------------------------------------- src/measurementsPanel.css | 12 ++- 2 files changed, 11 insertions(+), 170 deletions(-) diff --git a/index.html b/index.html index 3f8c243..29196d7 100644 --- a/index.html +++ b/index.html @@ -24,175 +24,6 @@ href="/libs/jstree/themes/mixed/style.css" /> - - diff --git a/src/measurementsPanel.css b/src/measurementsPanel.css index bbc1863..af89261 100644 --- a/src/measurementsPanel.css +++ b/src/measurementsPanel.css @@ -5,6 +5,16 @@ #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 .coordRow td{background:#2f383d;font-family:monospace;font-size:11px;padding:4px 6px;border:1px solid #404a50;border-radius:4px;margin:2px 0;} +#measurements_list .segmentRow td{background:transparent;padding:0 0 2px 0;} +#measurements_list .segmentConnector{width:1px;height:12px;background:#505b61;margin:2px auto 0;} +#measurements_list .segmentPill{display:inline-block;background:#3d474d;border:1px solid #566067;padding:2px 10px;margin:2px auto 4px;border-radius:6px;font-size:11px;font-family:monospace;} +#measurements_list .totalRow td{background:#424d53;font-weight:600;font-size:12px;text-align:center;border:1px solid #59646a;border-radius:6px;margin-top:4px;} +#measurements_list .separator td{padding:0;height:2px;background:transparent;} #measurements_list .attrRow td{font-size:11px;color:#cfd5d8;} #measurements_list .empty{opacity:.6;padding:8px;text-align:center;} +#measurements_list{max-height:400px;overflow-y:auto;margin:10px 0;} +#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);} From 251b33390544419a128a4a35a5b000210a250ee0 Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Thu, 25 Sep 2025 19:31:03 +0200 Subject: [PATCH 08/46] feat(#9): limit zoom out --- src/potreeViewer.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 2e7272e..f27edc7 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -3,6 +3,11 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { useDefaultRenderLoop: false }) + // Remove original scroll listener and add new one + const oc = viewer.orbitControls; + oc.removeEventListener('mousewheel', oc._listeners?.mousewheel?.[0]); + oc.addEventListener('mousewheel', clampScrollRadius); + if (settings.edl) viewer.setEDLEnabled(true) if (settings.fov) viewer.setFOV(settings.fov) if (settings.pointBudget) viewer.setPointBudget(settings.pointBudget) @@ -43,3 +48,14 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { return viewer } + +function clampScrollRadius(e) { + let resolvedRadius = this.scene.view.radius + this.radiusDelta; + let newRadius = resolvedRadius - e.delta * resolvedRadius * 0.1; + + const maxRadius = 10000000; + if (newRadius > maxRadius) newRadius = maxRadius; + + this.radiusDelta = newRadius - this.scene.view.radius; + this.stopTweens(); +} From fefe11bc39c0b0e6e5768319caa697dd24545bf9 Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Fri, 26 Sep 2025 14:00:04 +0200 Subject: [PATCH 09/46] fix(#9): hide globe when camera is below surface --- src/cameraSync.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cameraSync.js b/src/cameraSync.js index c2ca8b6..39ca0fc 100644 --- a/src/cameraSync.js +++ b/src/cameraSync.js @@ -23,7 +23,12 @@ export function syncCameras(potreeViewer, cesiumViewer) { const cUp = Cesium.Cartesian3.normalize( Cesium.Cartesian3.subtract(cUpTarget, cPos, new Cesium.Cartesian3()), new Cesium.Cartesian3() - ) + ) + + // Hide globe when camera is below surface + const camHeight = Cesium.Cartographic.fromCartesian(cPos).height + cesiumViewer.scene.globe.show = camHeight >= 0; + cesiumViewer.scene.skyAtmosphere.show = camHeight >= 0; cesiumViewer.camera.setView({ destination: cPos, @@ -41,4 +46,4 @@ export function syncCameras(potreeViewer, cesiumViewer) { const fovx = Math.atan(Math.tan(0.5 * fovy) * aspect) * 2 cesiumViewer.camera.frustum.fov = fovx } -} +} \ No newline at end of file From 1b3a9d882bb1aed1b20481d3cad15bab481ca761 Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Sat, 27 Sep 2025 15:08:49 +0200 Subject: [PATCH 10/46] fix(#9): hide globe when pivot is blockd by curvature --- src/cameraSync.js | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/cameraSync.js b/src/cameraSync.js index 39ca0fc..046d5dd 100644 --- a/src/cameraSync.js +++ b/src/cameraSync.js @@ -25,10 +25,10 @@ export function syncCameras(potreeViewer, cesiumViewer) { new Cesium.Cartesian3() ) - // Hide globe when camera is below surface - const camHeight = Cesium.Cartographic.fromCartesian(cPos).height - cesiumViewer.scene.globe.show = camHeight >= 0; - cesiumViewer.scene.skyAtmosphere.show = camHeight >= 0; + // Hide globe when the camera is below the surface or blocked by the curvature of the Earth + const showGlobe = shouldShowGlobe(cPos, cTarget); + cesiumViewer.scene.globe.show = showGlobe; + cesiumViewer.scene.skyAtmosphere.show = showGlobe; cesiumViewer.camera.setView({ destination: cPos, @@ -46,4 +46,38 @@ export function syncCameras(potreeViewer, cesiumViewer) { const fovx = Math.atan(Math.tan(0.5 * fovy) * aspect) * 2 cesiumViewer.camera.frustum.fov = fovx } +} + +/** + * Determines whether the globe should be visible based on the camera position. + * + * Returns false if the camera is below the globe surface or if the pivot + * point would be blocked by the curvature of the Earth. This is handled + * in a unified way by projecting the pivot to the globe surface and + * comparing the camera and pivot positions along the axis from the Earth's + * center through the pivot. + * + * @param cameraPos - The camera position in Cesium Cartesian3 coordinates + * @param pivot - The pivot point in the point cloud (Cartesian3) + * @returns true if the globe should be visible, false if it should be hidden + */ +function shouldShowGlobe(cameraPos, pivot) { + const ellipsoid = Cesium.Ellipsoid.WGS84; + const earthCenter = Cesium.Cartesian3.ZERO; + + // Get point on globe surface directly above the pivot + const carto = Cesium.Cartographic.fromCartesian(pivot); + const pivotSurface = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, 0, ellipsoid); + + // Axis vector from Earth center through pivot + const axis = Cesium.Cartesian3.subtract(pivotSurface, earthCenter, new Cesium.Cartesian3()); + Cesium.Cartesian3.normalize(axis, axis); + + // Project camera and pivot onto this axis + const camProj = Cesium.Cartesian3.dot(cameraPos, axis); + const pivotProj = Cesium.Cartesian3.dot(pivotSurface, axis); + + // If camera is "above" pivot on this axis, the globe should be visible + // If camera is "below" pivot on this axis, the globe should be not be visible + return camProj >= pivotProj; } \ No newline at end of file From 5767ca1078fbdb27533069f762c6b6e3bfbe5fa3 Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Sat, 27 Sep 2025 15:11:28 +0200 Subject: [PATCH 11/46] style(#9): prettier formatting --- src/cameraSync.js | 47 +++++++++++++++++++++++++++------------------ src/potreeViewer.js | 18 ++++++++--------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/cameraSync.js b/src/cameraSync.js index 046d5dd..173d9a0 100644 --- a/src/cameraSync.js +++ b/src/cameraSync.js @@ -23,12 +23,12 @@ export function syncCameras(potreeViewer, cesiumViewer) { const cUp = Cesium.Cartesian3.normalize( Cesium.Cartesian3.subtract(cUpTarget, cPos, new Cesium.Cartesian3()), new Cesium.Cartesian3() - ) + ) // Hide globe when the camera is below the surface or blocked by the curvature of the Earth - const showGlobe = shouldShowGlobe(cPos, cTarget); - cesiumViewer.scene.globe.show = showGlobe; - cesiumViewer.scene.skyAtmosphere.show = showGlobe; + const showGlobe = shouldShowGlobe(cPos, cTarget) + cesiumViewer.scene.globe.show = showGlobe + cesiumViewer.scene.skyAtmosphere.show = showGlobe cesiumViewer.camera.setView({ destination: cPos, @@ -52,32 +52,41 @@ export function syncCameras(potreeViewer, cesiumViewer) { * Determines whether the globe should be visible based on the camera position. * * Returns false if the camera is below the globe surface or if the pivot - * point would be blocked by the curvature of the Earth. This is handled - * in a unified way by projecting the pivot to the globe surface and - * comparing the camera and pivot positions along the axis from the Earth's + * point would be blocked by the curvature of the Earth. This is handled + * in a unified way by projecting the pivot to the globe surface and + * comparing the camera and pivot positions along the axis from the Earth's * center through the pivot. - * + * * @param cameraPos - The camera position in Cesium Cartesian3 coordinates * @param pivot - The pivot point in the point cloud (Cartesian3) * @returns true if the globe should be visible, false if it should be hidden */ function shouldShowGlobe(cameraPos, pivot) { - const ellipsoid = Cesium.Ellipsoid.WGS84; - const earthCenter = Cesium.Cartesian3.ZERO; - + const ellipsoid = Cesium.Ellipsoid.WGS84 + const earthCenter = Cesium.Cartesian3.ZERO + // Get point on globe surface directly above the pivot - const carto = Cesium.Cartographic.fromCartesian(pivot); - const pivotSurface = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, 0, ellipsoid); + const carto = Cesium.Cartographic.fromCartesian(pivot) + const pivotSurface = Cesium.Cartesian3.fromRadians( + carto.longitude, + carto.latitude, + 0, + ellipsoid + ) // Axis vector from Earth center through pivot - const axis = Cesium.Cartesian3.subtract(pivotSurface, earthCenter, new Cesium.Cartesian3()); - Cesium.Cartesian3.normalize(axis, axis); + const axis = Cesium.Cartesian3.subtract( + pivotSurface, + earthCenter, + new Cesium.Cartesian3() + ) + Cesium.Cartesian3.normalize(axis, axis) // Project camera and pivot onto this axis - const camProj = Cesium.Cartesian3.dot(cameraPos, axis); - const pivotProj = Cesium.Cartesian3.dot(pivotSurface, axis); + const camProj = Cesium.Cartesian3.dot(cameraPos, axis) + const pivotProj = Cesium.Cartesian3.dot(pivotSurface, axis) // If camera is "above" pivot on this axis, the globe should be visible // If camera is "below" pivot on this axis, the globe should be not be visible - return camProj >= pivotProj; -} \ No newline at end of file + return camProj >= pivotProj +} diff --git a/src/potreeViewer.js b/src/potreeViewer.js index f27edc7..58f1ab9 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -4,9 +4,9 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { }) // Remove original scroll listener and add new one - const oc = viewer.orbitControls; - oc.removeEventListener('mousewheel', oc._listeners?.mousewheel?.[0]); - oc.addEventListener('mousewheel', clampScrollRadius); + const oc = viewer.orbitControls + oc.removeEventListener('mousewheel', oc._listeners?.mousewheel?.[0]) + oc.addEventListener('mousewheel', clampScrollRadius) if (settings.edl) viewer.setEDLEnabled(true) if (settings.fov) viewer.setFOV(settings.fov) @@ -50,12 +50,12 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { } function clampScrollRadius(e) { - let resolvedRadius = this.scene.view.radius + this.radiusDelta; - let newRadius = resolvedRadius - e.delta * resolvedRadius * 0.1; + let resolvedRadius = this.scene.view.radius + this.radiusDelta + let newRadius = resolvedRadius - e.delta * resolvedRadius * 0.1 - const maxRadius = 10000000; - if (newRadius > maxRadius) newRadius = maxRadius; + const maxRadius = 10000000 + if (newRadius > maxRadius) newRadius = maxRadius - this.radiusDelta = newRadius - this.scene.view.radius; - this.stopTweens(); + this.radiusDelta = newRadius - this.scene.view.radius + this.stopTweens() } From 07a8effc48a686d525c46cd9d4ec79faa7bcf744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20Wahlstr=C3=B8m?= Date: Mon, 29 Sep 2025 10:00:19 +0200 Subject: [PATCH 12/46] feat(#7): :sparkles: Created "Evelation Control" section with belonging functionality --- index.html | 5 +- src/main.js | 138 +++++++++++++++++++++++++++++++++++++++++++++++++- src/style.css | 12 ++++- 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index c77bfb1..9190fa3 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,7 @@ type="text/css" href="/libs/jstree/themes/mixed/style.css" /> + @@ -67,6 +68,7 @@ viewer.loadGUI(() => { viewer.setLanguage('en') + $('#menu_customised').next().show() $('#menu_appearance').next().show() $('#menu_tools').next().show() $('#menu_scene').next().show() @@ -82,11 +84,12 @@ material.pointSizeType = Potree.PointSizeType.ADAPTIVE material.shape = Potree.PointShape.CIRCLE material.activeAttributeName = 'elevation' - material.gradient = Potree.Gradients['RAINBOW'] + material.gradient = Potree.Gradients['VIRIDIS'] viewer.scene.addPointCloud(pointcloud) viewer.fitToScreen() }) + diff --git a/src/main.js b/src/main.js index 79eee5f..578a794 100644 --- a/src/main.js +++ b/src/main.js @@ -1 +1,137 @@ -/* Empty for now, add logic later */ +/** + * Creating and inserting a new customised "Elevation Range" section in the sidebar above Apperance + * with the same css style features as Apperance +*/ +(function () { + + //Insert new the sidebar above Apperance + function insertCustomisedAboveAppearance() { + const appearanceHeader = document.querySelector('#menu_appearance'); + if (!appearanceHeader) return false; + if (document.querySelector('#menu_customised_header')) return true; + + //Clone the header for identical style to apperance + const customHeader = appearanceHeader.cloneNode(true); + customHeader.removeAttribute('id'); + customHeader.id = 'menu_customised_header'; + customHeader.textContent = 'Elevation Control'; + customHeader.style.cursor = 'pointer'; + customHeader.setAttribute('tabindex', '0'); + + //Creating a new "div" in the customised section similar to the Apperance div + const appearanceBody = appearanceHeader.nextElementSibling; + const bodyClass = appearanceBody ? appearanceBody.className : 'pv-menu-list'; + const customBody = document.createElement('div'); + customBody.className = bodyClass; + customBody.id = 'customised_list'; + customBody.style.display = ''; // start expanded + + //Insert both right before Appearance + appearanceHeader.parentElement.insertBefore(customHeader, appearanceHeader); + appearanceHeader.parentElement.insertBefore(customBody, appearanceHeader); + + //collapse/expand + const toggle = () => { + const hidden = customBody.style.display === 'none'; + customBody.style.display = hidden ? '' : 'none'; + }; + customHeader.addEventListener('click', toggle); + customHeader.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } + }); + + return true; + } + + function init() { + if (insertCustomisedAboveAppearance()) return; + let tries = 0; + const t = setInterval(() => { + tries++; + if (insertCustomisedAboveAppearance() || tries > 50) clearInterval(t); + }, 200); + } + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); + else init(); +})(); + + + +/** + * A function that: + * - Autoselects the first pointcloud + * - Move the Elevation block from the Scene section into the custimosed Elevation Control section + * + * Nice to know: In the oroginal code the Elevation section in the Properties panel is only built when the pointcloud is clicked on + */ +(function () { + const $ = (sel, root = document) => root.querySelector(sel); + + //Select the fist pointcloud in the sidebar so that the Elevation section is built + function autoSelectFirstPointCloud() { + const cloudIcon = document.querySelector('#scene_objects i.jstree-themeicon-custom'); + if (cloudIcon) { + cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true })); + return true; + } + return false; + } + + //(re)connect the elevation labels to the slider after the container is moved (was not handled by default) + function rebindElevationLabel() { + const slider = window.jQuery ? window.jQuery("#sldHeightRange") : null; + const label = document.getElementById("lblHeightRange"); + if (!slider || !slider.length || !label) return; + + const update = () => { + const low = $slider.slider("values", 0); + const high = $slider.slider("values", 1); + label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}`; + }; + + //clear any old namespaced handlers and attach fresh ones + slider.off("slide.custom slidestop.custom change.custom"); + slider.on("slide.custom", update); + slider.on("slidestop.custom change.custom", update); + update(); + } + + //Move the elevation range section to the customised "Elevation Control" section + function moveElevationContainer() { + const target = $('#customised_list'); + const elevationContainer = document.querySelector('#materials\\.elevation_container'); + if (!elevationContainer) return false; + target.appendChild(elevationContainer); + rebindElevationLabel(); + return true; + + } + + function init() { + let tries = 0; + const t = setInterval(() => { + const hasCloud = !!(window.viewer?.scene?.pointclouds?.length); + if (hasCloud) { + autoSelectFirstPointCloud(); + // Wait until potree builds the Properties, then move the container + let innerTries = 0; + const t2 = setInterval(() => { + const movedElevation = moveElevationContainer(); + if (movedElevation || ++innerTries > 100) clearInterval(t2); + }, 100); + + clearInterval(t); + } + if (++tries > 30) clearInterval(t); + }, 100); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); + + diff --git a/src/style.css b/src/style.css index 5bc7676..0a4215c 100644 --- a/src/style.css +++ b/src/style.css @@ -1 +1,11 @@ -/* Empty for now, add styles later */ +/* making sure that the divider span the whole sidebar (as the original code) */ +#customised_list > .divider { + margin: 0; + padding: 0; + } + +/* Padding of list items in the elevation control section as the original code*/ +#customised_list li { + list-style-type: none; + padding: 0 20px; + } \ No newline at end of file From 2f4ec32f64d4cc76a806afb9b007bf75f71ad52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20Wahlstr=C3=B8m?= Date: Mon, 29 Sep 2025 10:10:40 +0200 Subject: [PATCH 13/46] refactor(#7): :truck: Moved paths for evelation control in index.html --- index.html | 2 + src/elevation_control.css | 11 +++ src/elevation_control.js | 138 ++++++++++++++++++++++++++++++++++++++ src/main.js | 137 ------------------------------------- src/style.css | 11 --- 5 files changed, 151 insertions(+), 148 deletions(-) create mode 100644 src/elevation_control.css create mode 100644 src/elevation_control.js diff --git a/index.html b/index.html index 9190fa3..4c10df5 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,7 @@ href="/libs/jstree/themes/mixed/style.css" /> + @@ -91,5 +92,6 @@ }) + diff --git a/src/elevation_control.css b/src/elevation_control.css new file mode 100644 index 0000000..0a4215c --- /dev/null +++ b/src/elevation_control.css @@ -0,0 +1,11 @@ +/* making sure that the divider span the whole sidebar (as the original code) */ +#customised_list > .divider { + margin: 0; + padding: 0; + } + +/* Padding of list items in the elevation control section as the original code*/ +#customised_list li { + list-style-type: none; + padding: 0 20px; + } \ No newline at end of file diff --git a/src/elevation_control.js b/src/elevation_control.js new file mode 100644 index 0000000..e6a0f4e --- /dev/null +++ b/src/elevation_control.js @@ -0,0 +1,138 @@ +/** + * Creating and inserting a new customised "Elevation Range" section in the sidebar above Apperance + * with the same css style features as Apperance +*/ +(function () { + + //Insert new the sidebar above Apperance + function insertCustomisedAboveAppearance() { + const appearanceHeader = document.querySelector('#menu_appearance'); + if (!appearanceHeader) return false; + if (document.querySelector('#menu_customised_header')) return true; + + //Clone the header for identical style to apperance + const customHeader = appearanceHeader.cloneNode(true); + customHeader.removeAttribute('id'); + customHeader.id = 'menu_customised_header'; + customHeader.textContent = 'Elevation Control'; + customHeader.style.cursor = 'pointer'; + customHeader.setAttribute('tabindex', '0'); + + //Creating a new "div" in the customised section similar to the Apperance div + const appearanceBody = appearanceHeader.nextElementSibling; + const bodyClass = appearanceBody ? appearanceBody.className : 'pv-menu-list'; + const customBody = document.createElement('div'); + customBody.className = bodyClass; + customBody.id = 'customised_list'; + customBody.style.display = ''; // start expanded + + //Insert both right before Appearance + appearanceHeader.parentElement.insertBefore(customHeader, appearanceHeader); + appearanceHeader.parentElement.insertBefore(customBody, appearanceHeader); + + //collapse/expand + const toggle = () => { + const hidden = customBody.style.display === 'none'; + customBody.style.display = hidden ? '' : 'none'; + }; + customHeader.addEventListener('click', toggle); + customHeader.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } + }); + + return true; + } + + function init() { + if (insertCustomisedAboveAppearance()) return; + let tries = 0; + const t = setInterval(() => { + tries++; + if (insertCustomisedAboveAppearance() || tries > 50) clearInterval(t); + }, 200); + } + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); + else init(); + })(); + + + + /** + * A function that: + * - Autoselects the first pointcloud + * - Move the Elevation block from the Scene section into the custimosed Elevation Control section + * + * Nice to know: In the oroginal code the Elevation section in the Properties panel is only built when the pointcloud is clicked on + */ + (function () { + const $ = (sel, root = document) => root.querySelector(sel); + + //Select the fist pointcloud in the sidebar so that the Elevation section is built + function autoSelectFirstPointCloud() { + const cloudIcon = document.querySelector('#scene_objects i.jstree-themeicon-custom'); + if (cloudIcon) { + cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true })); + return true; + } + return false; + } + + //(re)connect the elevation labels to the slider after the container is moved (was not handled by default) + function rebindElevationLabel() { + const slider = window.jQuery ? window.jQuery("#sldHeightRange") : null; + const label = document.getElementById("lblHeightRange"); + if (!slider || !slider.length || !label) return; + + const update = () => { + const low = $slider.slider("values", 0); + const high = $slider.slider("values", 1); + label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}`; + }; + + //clear any old namespaced handlers and attach fresh ones + slider.off("slide.custom slidestop.custom change.custom"); + slider.on("slide.custom", update); + slider.on("slidestop.custom change.custom", update); + update(); + } + + //Move the elevation range section to the customised "Elevation Control" section + function moveElevationContainer() { + const target = $('#customised_list'); + const elevationContainer = document.querySelector('#materials\\.elevation_container'); + if (!elevationContainer) return false; + target.appendChild(elevationContainer); + rebindElevationLabel(); + return true; + + } + + function init() { + let tries = 0; + const t = setInterval(() => { + const hasCloud = !!(window.viewer?.scene?.pointclouds?.length); + if (hasCloud) { + autoSelectFirstPointCloud(); + // Wait until potree builds the Properties, then move the container + let innerTries = 0; + const t2 = setInterval(() => { + const movedElevation = moveElevationContainer(); + if (movedElevation || ++innerTries > 100) clearInterval(t2); + }, 100); + + clearInterval(t); + } + if (++tries > 30) clearInterval(t); + }, 100); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + })(); + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js index 578a794..e69de29 100644 --- a/src/main.js +++ b/src/main.js @@ -1,137 +0,0 @@ -/** - * Creating and inserting a new customised "Elevation Range" section in the sidebar above Apperance - * with the same css style features as Apperance -*/ -(function () { - - //Insert new the sidebar above Apperance - function insertCustomisedAboveAppearance() { - const appearanceHeader = document.querySelector('#menu_appearance'); - if (!appearanceHeader) return false; - if (document.querySelector('#menu_customised_header')) return true; - - //Clone the header for identical style to apperance - const customHeader = appearanceHeader.cloneNode(true); - customHeader.removeAttribute('id'); - customHeader.id = 'menu_customised_header'; - customHeader.textContent = 'Elevation Control'; - customHeader.style.cursor = 'pointer'; - customHeader.setAttribute('tabindex', '0'); - - //Creating a new "div" in the customised section similar to the Apperance div - const appearanceBody = appearanceHeader.nextElementSibling; - const bodyClass = appearanceBody ? appearanceBody.className : 'pv-menu-list'; - const customBody = document.createElement('div'); - customBody.className = bodyClass; - customBody.id = 'customised_list'; - customBody.style.display = ''; // start expanded - - //Insert both right before Appearance - appearanceHeader.parentElement.insertBefore(customHeader, appearanceHeader); - appearanceHeader.parentElement.insertBefore(customBody, appearanceHeader); - - //collapse/expand - const toggle = () => { - const hidden = customBody.style.display === 'none'; - customBody.style.display = hidden ? '' : 'none'; - }; - customHeader.addEventListener('click', toggle); - customHeader.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } - }); - - return true; - } - - function init() { - if (insertCustomisedAboveAppearance()) return; - let tries = 0; - const t = setInterval(() => { - tries++; - if (insertCustomisedAboveAppearance() || tries > 50) clearInterval(t); - }, 200); - } - - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); - else init(); -})(); - - - -/** - * A function that: - * - Autoselects the first pointcloud - * - Move the Elevation block from the Scene section into the custimosed Elevation Control section - * - * Nice to know: In the oroginal code the Elevation section in the Properties panel is only built when the pointcloud is clicked on - */ -(function () { - const $ = (sel, root = document) => root.querySelector(sel); - - //Select the fist pointcloud in the sidebar so that the Elevation section is built - function autoSelectFirstPointCloud() { - const cloudIcon = document.querySelector('#scene_objects i.jstree-themeicon-custom'); - if (cloudIcon) { - cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true })); - return true; - } - return false; - } - - //(re)connect the elevation labels to the slider after the container is moved (was not handled by default) - function rebindElevationLabel() { - const slider = window.jQuery ? window.jQuery("#sldHeightRange") : null; - const label = document.getElementById("lblHeightRange"); - if (!slider || !slider.length || !label) return; - - const update = () => { - const low = $slider.slider("values", 0); - const high = $slider.slider("values", 1); - label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}`; - }; - - //clear any old namespaced handlers and attach fresh ones - slider.off("slide.custom slidestop.custom change.custom"); - slider.on("slide.custom", update); - slider.on("slidestop.custom change.custom", update); - update(); - } - - //Move the elevation range section to the customised "Elevation Control" section - function moveElevationContainer() { - const target = $('#customised_list'); - const elevationContainer = document.querySelector('#materials\\.elevation_container'); - if (!elevationContainer) return false; - target.appendChild(elevationContainer); - rebindElevationLabel(); - return true; - - } - - function init() { - let tries = 0; - const t = setInterval(() => { - const hasCloud = !!(window.viewer?.scene?.pointclouds?.length); - if (hasCloud) { - autoSelectFirstPointCloud(); - // Wait until potree builds the Properties, then move the container - let innerTries = 0; - const t2 = setInterval(() => { - const movedElevation = moveElevationContainer(); - if (movedElevation || ++innerTries > 100) clearInterval(t2); - }, 100); - - clearInterval(t); - } - if (++tries > 30) clearInterval(t); - }, 100); - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } -})(); - - diff --git a/src/style.css b/src/style.css index 0a4215c..e69de29 100644 --- a/src/style.css +++ b/src/style.css @@ -1,11 +0,0 @@ -/* making sure that the divider span the whole sidebar (as the original code) */ -#customised_list > .divider { - margin: 0; - padding: 0; - } - -/* Padding of list items in the elevation control section as the original code*/ -#customised_list li { - list-style-type: none; - padding: 0 20px; - } \ No newline at end of file From 60aedc078bb64ae6108ff086ff3e07e915d54aef Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Mon, 29 Sep 2025 10:27:31 +0200 Subject: [PATCH 14/46] refactor(#9): change background name from None to Globe --- src/potreeViewer.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 58f1ab9..c098d30 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -13,7 +13,6 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { if (settings.pointBudget) viewer.setPointBudget(settings.pointBudget) viewer.loadSettingsFromURL() - viewer.setBackground(null) viewer.setDescription('Molloy Explorer') viewer.loadGUI(() => { @@ -29,6 +28,14 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { const pc = e.pointcloud viewer.scene.addPointCloud(pc) + // Change name of default background from 'None' to 'Globe"' + $('#background_options_none') + .text('Globe') + .attr('id', 'background_options_globe') + .val('globe') + + viewer.setBackground('globe') + pc.material.pointSizeType = Potree.PointSizeType.ADAPTIVE pc.material.shape = Potree.PointShape.CIRCLE pc.material.activeAttributeName = 'elevation' From 4c69a04d4a956b552f8a7bf24d152b4050efa5ca Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Mon, 29 Sep 2025 10:57:08 +0200 Subject: [PATCH 15/46] fix(#9): fix url and spelling mistake in config --- src/config.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/config.js b/src/config.js index 8245b56..b52bd33 100644 --- a/src/config.js +++ b/src/config.js @@ -1,12 +1,11 @@ -export const POTREE_POINTCLOUD_URL = - '../pointclouds/data_converted/metadata.json' +export const POTREE_POINTCLOUD_URL = '/pointclouds/data_converted/metadata.json' export const INITIAL_CESIUM_POS = { x: 4303414.154026048, y: 552161.235598733, z: 4660771.704035539, heading: 10, - pitch: -Cesium.Math.PI_PVER_FOUR, + pitch: -Cesium.Math.PI_OVER_FOUR, roll: 0.0 } From c405e67505c355899dd7ba15634778b916f4c67b Mon Sep 17 00:00:00 2001 From: Kleinc Date: Mon, 29 Sep 2025 11:38:53 +0200 Subject: [PATCH 16/46] feat(#10): :sparkles: Made the text clearer, added a box for elevation target The text was blurry before, but should now be clear. Also added the showing of elevation for a target point --- index.html | 7 +++- src/coordinateShowing.css | 24 +++++++++++++ src/coordinateShowing.js | 74 +++++++++++++++++++++++++++++++++++++++ src/main.js | 16 --------- src/style.css | 10 ------ 5 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 src/coordinateShowing.css create mode 100644 src/coordinateShowing.js diff --git a/index.html b/index.html index 3370c31..98d1737 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,7 @@ href="/libs/jstree/themes/mixed/style.css" /> + @@ -52,7 +53,10 @@ background-image: url('/build/potree/resources/images/background.jpg'); " > - + + +
+
@@ -93,5 +97,6 @@ }) + diff --git a/src/coordinateShowing.css b/src/coordinateShowing.css new file mode 100644 index 0000000..da616a2 --- /dev/null +++ b/src/coordinateShowing.css @@ -0,0 +1,24 @@ + +#posCanvas { + position: absolute; + left: 10px; + bottom: 10px; + width: 300px; + height: 50px; + pointer-events: none; + background-color: #19282C; + z-index: 10; + border-radius: 5px; +} + +#elevationCanvas { + position: absolute; + right: 100px; + top: 15px; + width: 300px; + height: 50px; + pointer-events: none; + background-color: #19282C; + z-index: 10; + border-radius: 5px; +} \ No newline at end of file diff --git a/src/coordinateShowing.js b/src/coordinateShowing.js new file mode 100644 index 0000000..ed1a1f1 --- /dev/null +++ b/src/coordinateShowing.js @@ -0,0 +1,74 @@ +// EPSG:32633 (WGS84 / UTM zone 33N) to WGS84 (lon/lat) +const utm33 = "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs"; //UTM zone 33N +const wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; //WGS84 is the current standard for GPS coordinates + +const posCanvas = document.getElementById('posCanvas'); +let posCtx = posCanvas.getContext('2d'); + +const elevationCanvas = document.getElementById('elevationCanvas'); +let elevationCtx = elevationCanvas.getContext('2d'); + + +function resizeCanvas(canvas) { + const dpr = window.devicePixelRatio || 1; + const ctx = canvas.getContext('2d'); + + // Set canvas internal size + canvas.width = canvas.clientWidth * dpr; + canvas.height = canvas.clientHeight * dpr; + + // Scale context so drawing uses CSS pixels + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + return ctx; +} + +function updateCameraOverlay() { + const cam = window.viewer.scene.view.position; + const [lon, lat] = proj4(utm33, wgs84, [cam.x, cam.y]); // Conversion using proj4js library + + const centerX = posCanvas.clientWidth / 2; + const centerY = posCanvas.clientHeight / 2; + + posCtx.clearRect(0, 0, posCanvas.width, posCanvas.height); + posCtx.fillStyle = '#cccccc'; + posCtx.font = '20px Arial'; + posCtx.textAlign = "center"; + posCtx.textBaseline = "middle"; + posCtx.fillText(`lat = ${lat.toFixed(2)}˚ lon = ${lon.toFixed(2)}˚`, centerX, centerY); +} + + +function targetElevation() { + const pivot = window.viewer.scene.view.getPivot(); + const mode = window.viewer.getControls(); + + const centerX = elevationCanvas.clientWidth / 2; + const centerY = elevationCanvas.clientHeight / 2; + + if (mode === window.viewer.orbitControls) { + elevationCanvas.style.display = 'inline'; + elevationCtx.clearRect(0, 0, elevationCanvas.width, elevationCanvas.height); + elevationCtx.fillStyle = '#cccccc'; + elevationCtx.font = '20px Arial'; + elevationCtx.textAlign = "center"; + elevationCtx.textBaseline = "middle"; + elevationCtx.fillText(`Target elevation = ${pivot.z.toFixed(2)}m`, centerX, centerY); + } + else{ + elevationCanvas.style.display = 'none'; + } +} + + +viewer.addEventListener("update", targetElevation); +viewer.addEventListener("update", updateCameraOverlay); + +posCtx = resizeCanvas(posCanvas); +elevationCtx = resizeCanvas(elevationCanvas); + +window.addEventListener('resize', () => { + posCtx = resizeCanvas(posCanvas); + elevationCtx = resizeCanvas(elevationCanvas); +}); + + diff --git a/src/main.js b/src/main.js index a3de3e7..8b13789 100644 --- a/src/main.js +++ b/src/main.js @@ -1,17 +1 @@ -// EPSG:32633 (WGS84 / UTM zone 33N) → WGS84 (lon/lat) -const utm33 = "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs"; //UTM zone 33N -const wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; //Current standard for GPS coordinates -const posCanvas = document.getElementById('camera-pos'); -const context = posCanvas.getContext('2d'); - -function updateCameraOverlay() { - const cam = window.viewer.scene.view.position; - const [lon, lat] = proj4(utm33, wgs84, [cam.x, cam.y]); // Conversion using proj4js library - - context.clearRect(0, 0, posCanvas.width, posCanvas.height); - context.fillStyle = 'white'; - context.font = '20px Times New Roman'; - context.fillText(`lat=${lat.toFixed(2)}˚ lon=${lon.toFixed(2)}˚`, 10, 40); -} -viewer.addEventListener("update", updateCameraOverlay); diff --git a/src/style.css b/src/style.css index 9cdf607..e69de29 100644 --- a/src/style.css +++ b/src/style.css @@ -1,10 +0,0 @@ - -#camera-pos { - position: absolute; - left: 10px; - bottom: 10px; - width: 300px; - height: 60px; - pointer-events: none; - z-index: 10; -} \ No newline at end of file From 3d6ec5e4a6e889e2a3211569abccb9170a682df6 Mon Sep 17 00:00:00 2001 From: Kleinc Date: Mon, 29 Sep 2025 13:39:36 +0200 Subject: [PATCH 17/46] docs(#10): :memo: Added function documentation --- src/coordinateShowing.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/coordinateShowing.js b/src/coordinateShowing.js index ed1a1f1..23ee291 100644 --- a/src/coordinateShowing.js +++ b/src/coordinateShowing.js @@ -8,7 +8,11 @@ let posCtx = posCanvas.getContext('2d'); const elevationCanvas = document.getElementById('elevationCanvas'); let elevationCtx = elevationCanvas.getContext('2d'); - +/** + * Resizes the canvas and its context to account for device pixel ratio. + * @param {*} canvas - The canvas element to resize. + * @returns {*} - The resized canvas context. + */ function resizeCanvas(canvas) { const dpr = window.devicePixelRatio || 1; const ctx = canvas.getContext('2d'); @@ -22,10 +26,12 @@ function resizeCanvas(canvas) { return ctx; } +/** + * Updates the camera overlay with the current latitude and longitude. + */ function updateCameraOverlay() { const cam = window.viewer.scene.view.position; const [lon, lat] = proj4(utm33, wgs84, [cam.x, cam.y]); // Conversion using proj4js library - const centerX = posCanvas.clientWidth / 2; const centerY = posCanvas.clientHeight / 2; @@ -37,7 +43,9 @@ function updateCameraOverlay() { posCtx.fillText(`lat = ${lat.toFixed(2)}˚ lon = ${lon.toFixed(2)}˚`, centerX, centerY); } - +/** + * Displays the target elevation when in orbit mode. + */ function targetElevation() { const pivot = window.viewer.scene.view.getPivot(); const mode = window.viewer.getControls(); From 2b66727d8531fc916de93d777ac8f0b08d6c0d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20Wahlstr=C3=B8m?= Date: Mon, 29 Sep 2025 14:14:25 +0200 Subject: [PATCH 18/46] refactor(#7): :recycle: Updated code for consistensy in the sidebar section --- index.html | 13 ++- src/ElevationControl/elevationControl.js | 107 ++++++++++++++++++ src/elevation_control.css | 11 -- src/elevation_control.js | 138 ----------------------- 4 files changed, 117 insertions(+), 152 deletions(-) create mode 100644 src/ElevationControl/elevationControl.js delete mode 100644 src/elevation_control.css delete mode 100644 src/elevation_control.js diff --git a/index.html b/index.html index 4c10df5..eb930b5 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,11 @@ href="/libs/jstree/themes/mixed/style.css" /> - + @@ -55,6 +59,7 @@
+ - diff --git a/src/ElevationControl/elevationControl.js b/src/ElevationControl/elevationControl.js new file mode 100644 index 0000000..276456b --- /dev/null +++ b/src/ElevationControl/elevationControl.js @@ -0,0 +1,107 @@ +//Cerating a customized section "Elevation Control" +window.createElevationPanel = function createElevationPanel(viewer) { + const container = document.getElementById('elevation_list') + let targetContainer = container + if (!targetContainer) { + // Create a new accordion section for Elevation Control + const menu = document.getElementById('potree_menu') + if (menu) { + const header = document.createElement('h3') + header.id = 'menu_elevation' + header.innerHTML = 'Elevation Control' + const panel = document.createElement('div') + panel.className = 'pv-menu-list' + panel.innerHTML = '
' + const about = document.getElementById('menu_appearance') + if (about) { + menu.insertBefore(panel, about) + menu.insertBefore(header, panel) + } else { + menu.appendChild(header) + menu.appendChild(panel) + } + // Activate accordion behavior if jQuery UI accordion already initialized + if ($(menu).accordion) { + try { + $(menu).accordion('refresh') + } catch (e) {} + } + // Toggle on header click if not managed by accordion refresh + header.addEventListener( + 'click', + () => + (panel.style.display = panel.style.display === 'none' ? '' : 'none') + ) + targetContainer = panel.querySelector('#elevation_list') + } + } +} + +//Select the fist pointcloud in the sidebar so that the Elevation section is built +function autoSelectFirstPointCloud() { + const cloudIcon = document.querySelector( + '#scene_objects i.jstree-themeicon-custom' + ) + if (cloudIcon) { + cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true })) + return true + } + return false +} + +//(re)connect the elevation labels to the slider after the container is moved (was not handled by default) +function rebindElevationLabel() { + const slider = window.jQuery ? window.jQuery('#sldHeightRange') : null + const label = document.getElementById('lblHeightRange') + if (!slider || !slider.length || !label) return + + const update = () => { + const low = slider.slider('values', 0) + const high = slider.slider('values', 1) + label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}` + } + + //clear any old namespaced handlers and attach fresh ones + slider.off('slide.custom slidestop.custom change.custom') + slider.on('slide.custom', update) + slider.on('slidestop.custom change.custom', update) + update() +} + +//Move the elevation range section to the customised "Elevation Control" section +function moveElevationContainer() { + const target = document.getElementById('elevation_list') + const elevationContainer = document.querySelector( + '#materials\\.elevation_container' + ) + if (!elevationContainer) return false + target.appendChild(elevationContainer) + rebindElevationLabel() + return true +} + +//initiate and orchestrate all funcitons to render the Evelation control section of the sidebar propperly +window.initElevationControls = function initElevationControls(viewer) { + + //Creates the section + createElevationPanel(viewer) + + //Only move the ElevationContainer if the source container to exist + const menu = + document.getElementById('potree_menu') || + document.getElementById('menu') || + document.body + const observer = new MutationObserver(() => { + const found = document.querySelector('#materials\\.elevation_container') + if (found) { + observer.disconnect() + //Move and rebind once it exists + const ok = moveElevationContainer() + if (!ok) console.warn('[Elevation] moveElevationContainer failed') + } + }) + observer.observe(menu, { childList: true, subtree: true }) + + //Trigger Potree to build Materials UI by selecting the first point cloud (if nothing selected yet) + autoSelectFirstPointCloud() +} diff --git a/src/elevation_control.css b/src/elevation_control.css deleted file mode 100644 index 0a4215c..0000000 --- a/src/elevation_control.css +++ /dev/null @@ -1,11 +0,0 @@ -/* making sure that the divider span the whole sidebar (as the original code) */ -#customised_list > .divider { - margin: 0; - padding: 0; - } - -/* Padding of list items in the elevation control section as the original code*/ -#customised_list li { - list-style-type: none; - padding: 0 20px; - } \ No newline at end of file diff --git a/src/elevation_control.js b/src/elevation_control.js deleted file mode 100644 index e6a0f4e..0000000 --- a/src/elevation_control.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Creating and inserting a new customised "Elevation Range" section in the sidebar above Apperance - * with the same css style features as Apperance -*/ -(function () { - - //Insert new the sidebar above Apperance - function insertCustomisedAboveAppearance() { - const appearanceHeader = document.querySelector('#menu_appearance'); - if (!appearanceHeader) return false; - if (document.querySelector('#menu_customised_header')) return true; - - //Clone the header for identical style to apperance - const customHeader = appearanceHeader.cloneNode(true); - customHeader.removeAttribute('id'); - customHeader.id = 'menu_customised_header'; - customHeader.textContent = 'Elevation Control'; - customHeader.style.cursor = 'pointer'; - customHeader.setAttribute('tabindex', '0'); - - //Creating a new "div" in the customised section similar to the Apperance div - const appearanceBody = appearanceHeader.nextElementSibling; - const bodyClass = appearanceBody ? appearanceBody.className : 'pv-menu-list'; - const customBody = document.createElement('div'); - customBody.className = bodyClass; - customBody.id = 'customised_list'; - customBody.style.display = ''; // start expanded - - //Insert both right before Appearance - appearanceHeader.parentElement.insertBefore(customHeader, appearanceHeader); - appearanceHeader.parentElement.insertBefore(customBody, appearanceHeader); - - //collapse/expand - const toggle = () => { - const hidden = customBody.style.display === 'none'; - customBody.style.display = hidden ? '' : 'none'; - }; - customHeader.addEventListener('click', toggle); - customHeader.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } - }); - - return true; - } - - function init() { - if (insertCustomisedAboveAppearance()) return; - let tries = 0; - const t = setInterval(() => { - tries++; - if (insertCustomisedAboveAppearance() || tries > 50) clearInterval(t); - }, 200); - } - - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); - else init(); - })(); - - - - /** - * A function that: - * - Autoselects the first pointcloud - * - Move the Elevation block from the Scene section into the custimosed Elevation Control section - * - * Nice to know: In the oroginal code the Elevation section in the Properties panel is only built when the pointcloud is clicked on - */ - (function () { - const $ = (sel, root = document) => root.querySelector(sel); - - //Select the fist pointcloud in the sidebar so that the Elevation section is built - function autoSelectFirstPointCloud() { - const cloudIcon = document.querySelector('#scene_objects i.jstree-themeicon-custom'); - if (cloudIcon) { - cloudIcon.dispatchEvent(new MouseEvent('click', { bubbles: true })); - return true; - } - return false; - } - - //(re)connect the elevation labels to the slider after the container is moved (was not handled by default) - function rebindElevationLabel() { - const slider = window.jQuery ? window.jQuery("#sldHeightRange") : null; - const label = document.getElementById("lblHeightRange"); - if (!slider || !slider.length || !label) return; - - const update = () => { - const low = $slider.slider("values", 0); - const high = $slider.slider("values", 1); - label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}`; - }; - - //clear any old namespaced handlers and attach fresh ones - slider.off("slide.custom slidestop.custom change.custom"); - slider.on("slide.custom", update); - slider.on("slidestop.custom change.custom", update); - update(); - } - - //Move the elevation range section to the customised "Elevation Control" section - function moveElevationContainer() { - const target = $('#customised_list'); - const elevationContainer = document.querySelector('#materials\\.elevation_container'); - if (!elevationContainer) return false; - target.appendChild(elevationContainer); - rebindElevationLabel(); - return true; - - } - - function init() { - let tries = 0; - const t = setInterval(() => { - const hasCloud = !!(window.viewer?.scene?.pointclouds?.length); - if (hasCloud) { - autoSelectFirstPointCloud(); - // Wait until potree builds the Properties, then move the container - let innerTries = 0; - const t2 = setInterval(() => { - const movedElevation = moveElevationContainer(); - if (movedElevation || ++innerTries > 100) clearInterval(t2); - }, 100); - - clearInterval(t); - } - if (++tries > 30) clearInterval(t); - }, 100); - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } - })(); - - - \ No newline at end of file From 75c5b4370198174a1fd42699d1bedff5b8b38a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 1 Oct 2025 12:45:04 +0200 Subject: [PATCH 19/46] feat(#5): :sparkles: Display measurements in a sorted list, and move the tools for measurements to the same tab Closes #5 --- index.html | 13 +- public/build/potree/sidebar.html | 11 - src/measurementsPanel.css | 20 ++ src/measurementsPanel.js | 386 +++++++++++++++++++++++-------- 4 files changed, 318 insertions(+), 112 deletions(-) diff --git a/index.html b/index.html index 29196d7..84536ed 100644 --- a/index.html +++ b/index.html @@ -55,8 +55,8 @@ - + diff --git a/src/measurementsPanel.css b/src/measurementsPanel.css index 67d7d1f..9e05476 100644 --- a/src/measurementsPanel.css +++ b/src/measurementsPanel.css @@ -1,40 +1,228 @@ -#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 .coordRow td{background:#2f383d;font-family:monospace;font-size:11px;padding:4px 6px;border:1px solid #404a50;border-radius:4px;margin:2px 0;} -#measurements_list .segmentRow td{background:transparent;padding:0 0 2px 0;} -#measurements_list .segmentConnector{width:1px;height:12px;background:#505b61;margin:2px auto 0;} -#measurements_list .segmentPill{display:inline-block;background:#3d474d;border:1px solid #566067;padding:2px 10px;margin:2px auto 4px;border-radius:6px;font-size:11px;font-family:monospace;} -#measurements_list .totalRow td{background:#424d53;font-weight:600;font-size:12px;text-align:center;border:1px solid #59646a;border-radius:6px;margin-top:4px;} -#measurements_list .separator td{padding:0;height:2px;background:transparent;} -#measurements_list .attrRow td{font-size:11px;color:#cfd5d8;} -#measurements_list .empty{opacity:.6;padding:8px;text-align:center;} -#measurements_list{max-height:400px;overflow-y:auto;margin:10px 0;} -#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);} +#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 .coordRow td { + background: #2f383d; + font-family: monospace; + font-size: 11px; + padding: 4px 6px; + border: 1px solid #404a50; + border-radius: 4px; + margin: 2px 0; +} +#measurements_list .segmentRow td { + background: transparent; + padding: 0 0 2px 0; +} +#measurements_list .segmentConnector { + width: 1px; + height: 12px; + background: #505b61; + margin: 2px auto 0; +} +#measurements_list .segmentPill { + display: inline-block; + background: #3d474d; + border: 1px solid #566067; + padding: 2px 10px; + margin: 2px auto 4px; + border-radius: 6px; + font-size: 11px; + font-family: monospace; +} +#measurements_list .totalRow td { + background: #424d53; + font-weight: 600; + font-size: 12px; + text-align: center; + border: 1px solid #59646a; + border-radius: 6px; + margin-top: 4px; +} +#measurements_list .separator td { + padding: 0; + height: 2px; + background: transparent; +} +#measurements_list .attrRow td { + font-size: 11px; + color: #cfd5d8; +} +#measurements_list .empty { + opacity: 0.6; + padding: 8px; + text-align: center; +} +#measurements_list { + max-height: 400px; + overflow-y: auto; + margin: 10px 0; +} +#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); +} /* Modern grouped measurement list */ -#measurement_items{background:#252d31;border:1px solid #1b2326;border-radius:6px;font-size:12px;color:#d9e2e6;} -#measurement_items .m-empty{padding:10px;text-align:center;opacity:.6;} -#measurement_items .m-group{border-top:1px solid #303a3f;} -#measurement_items .m-group:first-child{border-top:none;} -#measurement_items .m-group-header{display:flex;align-items:center;gap:6px;padding:6px 10px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;font-size:11px;background:linear-gradient(#2f383d,#2b3438);cursor:pointer;user-select:none;position:sticky;top:0;z-index:1;} -#measurement_items .m-group-header:hover{background:#374247;} -#measurement_items .m-group-caret{font-size:10px;width:14px;text-align:center;color:#8fb9c9;} -#measurement_items .m-group-title{flex:1;color:#c7d4d9;} -#measurement_items .m-group-count{background:#3d4850;color:#9fb7c2;font-size:10px;padding:2px 6px;border-radius:10px;line-height:1;} -#measurement_items .m-group-body{padding:4px 4px 6px;} -#measurement_items .m-row{display:flex;align-items:center;gap:6px;padding:6px 8px;margin:2px 2px;border:1px solid transparent;border-radius:4px;background:#2c3539;transition:background .15s,border-color .15s;cursor:pointer;} -#measurement_items .m-row-icon{width:16px;flex:0 0 16px;text-align:center;font-size:12px;color:#8fb9c9;filter:drop-shadow(0 0 2px #0a0f12);} -#measurement_items .m-row:hover{background:#354045;border-color:#425056;} -#measurement_items .m-row.active{background:#1f4b63;border-color:#2f6b8c;box-shadow:0 0 0 1px #2f6b8c66;} -#measurement_items .m-row-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} -#measurement_items .m-row-delete{background:#3b2626;border:1px solid #5a3a3a;color:#ff9a9a;font-weight:600;font-size:11px;line-height:1;padding:4px 6px;border-radius:4px;cursor:pointer;transition:background .15s, color .15s;} -#measurement_items .m-row-delete:hover{background:#5a2d2d;color:#fff;} - +#measurement_items { + background: #252d31; + border: 1px solid #1b2326; + border-radius: 6px; + font-size: 12px; + color: #d9e2e6; +} +#measurement_items .m-empty { + padding: 10px; + text-align: center; + opacity: 0.6; +} +#measurement_items .m-group { + border-top: 1px solid #303a3f; +} +#measurement_items .m-group:first-child { + border-top: none; +} +#measurement_items .m-group-header { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + font-size: 11px; + background: linear-gradient(#2f383d, #2b3438); + cursor: pointer; + user-select: none; + position: sticky; + top: 0; + z-index: 1; +} +#measurement_items .m-group-header:hover { + background: #374247; +} +#measurement_items .m-group-caret { + font-size: 10px; + width: 14px; + text-align: center; + color: #8fb9c9; +} +#measurement_items .m-group-title { + flex: 1; + color: #c7d4d9; +} +#measurement_items .m-group-count { + background: #3d4850; + color: #9fb7c2; + font-size: 10px; + padding: 2px 6px; + border-radius: 10px; + line-height: 1; +} +#measurement_items .m-group-body { + padding: 4px 4px 6px; +} +#measurement_items .m-row { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + margin: 2px 2px; + border: 1px solid transparent; + border-radius: 4px; + background: #2c3539; + transition: + background 0.15s, + border-color 0.15s; + cursor: pointer; +} +#measurement_items .m-row-icon { + width: 16px; + flex: 0 0 16px; + text-align: center; + font-size: 12px; + color: #8fb9c9; + filter: drop-shadow(0 0 2px #0a0f12); +} +#measurement_items .m-row:hover { + background: #354045; + border-color: #425056; +} +#measurement_items .m-row.active { + background: #1f4b63; + border-color: #2f6b8c; + box-shadow: 0 0 0 1px #2f6b8c66; +} +#measurement_items .m-row-label { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#measurement_items .m-row-delete { + background: #3b2626; + border: 1px solid #5a3a3a; + color: #ff9a9a; + font-weight: 600; + font-size: 11px; + line-height: 1; + padding: 4px 6px; + border-radius: 4px; + cursor: pointer; + transition: + background 0.15s, + color 0.15s; +} +#measurement_items .m-row-delete:hover { + background: #5a2d2d; + color: #fff; +} diff --git a/src/measurementsPanel.js b/src/measurementsPanel.js index 7af504d..e2052ea 100644 --- a/src/measurementsPanel.js +++ b/src/measurementsPanel.js @@ -1,321 +1,368 @@ /** * Measurements Panel - * Injects a custom Measurements accordion section, shows grouped measurement + * Injects a custom Measurements tab section, shows grouped measurement * entries with per-type numbering, syncs selection with Potree's jsTree, and * dynamically mounts the native properties panel when a measurement is active. */ -window.initMeasurementsPanel = function initMeasurementsPanel(viewer){ +window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { // Resolve or create measurements container in Potree menu - const existingListContainer = document.getElementById('measurements_list'); - let targetContainer = existingListContainer; - if(!targetContainer){ - const menu = document.getElementById('potree_menu'); - if(menu){ - const header = document.createElement('h3'); - header.id = 'menu_point_measurements'; - header.innerHTML = 'Measurements'; - const panel = document.createElement('div'); - panel.className = 'pv-menu-list'; - panel.innerHTML = '
'; + const existingListContainer = document.getElementById('measurements_list') + let targetContainer = existingListContainer + if (!targetContainer) { + const menu = document.getElementById('potree_menu') + if (menu) { + const header = document.createElement('h3') + header.id = 'menu_point_measurements' + header.innerHTML = 'Measurements' + const panel = document.createElement('div') + panel.className = 'pv-menu-list' + panel.innerHTML = + '
' // Insert before filters/about if possible, else append at end - const about = document.getElementById('menu_about'); - if(about){ - menu.insertBefore(panel, about); - menu.insertBefore(header, panel); + const about = document.getElementById('menu_about') + if (about) { + menu.insertBefore(panel, about) + menu.insertBefore(header, panel) } else { - menu.appendChild(header); - menu.appendChild(panel); + menu.appendChild(header) + menu.appendChild(panel) } // Activate tab behavior if jQuery UI accordion already initialized - if($(menu).accordion){ - try { $(menu).accordion('refresh'); } catch(e) {} + if ($(menu).accordion) { + try { + $(menu).accordion('refresh') + } catch (e) {} } // Toggle on header click if not managed by accordion refresh - header.addEventListener('click', ()=> panel.style.display = panel.style.display==='none'?'':'none'); - targetContainer = panel.querySelector('#measurements_list'); + header.addEventListener( + 'click', + () => + (panel.style.display = panel.style.display === 'none' ? '' : 'none') + ) + targetContainer = panel.querySelector('#measurements_list') } } - if(!targetContainer){ - console.warn('Measurements list container not found and dynamic injection failed'); - return; + if (!targetContainer) { + console.warn( + 'Measurements list container not found and dynamic injection failed' + ) + return } - let listRoot = document.createElement('div'); - listRoot.id = 'measurement_items'; - listRoot.style.cssText = 'max-height:260px; overflow:auto; margin-bottom:6px; border:1px solid #333; border-radius:4px;'; - targetContainer.parentElement && targetContainer.parentElement.insertBefore(listRoot, targetContainer); + let listRoot = document.createElement('div') + listRoot.id = 'measurement_items' + listRoot.style.cssText = + 'max-height:260px; overflow:auto; margin-bottom:6px; border:1px solid #333; border-radius:4px;' + targetContainer.parentElement && + targetContainer.parentElement.insertBefore(listRoot, targetContainer) // Creating an incremental number for each measurment type - const creationOrder = []; - const uuidRef = new Map(); + const creationOrder = [] + const uuidRef = new Map() - function trackCreation(obj){ - if(!obj || !obj.uuid) return; - if(!uuidRef.has(obj.uuid)){ - uuidRef.set(obj.uuid, obj); - creationOrder.push(obj.uuid); + function trackCreation(obj) { + if (!obj || !obj.uuid) return + if (!uuidRef.has(obj.uuid)) { + uuidRef.set(obj.uuid, obj) + creationOrder.push(obj.uuid) } else { - uuidRef.set(obj.uuid, obj); + uuidRef.set(obj.uuid, obj) } } - function buildTypeIndices(items){ - const indexMap = new Map(); - for(const uuid of creationOrder){ - const entry = items.find(it=>it.obj.uuid===uuid); - if(!entry) continue; - const t = entry.type; - if(!indexMap.has(t)) indexMap.set(t, new Map()); - const map = indexMap.get(t); - if(!map.has(uuid)) map.set(uuid, map.size + 1); + function buildTypeIndices(items) { + const indexMap = new Map() + for (const uuid of creationOrder) { + const entry = items.find((it) => it.obj.uuid === uuid) + if (!entry) continue + const t = entry.type + if (!indexMap.has(t)) indexMap.set(t, new Map()) + const map = indexMap.get(t) + if (!map.has(uuid)) map.set(uuid, map.size + 1) } - return indexMap; + return indexMap } const TYPE_ICONS = { - 'Point':'●', - 'Distance':'﹔', - 'Height':'↕', - 'Area':'▧', - 'Angle':'∠', - 'Circle':'◯', - 'Azimuth':'N', - 'Volume':'▣', - 'Profile':'≋', - 'Measurement':'●' - }; + Point: '●', + Distance: '﹔', + Height: '↕', + Area: '▧', + Angle: '∠', + Circle: '◯', + Azimuth: 'N', + Volume: '▣', + Profile: '≋', + Measurement: '●' + } - function resolveType(m){ - const ctor = m.constructor?.name || ''; - if(/Volume/i.test(ctor)) return 'Volume'; - if(/Profile/i.test(ctor)) return 'Profile'; - if(/Circle/i.test(ctor) || m.showCircle) return 'Circle'; - if(/Angle/i.test(ctor) || m.showAngle) return 'Angle'; - if(/Height/i.test(ctor) || m.showHeight && m.points?.length >= 2) return 'Height'; - if(/Azimuth/i.test(ctor) || m.showAzimuth) return 'Azimuth'; - if(/Area/i.test(ctor) || (m.showDistances && m.showArea)) return 'Area'; - if(m.className) return m.className; + function resolveType(m) { + const ctor = m.constructor?.name || '' + if (/Volume/i.test(ctor)) return 'Volume' + if (/Profile/i.test(ctor)) return 'Profile' + if (/Circle/i.test(ctor) || m.showCircle) return 'Circle' + if (/Angle/i.test(ctor) || m.showAngle) return 'Angle' + if (/Height/i.test(ctor) || (m.showHeight && m.points?.length >= 2)) + return 'Height' + if (/Azimuth/i.test(ctor) || m.showAzimuth) return 'Azimuth' + if (/Area/i.test(ctor) || (m.showDistances && m.showArea)) return 'Area' + if (m.className) return m.className // Fallback heuristics on point count - if(m.points){ - if(m.points.length === 1) return 'Point'; - if(m.points.length === 2 && m.showHeight) return 'Height'; - return 'Distance'; + if (m.points) { + if (m.points.length === 1) return 'Point' + if (m.points.length === 2 && m.showHeight) return 'Height' + return 'Distance' } - return 'Measurement'; + return 'Measurement' } - const originalPropertiesPanel = document.querySelector('#scene_object_properties'); + const originalPropertiesPanel = document.querySelector( + '#scene_object_properties' + ) // This section is for moving the "scene" object to the meaasurements tab, only if the measurements section is selected, so that all other folders (like camera) stays where it should be - let placeholder = null; - let originalParent = null; - function ensurePlaceholder(){ - if(originalPropertiesPanel && !placeholder){ - originalParent = originalPropertiesPanel.parentElement; - placeholder = document.createElement('div'); - placeholder.id = 'scene_object_properties_placeholder'; - placeholder.style.display='none'; - originalParent.insertBefore(placeholder, originalPropertiesPanel); + let placeholder = null + let originalParent = null + function ensurePlaceholder() { + if (originalPropertiesPanel && !placeholder) { + originalParent = originalPropertiesPanel.parentElement + placeholder = document.createElement('div') + placeholder.id = 'scene_object_properties_placeholder' + placeholder.style.display = 'none' + originalParent.insertBefore(placeholder, originalPropertiesPanel) } } - function showPanelInMeasurements(){ - ensurePlaceholder(); - if(!originalPropertiesPanel) return; - if(originalPropertiesPanel.parentElement !== targetContainer){ - targetContainer.innerHTML=''; - targetContainer.appendChild(originalPropertiesPanel); + function showPanelInMeasurements() { + ensurePlaceholder() + if (!originalPropertiesPanel) return + if (originalPropertiesPanel.parentElement !== targetContainer) { + targetContainer.innerHTML = '' + targetContainer.appendChild(originalPropertiesPanel) } } - function restorePanelToOriginal(){ - if(!originalPropertiesPanel || !placeholder || !originalParent) return; - if(originalPropertiesPanel.parentElement !== originalParent){ - originalParent.insertBefore(originalPropertiesPanel, placeholder.nextSibling); + function restorePanelToOriginal() { + if (!originalPropertiesPanel || !placeholder || !originalParent) return + if (originalPropertiesPanel.parentElement !== originalParent) { + originalParent.insertBefore( + originalPropertiesPanel, + placeholder.nextSibling + ) } - if(targetContainer && targetContainer.children.length === 0){ - targetContainer.innerHTML = '
Select a measurement to view its properties here
'; + if (targetContainer && targetContainer.children.length === 0) { + targetContainer.innerHTML = + '
Select a measurement to view its properties here
' } } - if(targetContainer){ - targetContainer.innerHTML = '
Select a measurement to view its properties here
'; + if (targetContainer) { + targetContainer.innerHTML = + '
Select a measurement to view its properties here
' } // Helper to decide if a uuid is a measurement-like object - function isMeasurementUUID(uuid){ - if(!uuid) return false; - const s = viewer.scene; - return s.measurements.some(m=>m.uuid===uuid) || s.volumes.some(v=>v.uuid===uuid) || s.profiles.some(p=>p.uuid===uuid); + function isMeasurementUUID(uuid) { + if (!uuid) return false + const s = viewer.scene + return ( + s.measurements.some((m) => m.uuid === uuid) || + s.volumes.some((v) => v.uuid === uuid) || + s.profiles.some((p) => p.uuid === uuid) + ) } // If on load there's already a selected measurement, move panel immediately - setTimeout(()=>{ + setTimeout(() => { try { - const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); - if(tree){ - const sel = tree.get_selected(true)[0]; - if(sel && sel.data && isMeasurementUUID(sel.data.uuid)){ - showPanelInMeasurements(); + const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (tree) { + const sel = tree.get_selected(true)[0] + if (sel && sel.data && isMeasurementUUID(sel.data.uuid)) { + showPanelInMeasurements() } } - } catch(_e) {} - },0); + } catch (_e) {} + }, 0) // Build/update the measurement list - function rebuildMeasurementList(){ - if(!listRoot) return; - const scene = viewer.scene; + function rebuildMeasurementList() { + if (!listRoot) return + const scene = viewer.scene const itemsRaw = [ - ...scene.measurements.map(m=>({type: resolveType(m), obj:m})), - ...scene.profiles.map(p=>({type:'Profile', obj:p})), - ...scene.volumes.map(v=>({type:'Volume', obj:v})) - ]; - itemsRaw.forEach(entry=> trackCreation(entry.obj)); - const perTypeNumbers = buildTypeIndices(itemsRaw); + ...scene.measurements.map((m) => ({ type: resolveType(m), obj: m })), + ...scene.profiles.map((p) => ({ type: 'Profile', obj: p })), + ...scene.volumes.map((v) => ({ type: 'Volume', obj: v })) + ] + itemsRaw.forEach((entry) => trackCreation(entry.obj)) + const perTypeNumbers = buildTypeIndices(itemsRaw) // Group by type while preserving overall creation order within each type - const groups = new Map(); - for(const entry of itemsRaw){ - if(!groups.has(entry.type)) groups.set(entry.type, []); - groups.get(entry.type).push(entry); + const groups = new Map() + for (const entry of itemsRaw) { + if (!groups.has(entry.type)) groups.set(entry.type, []) + groups.get(entry.type).push(entry) } - const order = Array.from(groups.keys()).sort(); - listRoot.innerHTML = ''; - if(itemsRaw.length===0){ - listRoot.innerHTML = '
No measurements
'; - return; + const order = Array.from(groups.keys()).sort() + listRoot.innerHTML = '' + if (itemsRaw.length === 0) { + listRoot.innerHTML = '
No measurements
' + return } - order.forEach(type=>{ - const section = document.createElement('div'); - section.className = 'm-group'; - const gid = 'g_'+type.toLowerCase(); - section.innerHTML = `\n
\n \n ${type}\n \n
\n
`; - listRoot.appendChild(section); - const body = section.querySelector('.m-group-body'); - groups.get(type).forEach(entry=>{ - const m = entry.obj; - const num = perTypeNumbers.get(type)?.get(m.uuid) || 0; - const baseName = `${type} #${num}`; - const row = document.createElement('div'); - row.className = 'm-row'; - row.dataset.uuid = m.uuid; - const icon = TYPE_ICONS[type] || TYPE_ICONS['Measurement']; - row.innerHTML = `${icon}${baseName}`+ - ''; - body.appendChild(row); - }); - section.querySelector('#'+gid+'_count').textContent = groups.get(type).length; - }); + order.forEach((type) => { + const section = document.createElement('div') + section.className = 'm-group' + const gid = 'g_' + type.toLowerCase() + section.innerHTML = `\n
\n \n ${type}\n \n
\n
` + listRoot.appendChild(section) + const body = section.querySelector('.m-group-body') + groups.get(type).forEach((entry) => { + const m = entry.obj + const num = perTypeNumbers.get(type)?.get(m.uuid) || 0 + const baseName = `${type} #${num}` + const row = document.createElement('div') + row.className = 'm-row' + row.dataset.uuid = m.uuid + const icon = TYPE_ICONS[type] || TYPE_ICONS['Measurement'] + row.innerHTML = + `${icon}${baseName}` + + '' + body.appendChild(row) + }) + section.querySelector('#' + gid + '_count').textContent = + groups.get(type).length + }) } - rebuildMeasurementList(); + rebuildMeasurementList() // Hook into scene add/remove events to refresh list - viewer.scene.addEventListener('measurement_added', (e)=>{ - const obj = e.measurement || e.object || e.detail || null; + viewer.scene.addEventListener('measurement_added', (e) => { + const obj = e.measurement || e.object || e.detail || null // Some measurements start as a point, then become distance or height when adding more points to it. This is a listener for that. - if(obj && obj.addEventListener && !obj._mp_listenersAttached){ - obj._mp_listenersAttached = true; + if (obj && obj.addEventListener && !obj._mp_listenersAttached) { + obj._mp_listenersAttached = true // Potree measurement objects usually fire their own events; fallback to polling if needed - ['marker_added','marker_removed','point_added','point_removed'].forEach(ev=>{ - try { obj.addEventListener(ev, ()=> rebuildMeasurementList()); } catch(_e) {} - }); + ;[ + 'marker_added', + 'marker_removed', + 'point_added', + 'point_removed' + ].forEach((ev) => { + try { + obj.addEventListener(ev, () => rebuildMeasurementList()) + } catch (_e) {} + }) } - rebuildMeasurementList(); - if(obj && isMeasurementUUID(obj.uuid)){ - showPanelInMeasurements(); + rebuildMeasurementList() + if (obj && isMeasurementUUID(obj.uuid)) { + showPanelInMeasurements() } else { - const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree(); - if(tree){ - const sel = tree.get_selected(true)[0]; - if(sel && sel.data && isMeasurementUUID(sel.data.uuid)){ - showPanelInMeasurements(); + const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (tree) { + const sel = tree.get_selected(true)[0] + if (sel && sel.data && isMeasurementUUID(sel.data.uuid)) { + showPanelInMeasurements() } } } - }); + }) //volume and profile have their own event handlers and wont be included in the basic "measurement_added" - viewer.scene.addEventListener('measurement_removed', rebuildMeasurementList); - viewer.scene.addEventListener('volume_added', rebuildMeasurementList); - viewer.scene.addEventListener('volume_removed', rebuildMeasurementList); - viewer.scene.addEventListener('profile_added', rebuildMeasurementList); - viewer.scene.addEventListener('profile_removed', rebuildMeasurementList); + viewer.scene.addEventListener('measurement_removed', rebuildMeasurementList) + viewer.scene.addEventListener('volume_added', rebuildMeasurementList) + viewer.scene.addEventListener('volume_removed', rebuildMeasurementList) + viewer.scene.addEventListener('profile_added', rebuildMeasurementList) + viewer.scene.addEventListener('profile_removed', rebuildMeasurementList) // Click handling for selection, focus and delete - listRoot.addEventListener('click', (e)=>{ - const header = e.target.closest('.m-group-header'); - if(header){ - const open = header.getAttribute('data-open') === 'true'; - header.setAttribute('data-open', String(!open)); - const caret = header.querySelector('.m-group-caret'); - const body = header.parentElement.querySelector('.m-group-body'); - if(body){ body.style.display = open ? 'none':'block'; } - if(caret){ caret.textContent = open ? '▸':'▾'; } - return; + listRoot.addEventListener('click', (e) => { + const header = e.target.closest('.m-group-header') + if (header) { + const open = header.getAttribute('data-open') === 'true' + header.setAttribute('data-open', String(!open)) + const caret = header.querySelector('.m-group-caret') + const body = header.parentElement.querySelector('.m-group-body') + if (body) { + body.style.display = open ? 'none' : 'block' + } + if (caret) { + caret.textContent = open ? '▸' : '▾' + } + return } - const btn = e.target.closest('button'); - const row = e.target.closest('.m-row[data-uuid]'); - if(!row) return; - const uuid = row.dataset.uuid; - const scene = viewer.scene; - const all = [...scene.measurements, ...scene.profiles, ...scene.volumes]; - const obj = all.find(o=>o.uuid===uuid); - if(!obj) return; - if(btn){ - const act = btn.dataset.act; - if(act==='delete'){ - if(scene.removeMeasurement && scene.measurements.includes(obj)) scene.removeMeasurement(obj); - else if(scene.removeVolume && scene.volumes.includes(obj)) scene.removeVolume(obj); - else if(scene.removeProfile && scene.profiles.includes(obj)) scene.removeProfile(obj); - rebuildMeasurementList(); - return; + const btn = e.target.closest('button') + const row = e.target.closest('.m-row[data-uuid]') + if (!row) return + const uuid = row.dataset.uuid + const scene = viewer.scene + const all = [...scene.measurements, ...scene.profiles, ...scene.volumes] + const obj = all.find((o) => o.uuid === uuid) + if (!obj) return + if (btn) { + const act = btn.dataset.act + if (act === 'delete') { + if (scene.removeMeasurement && scene.measurements.includes(obj)) + scene.removeMeasurement(obj) + else if (scene.removeVolume && scene.volumes.includes(obj)) + scene.removeVolume(obj) + else if (scene.removeProfile && scene.profiles.includes(obj)) + scene.removeProfile(obj) + rebuildMeasurementList() + return } } else { - const treeElement = document.getElementById('jstree_scene'); - if(treeElement && $(treeElement).jstree){ - let measurementsRoot = $('#jstree_scene').jstree().get_json('measurements'); - const findNode = (root) => root.children.find(ch => ch.data && ch.data.uuid === uuid); - let node = measurementsRoot && findNode(measurementsRoot); - if(!node){ - node = measurementsRoot && measurementsRoot.children.find(ch => ch.data && ch.data.uuid === uuid); + const treeElement = document.getElementById('jstree_scene') + if (treeElement && $(treeElement).jstree) { + let measurementsRoot = $('#jstree_scene') + .jstree() + .get_json('measurements') + const findNode = (root) => + root.children.find((ch) => ch.data && ch.data.uuid === uuid) + let node = measurementsRoot && findNode(measurementsRoot) + if (!node) { + node = + measurementsRoot && + measurementsRoot.children.find( + (ch) => ch.data && ch.data.uuid === uuid + ) } - if(node){ - $.jstree.reference(node.id).deselect_all(); - $.jstree.reference(node.id).select_node(node.id); + if (node) { + $.jstree.reference(node.id).deselect_all() + $.jstree.reference(node.id).select_node(node.id) } } - [...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach(el=>{ - el.classList.toggle('active', el.dataset.uuid===uuid); - }); - showPanelInMeasurements(); + ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { + el.classList.toggle('active', el.dataset.uuid === uuid) + }) + showPanelInMeasurements() } - }); + }) // Sync highlight if selection changes via original tree - document.addEventListener('click', (e)=>{ - if(!e.target.closest('#jstree_scene')) return; - setTimeout(()=>{ - const tree = $('#jstree_scene').jstree(); - if(!tree) return; - const sel = tree.get_selected(true)[0]; - if(sel && sel.data && sel.data.uuid){ - const uuid = sel.data.uuid; - [...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach(el=>{ - el.classList.toggle('active', el.dataset.uuid===uuid); - }); + document.addEventListener('click', (e) => { + if (!e.target.closest('#jstree_scene')) return + setTimeout(() => { + const tree = $('#jstree_scene').jstree() + if (!tree) return + const sel = tree.get_selected(true)[0] + if (sel && sel.data && sel.data.uuid) { + const uuid = sel.data.uuid + ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { + el.classList.toggle('active', el.dataset.uuid === uuid) + }) // Determine if selected node is a measurement-like object; if not, restore. - const isMeasurement = sel && sel.data && isMeasurementUUID(sel.data.uuid); - if(isMeasurement){ - showPanelInMeasurements(); + const isMeasurement = + sel && sel.data && isMeasurementUUID(sel.data.uuid) + if (isMeasurement) { + showPanelInMeasurements() } else { - restorePanelToOriginal(); + restorePanelToOriginal() } } - },0); - }); + }, 0) + }) // Move existing tools UI into this section - const toolsHost = document.getElementById('measurement_tools_host'); - const existingTools = document.getElementById('tools'); - if(toolsHost && existingTools){ - toolsHost.appendChild(existingTools); + const toolsHost = document.getElementById('measurement_tools_host') + const existingTools = document.getElementById('tools') + if (toolsHost && existingTools) { + toolsHost.appendChild(existingTools) } } From 5a328f84afb1c1aad46d33c5052fa013e6dbb70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20Wahlstr=C3=B8m?= Date: Wed, 1 Oct 2025 12:56:42 +0200 Subject: [PATCH 21/46] refactor(#7): :fire: Remove link to css not in use --- index.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/index.html b/index.html index eb930b5..d0563c8 100644 --- a/index.html +++ b/index.html @@ -24,11 +24,6 @@ href="/libs/jstree/themes/mixed/style.css" /> - From d028b7eb0914321fa14fa503982396ede1a18568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Wed, 1 Oct 2025 13:18:34 +0200 Subject: [PATCH 22/46] refactor(#5): :recycle: Small cleanup --- index.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index e29d5c8..64e8991 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,7 @@ href="/libs/jstree/themes/mixed/style.css" /> + @@ -55,9 +56,9 @@ - - - + diff --git a/src/ElevationControl/elevationControl.js b/src/ElevationControl/elevationControl.js index 6a16562..c4f28ae 100644 --- a/src/ElevationControl/elevationControl.js +++ b/src/ElevationControl/elevationControl.js @@ -88,7 +88,7 @@ function moveElevationContainer() { } //initiate and orchestrate all funcitons to render the Evelation control section of the sidebar propperly -window.initElevationControls = function initElevationControls(viewer) { +export function initElevationControls(viewer) { //Creates the section createElevationPanel(viewer) diff --git a/src/potreeViewer.js b/src/potreeViewer.js index c6a20a3..06aaedf 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,3 +1,5 @@ +import { initElevationControls } from './ElevationControl/elevationControl.js' + /** * Initializes the Potree viewer used to visualize the point cloud. * @@ -31,9 +33,7 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { $('#menu_filters').next().show() viewer.toggleSidebar() - if (window.initElevationControls) { - window.initElevationControls(viewer) - } + initElevationControls(viewer) }) const e = await Potree.loadPointCloud(pointcloudUrl) From 4f44bf375fb971abfe67cb240f02ccbd610c4e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Sat, 4 Oct 2025 21:45:49 +0200 Subject: [PATCH 29/46] style(#5): :lipstick: Move inline styling to the linked css file, and ran format --- index.html | 5 +- src/ElevationControl/elevationControl.js | 1 - src/MeasurementControl/measurementsPanel.css | 34 ++++++- src/MeasurementControl/measurementsPanel.js | 99 +++++++++++++++----- 4 files changed, 110 insertions(+), 29 deletions(-) diff --git a/index.html b/index.html index c32c195..85732a9 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,10 @@ href="/libs/jstree/themes/mixed/style.css" /> - + diff --git a/src/ElevationControl/elevationControl.js b/src/ElevationControl/elevationControl.js index 276456b..a8f4ebb 100644 --- a/src/ElevationControl/elevationControl.js +++ b/src/ElevationControl/elevationControl.js @@ -82,7 +82,6 @@ function moveElevationContainer() { //initiate and orchestrate all funcitons to render the Evelation control section of the sidebar propperly window.initElevationControls = function initElevationControls(viewer) { - //Creates the section createElevationPanel(viewer) diff --git a/src/MeasurementControl/measurementsPanel.css b/src/MeasurementControl/measurementsPanel.css index 18af527..f227535 100644 --- a/src/MeasurementControl/measurementsPanel.css +++ b/src/MeasurementControl/measurementsPanel.css @@ -227,10 +227,30 @@ color: #fff; } -.m-list-title, .m-data-title { +.measurements-panel { + position: relative; +} +.measurement-tools-host { + margin-bottom: 6px; +} +.measurement-items-root { + max-height: 260px; + overflow: auto; + margin-bottom: 6px; + border: 1px solid #333; + border-radius: 4px; +} +.measurement-info-message { + padding: 4px 8px; + font-size: 12px; + opacity: 0.6; +} + +.m-list-title, +.m-data-title { font-size: 12px; font-weight: 600; - letter-spacing: .5px; + letter-spacing: 0.5px; text-transform: uppercase; padding: 6px 8px 4px 14px; margin-left: 2px; @@ -239,5 +259,11 @@ align-items: center; gap: 6px; } -.m-list-title { margin: 6px 0 4px; } -.m-data-title { margin: 10px 0 8px; border-top:1px solid #303a3f; padding-top:10px; } +.m-list-title { + margin: 6px 0 4px; +} +.m-data-title { + margin: 10px 0 8px; + border-top: 1px solid #303a3f; + padding-top: 10px; +} diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index e6b32f7..c834839 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -15,11 +15,19 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { if (menu) { const header = document.createElement('h3') header.id = 'menu_point_measurements' - header.innerHTML = 'Measurements' + const headerSpan = document.createElement('span') + headerSpan.textContent = 'Measurements' + header.appendChild(headerSpan) const panel = document.createElement('div') - panel.className = 'pv-menu-list' - panel.innerHTML = - '
' + panel.className = 'pv-menu-list measurements-panel' + const toolsHostDiv = document.createElement('div') + toolsHostDiv.id = 'measurement_tools_host' + toolsHostDiv.className = 'measurement-tools-host' + const listContainerDiv = document.createElement('div') + listContainerDiv.id = 'measurements_list' + listContainerDiv.className = 'auto' + panel.appendChild(toolsHostDiv) + panel.appendChild(listContainerDiv) // Insert before filters/tools if possible, else append at end const tools = document.getElementById('menu_tools') if (tools) { @@ -39,7 +47,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { if ($(menu).accordion && $(menu).data('uiAccordion')) return if (window.jQuery) { const $p = window.jQuery(panel) - ;($p.is(':visible') ? $p.slideUp(350) : $p.slideDown(350)) + $p.is(':visible') ? $p.slideUp(350) : $p.slideDown(350) return } }) @@ -55,8 +63,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { let listRoot = document.createElement('div') listRoot.id = 'measurement_items' - listRoot.style.cssText = - 'max-height:260px; overflow:auto; margin-bottom:6px; border:1px solid #333; border-radius:4px;' + listRoot.className = 'measurement-items-root' const listTitle = document.createElement('div') listTitle.className = 'm-list-title' listTitle.textContent = 'List of measurements' @@ -168,14 +175,20 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { ) } if (targetContainer && targetContainer.children.length === 0) { - targetContainer.innerHTML = - '
Select a measurement to view its properties here
' + targetContainer.innerHTML = '' + const msg = document.createElement('div') + msg.className = 'measurement-info-message' + msg.textContent = 'Select a measurement to view its properties here' + targetContainer.appendChild(msg) } } if (targetContainer) { - targetContainer.innerHTML = - '
Select a measurement to view its properties here
' + targetContainer.innerHTML = '' + const msg2 = document.createElement('div') + msg2.className = 'measurement-info-message' + msg2.textContent = 'Select a measurement to view its properties here' + targetContainer.appendChild(msg2) } // Helper to decide if a uuid is a measurement-like object @@ -223,16 +236,38 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { const order = Array.from(groups.keys()).sort() listRoot.innerHTML = '' if (itemsRaw.length === 0) { - listRoot.innerHTML = '
No measurements
' + const emptyEl = document.createElement('div') + emptyEl.className = 'm-empty' + emptyEl.textContent = 'No measurements' + listRoot.appendChild(emptyEl) return } order.forEach((type) => { const section = document.createElement('div') section.className = 'm-group' const gid = 'g_' + type.toLowerCase() - section.innerHTML = `\n
\n \n ${type}\n \n
\n
` + const header = document.createElement('div') + header.className = 'm-group-header' + header.dataset.group = gid + header.dataset.open = 'true' + const caret = document.createElement('span') + caret.className = 'm-group-caret' + caret.textContent = '▾' + const titleSpan = document.createElement('span') + titleSpan.className = 'm-group-title' + titleSpan.textContent = type + const countSpan = document.createElement('span') + countSpan.className = 'm-group-count' + countSpan.id = gid + '_count' + header.appendChild(caret) + header.appendChild(titleSpan) + header.appendChild(countSpan) + const body = document.createElement('div') + body.className = 'm-group-body' + body.id = gid + '_body' + section.appendChild(header) + section.appendChild(body) listRoot.appendChild(section) - const body = section.querySelector('.m-group-body') groups.get(type).forEach((entry) => { const m = entry.obj const num = perTypeNumbers.get(type)?.get(m.uuid) || 0 @@ -240,14 +275,24 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { const row = document.createElement('div') row.className = 'm-row' row.dataset.uuid = m.uuid - const icon = TYPE_ICONS[type] || TYPE_ICONS['Measurement'] - row.innerHTML = - `${icon}${baseName}` + - '' + const iconSpan = document.createElement('span') + iconSpan.className = 'm-row-icon' + iconSpan.textContent = TYPE_ICONS[type] || TYPE_ICONS['Measurement'] + const labelSpan = document.createElement('span') + labelSpan.className = 'm-row-label' + labelSpan.title = baseName + labelSpan.textContent = baseName + const delBtn = document.createElement('button') + delBtn.className = 'm-row-delete' + delBtn.dataset.act = 'delete' + delBtn.title = 'Delete' + delBtn.textContent = '×' + row.appendChild(iconSpan) + row.appendChild(labelSpan) + row.appendChild(delBtn) body.appendChild(row) }) - section.querySelector('#' + gid + '_count').textContent = - groups.get(type).length + countSpan.textContent = groups.get(type).length }) } @@ -351,7 +396,9 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { el.classList.toggle('active', el.dataset.uuid === uuid) }) - const thisRow = listRoot.querySelector(`.m-row[data-uuid="${uuid}"] .m-row-label`) + const thisRow = listRoot.querySelector( + `.m-row[data-uuid="${uuid}"] .m-row-label` + ) if (thisRow) { lastSelection.uuid = uuid lastSelection.label = thisRow.textContent.trim() @@ -372,7 +419,9 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { el.classList.toggle('active', el.dataset.uuid === uuid) }) - const activeRow = listRoot.querySelector(`.m-row[data-uuid="${uuid}"] .m-row-label`) + const activeRow = listRoot.querySelector( + `.m-row[data-uuid="${uuid}"] .m-row-label` + ) if (activeRow) { lastSelection.uuid = uuid lastSelection.label = activeRow.textContent.trim() @@ -403,7 +452,11 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { const measLi = measOptions.closest('li') || measOptions let prev = measLi.previousElementSibling for (let i = 0; i < 3 && prev; i++) { - if (prev.classList && prev.classList.contains('divider') && /Measurement/i.test(prev.textContent || '')) { + if ( + prev.classList && + prev.classList.contains('divider') && + /Measurement/i.test(prev.textContent || '') + ) { prev.remove() break } From e1175caa0a82e6be209a5f6f5a544ca1a085c687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Sat, 4 Oct 2025 22:02:45 +0200 Subject: [PATCH 30/46] refactor(#5): :recycle: Made a helper function to reduce redundancy --- src/MeasurementControl/measurementsPanel.js | 41 +++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index c834839..dfc173e 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -202,6 +202,25 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { ) } + // Update visual active state in list and track lastSelection label + function updateActiveSelection(uuid) { + ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { + el.classList.toggle('active', el.dataset.uuid === uuid) + }) + if (uuid) { + const labelEl = listRoot.querySelector( + `.m-row[data-uuid="${uuid}"] .m-row-label` + ) + if (labelEl) { + lastSelection.uuid = uuid + lastSelection.label = labelEl.textContent.trim() + } + } else { + lastSelection.uuid = null + lastSelection.label = '' + } + } + // If on load there's already a selected measurement, move panel immediately setTimeout(() => { try { @@ -393,16 +412,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { $.jstree.reference(node.id).select_node(node.id) } } - ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { - el.classList.toggle('active', el.dataset.uuid === uuid) - }) - const thisRow = listRoot.querySelector( - `.m-row[data-uuid="${uuid}"] .m-row-label` - ) - if (thisRow) { - lastSelection.uuid = uuid - lastSelection.label = thisRow.textContent.trim() - } + updateActiveSelection(uuid) showPanelInMeasurements() } }) @@ -416,16 +426,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { const sel = tree.get_selected(true)[0] if (sel && sel.data && sel.data.uuid) { const uuid = sel.data.uuid - ;[...listRoot.querySelectorAll('.m-row[data-uuid]')].forEach((el) => { - el.classList.toggle('active', el.dataset.uuid === uuid) - }) - const activeRow = listRoot.querySelector( - `.m-row[data-uuid="${uuid}"] .m-row-label` - ) - if (activeRow) { - lastSelection.uuid = uuid - lastSelection.label = activeRow.textContent.trim() - } + updateActiveSelection(uuid) // Determine if selected node is a measurement-like object; if not, restore. const isMeasurement = sel && sel.data && isMeasurementUUID(sel.data.uuid) From e575fbf4cf225ecb4d36736199d8850522a16f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Sat, 4 Oct 2025 22:48:30 +0200 Subject: [PATCH 31/46] fix(#5): :bug: fixed issues where the title for displayed data was not updated --- src/MeasurementControl/measurementsPanel.js | 147 +++++++++++++++++--- 1 file changed, 128 insertions(+), 19 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index dfc173e..d9cdb46 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -313,17 +313,43 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { }) countSpan.textContent = groups.get(type).length }) + if (originalPropertiesPanel.parentElement === targetContainer) { + let selectedUUID = null + try { + const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() + if (tree) { + const sel = tree.get_selected(true)[0] + sel && + sel.data && + isMeasurementUUID(sel.data.uuid) && + (selectedUUID = sel.data.uuid) + } + } catch (_e) {} + + const titleEl = targetContainer.querySelector('#measurement_data_title') + if ( + selectedUUID && + listRoot.querySelector(`.m-row[data-uuid="${selectedUUID}"]`) + ) { + // Update lastSelection + active row highlight + updateActiveSelection(selectedUUID) + if (titleEl) titleEl.textContent = `Data for ${lastSelection.label}` + } else { + lastSelection.uuid = null + lastSelection.label = '' + if (titleEl) titleEl.textContent = 'Measurement Data' + } + } } rebuildMeasurementList() // Hook into scene add/remove events to refresh list - viewer.scene.addEventListener('measurement_added', (e) => { - const obj = e.measurement || e.object || e.detail || null - // Some measurements start as a point, then become distance or height when adding more points to it. This is a listener for that. - if (obj && obj.addEventListener && !obj._mp_listenersAttached) { + function handleMeasurementLikeAdded(obj) { + if (!obj) return + // Attach point/marker listeners for any measurement-like object with points + if (obj.addEventListener && !obj._mp_listenersAttached) { obj._mp_listenersAttached = true - // Potree measurement objects usually fire their own events; fallback to polling if needed ;[ 'marker_added', 'marker_removed', @@ -331,29 +357,112 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { 'point_removed' ].forEach((ev) => { try { - obj.addEventListener(ev, () => rebuildMeasurementList()) + obj.addEventListener(ev, () => { + rebuildMeasurementList() + if (lastSelection.uuid === obj.uuid) { + updateActiveSelection(obj.uuid) + showPanelInMeasurements() + } + }) } catch (_e) {} }) } + + // Distance/Height/Angle measurements can change type based on point count + if (!obj._mp_typeWatcher && obj.points) { + obj._mp_typeWatcher = true + let lastType = resolveType(obj) + const watcher = setInterval(() => { + const currentType = resolveType(obj) + if (currentType !== lastType) { + lastType = currentType + rebuildMeasurementList() + if (lastSelection.uuid === obj.uuid) { + updateActiveSelection(obj.uuid) + showPanelInMeasurements() + } + } + if (currentType !== 'Point' || (obj.points && obj.points.length > 1)) { + clearInterval(watcher) + } + }, 250) + } rebuildMeasurementList() - if (obj && isMeasurementUUID(obj.uuid)) { - showPanelInMeasurements() - } else { + if (!isMeasurementUUID(obj.uuid)) return + let autoSelect = false + try { const tree = $('#jstree_scene').jstree && $('#jstree_scene').jstree() if (tree) { const sel = tree.get_selected(true)[0] - if (sel && sel.data && isMeasurementUUID(sel.data.uuid)) { - showPanelInMeasurements() - } + if (!sel || !sel.data || !isMeasurementUUID(sel.data.uuid)) + autoSelect = true } + } catch (_e) {} + if (autoSelect) { + try { + const treeEl = document.getElementById('jstree_scene') + if (treeEl && $(treeEl).jstree) { + const tree = $(treeEl).jstree() + const measurementsRoot = tree.get_json('measurements') + const findNode = (root) => + root.children.find((ch) => ch.data && ch.data.uuid === obj.uuid) + const node = measurementsRoot && findNode(measurementsRoot) + if (node) { + $.jstree.reference(node.id).deselect_all() + $.jstree.reference(node.id).select_node(node.id) + updateActiveSelection(obj.uuid) + } + } + } catch (_e) {} + } else { + updateActiveSelection(obj.uuid) + } + showPanelInMeasurements() + } + + viewer.scene.addEventListener('measurement_added', (e) => { + const obj = e.measurement || e.object || e.detail || null + handleMeasurementLikeAdded(obj) + }) + viewer.scene.addEventListener('volume_added', (e) => { + const obj = e.volume || e.object || e.detail || null + handleMeasurementLikeAdded(obj) + }) + viewer.scene.addEventListener('profile_added', (e) => { + const obj = e.profile || e.object || e.detail || null + handleMeasurementLikeAdded(obj) + }) + + viewer.scene.addEventListener('measurement_removed', (e) => { + rebuildMeasurementList() + const removed = e.measurement || e.object || e.detail || null + if (removed && removed.uuid && removed.uuid === lastSelection.uuid) { + lastSelection.uuid = null + lastSelection.label = '' + const titleEl = targetContainer.querySelector('#measurement_data_title') + if (titleEl) titleEl.textContent = 'Measurement Data' + } + }) + viewer.scene.addEventListener('volume_removed', (e) => { + rebuildMeasurementList() + const removed = e.volume || e.object || e.detail || null + if (removed && removed.uuid && removed.uuid === lastSelection.uuid) { + lastSelection.uuid = null + lastSelection.label = '' + const titleEl = targetContainer.querySelector('#measurement_data_title') + if (titleEl) titleEl.textContent = 'Measurement Data' + } + }) + viewer.scene.addEventListener('profile_removed', (e) => { + rebuildMeasurementList() + const removed = e.profile || e.object || e.detail || null + if (removed && removed.uuid && removed.uuid === lastSelection.uuid) { + lastSelection.uuid = null + lastSelection.label = '' + const titleEl = targetContainer.querySelector('#measurement_data_title') + if (titleEl) titleEl.textContent = 'Measurement Data' } }) - //volume and profile have their own event handlers and wont be included in the basic "measurement_added" - viewer.scene.addEventListener('measurement_removed', rebuildMeasurementList) - viewer.scene.addEventListener('volume_added', rebuildMeasurementList) - viewer.scene.addEventListener('volume_removed', rebuildMeasurementList) - viewer.scene.addEventListener('profile_added', rebuildMeasurementList) - viewer.scene.addEventListener('profile_removed', rebuildMeasurementList) // Click handling for selection, focus and delete listRoot.addEventListener('click', (e) => { @@ -412,7 +521,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { $.jstree.reference(node.id).select_node(node.id) } } - updateActiveSelection(uuid) + updateActiveSelection(uuid) showPanelInMeasurements() } }) From 2b8f6b68b2f45471bb87015541200c3a487916aa Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Sun, 5 Oct 2025 19:09:08 +0200 Subject: [PATCH 32/46] docs(#9): add some explanatory comments --- src/potreeViewer.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 06aaedf..3f04686 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -56,9 +56,11 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { pc.material.gradient = Potree.Gradients['VIRIDIS'] e.pointcloud.projection = '+proj=geocent +datum=WGS84 +units=m +no_defs' + + // Initialize camera position and target point (manually chosen) viewer.scene.view.setView( - [1993552.9, 87954.487, 7134018.721], - [1184471.63, 63828.49, 6243615.52] + [1993552.9, 87954.487, 7134018.721], // Initial camera position + [1184471.63, 63828.49, 6243615.52] // Initial target point ) return viewer @@ -96,20 +98,30 @@ function overrideShaderForGradient(pc) { /vec3 getElevation\(\)[\s\S]*?\}/, ` vec3 getElevation(){ + // Transform the vertex position into world coordinates vec4 world = modelMatrix * vec4(position, 1.0); + + // Compute distance from Earth's center and latitude float radius = length(world.xyz); float latitude = asin(world.z / radius); - const float a = 6378137.0; - const float b = 6356752.3; + const float a = 6378137.0; // Equatorial radius + const float b = 6356752.3; // Polar radius + // Compute distance from Earth's center to the surface at the given latitude float cosLat = cos(latitude); float sinLat = sin(latitude); float numerator = (a*a * cosLat) * (a*a * cosLat) + (b*b * sinLat) * (b*b * sinLat); float denominator = (a * cosLat) * (a * cosLat) + (b * sinLat) * (b * sinLat); float radiusAtLatitude = sqrt(numerator / denominator); - float w = (radius - radiusAtLatitude - elevationRange.x) / (elevationRange.y - elevationRange.x); + // Compute depth below the ellipsoid (sea level) + float depth = radius - radiusAtLatitude; + + // Normalize depth to a [0, 1] range for coloring + float w = (depth - elevationRange.x) / (elevationRange.y - elevationRange.x); + + // Sample color from gradient texture based on normalized depth return texture2D(gradient, vec2(w, 1.0-w)).rgb; } ` From 66aa93453f50266634cff95dab584c9eebce4814 Mon Sep 17 00:00:00 2001 From: Adrian Solberg Date: Sun, 5 Oct 2025 19:11:37 +0200 Subject: [PATCH 33/46] chore(#9): remove old potree background image --- index.html | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 09b18c3..432ff20 100644 --- a/index.html +++ b/index.html @@ -49,23 +49,17 @@
-
-
+
+
From fd23c74f46d6706408498e979b6fd961efe34973 Mon Sep 17 00:00:00 2001 From: Kleinc Date: Sun, 5 Oct 2025 20:03:41 +0200 Subject: [PATCH 34/46] refactor(#10): Changed to new coordinate system, generalized code The code is now refactored to use the new coordinate system. It is also generalized more to not look as messy as before. --- src/coordinateShowing.js | 89 ++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/src/coordinateShowing.js b/src/coordinateShowing.js index 23ee291..8bd0587 100644 --- a/src/coordinateShowing.js +++ b/src/coordinateShowing.js @@ -1,12 +1,11 @@ -// EPSG:32633 (WGS84 / UTM zone 33N) to WGS84 (lon/lat) -const utm33 = "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs"; //UTM zone 33N -const wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; //WGS84 is the current standard for GPS coordinates - -const posCanvas = document.getElementById('posCanvas'); -let posCtx = posCanvas.getContext('2d'); +const ecef = "+proj=geocent +datum=WGS84 +units=m +no_defs"; // EPSG:4978 (geocentric coordinates) +const wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; // EPSG:4326 (geographic coordinates) +const posCanvas = document.getElementById('posCanvas'); // lat/lon const elevationCanvas = document.getElementById('elevationCanvas'); -let elevationCtx = elevationCanvas.getContext('2d'); + +let posCtx; +let elevationCtx; /** * Resizes the canvas and its context to account for device pixel ratio. @@ -17,7 +16,6 @@ function resizeCanvas(canvas) { const dpr = window.devicePixelRatio || 1; const ctx = canvas.getContext('2d'); - // Set canvas internal size canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr; @@ -27,56 +25,59 @@ function resizeCanvas(canvas) { } /** - * Updates the camera overlay with the current latitude and longitude. + * Draw the text on a given canvas. + */ +function drawText(ctx, text, canvas) { + const centerX = canvas.clientWidth / 2; + const centerY = canvas.clientHeight / 2; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#cccccc'; + ctx.font = '20px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(text, centerX, centerY); +} + +/** + * Updates the lat/lon position. */ -function updateCameraOverlay() { +function updatePosition() { const cam = window.viewer.scene.view.position; - const [lon, lat] = proj4(utm33, wgs84, [cam.x, cam.y]); // Conversion using proj4js library - const centerX = posCanvas.clientWidth / 2; - const centerY = posCanvas.clientHeight / 2; - - posCtx.clearRect(0, 0, posCanvas.width, posCanvas.height); - posCtx.fillStyle = '#cccccc'; - posCtx.font = '20px Arial'; - posCtx.textAlign = "center"; - posCtx.textBaseline = "middle"; - posCtx.fillText(`lat = ${lat.toFixed(2)}˚ lon = ${lon.toFixed(2)}˚`, centerX, centerY); + const [lon, lat] = proj4(ecef, wgs84, [cam.x, cam.y, cam.z]); + drawText(posCtx, `lat = ${lat.toFixed(2)}˚ lon = ${lon.toFixed(2)}˚`, posCanvas); } -/** - * Displays the target elevation when in orbit mode. +/** + * Shows target elevations if camera is in orbit mode. */ -function targetElevation() { +function updateTargetElevation() { const pivot = window.viewer.scene.view.getPivot(); - const mode = window.viewer.getControls(); - - const centerX = elevationCanvas.clientWidth / 2; - const centerY = elevationCanvas.clientHeight / 2; + const controls = window.viewer.getControls(); + const height = proj4(ecef, wgs84, [pivot.x, pivot.y, pivot.z])[2]; - if (mode === window.viewer.orbitControls) { + if (controls === window.viewer.orbitControls) { elevationCanvas.style.display = 'inline'; - elevationCtx.clearRect(0, 0, elevationCanvas.width, elevationCanvas.height); - elevationCtx.fillStyle = '#cccccc'; - elevationCtx.font = '20px Arial'; - elevationCtx.textAlign = "center"; - elevationCtx.textBaseline = "middle"; - elevationCtx.fillText(`Target elevation = ${pivot.z.toFixed(2)}m`, centerX, centerY); - } - else{ + drawText(elevationCtx, `Target elevation = ${height.toFixed(2)}m`, elevationCanvas); + } else { elevationCanvas.style.display = 'none'; } } -viewer.addEventListener("update", targetElevation); -viewer.addEventListener("update", updateCameraOverlay); - -posCtx = resizeCanvas(posCanvas); -elevationCtx = resizeCanvas(elevationCanvas); - -window.addEventListener('resize', () => { +function init() { posCtx = resizeCanvas(posCanvas); elevationCtx = resizeCanvas(elevationCanvas); -}); + + viewer.addEventListener("update", updatePosition); + viewer.addEventListener("update", updateTargetElevation); + + window.addEventListener('resize', () => { + posCtx = resizeCanvas(posCanvas); + elevationCtx = resizeCanvas(elevationCanvas); + }); +} + +init(); + From 22e2c246484dd0ebc20d958628f163f833a7eeb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Sun, 5 Oct 2025 23:23:12 +0200 Subject: [PATCH 35/46] fix(#5): :bug: Fix UI title displays and some style changes --- src/MeasurementControl/measurementsPanel.css | 6 +- src/MeasurementControl/measurementsPanel.js | 78 +++++++++++++------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.css b/src/MeasurementControl/measurementsPanel.css index f227535..96f735f 100644 --- a/src/MeasurementControl/measurementsPanel.css +++ b/src/MeasurementControl/measurementsPanel.css @@ -231,7 +231,7 @@ position: relative; } .measurement-tools-host { - margin-bottom: 6px; + margin-bottom: 32px; } .measurement-items-root { max-height: 260px; @@ -259,6 +259,10 @@ align-items: center; gap: 6px; } +.measurement-options-block { + margin-top: 12px !important; + display: block; +} .m-list-title { margin: 6px 0 4px; } diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index d9cdb46..66cc769 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -64,11 +64,13 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { let listRoot = document.createElement('div') listRoot.id = 'measurement_items' listRoot.className = 'measurement-items-root' - const listTitle = document.createElement('div') - listTitle.className = 'm-list-title' - listTitle.textContent = 'List of measurements' + const listDivider = document.createElement('div') + listDivider.className = 'divider' + const dividerSpan = document.createElement('span') + dividerSpan.textContent = 'List of Measurements' + listDivider.appendChild(dividerSpan) if (targetContainer.parentElement) { - targetContainer.parentElement.insertBefore(listTitle, targetContainer) + targetContainer.parentElement.insertBefore(listDivider, targetContainer) targetContainer.parentElement.insertBefore(listRoot, targetContainer) } @@ -150,20 +152,49 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { function showPanelInMeasurements() { ensurePlaceholder() if (!originalPropertiesPanel) return - const desiredTitle = lastSelection.label - ? `Data for ${lastSelection.label}` - : 'Measurement Data' + const hasSelection = !!lastSelection.label if (originalPropertiesPanel.parentElement !== targetContainer) { targetContainer.innerHTML = '' - const titleEl = document.createElement('div') - titleEl.id = 'measurement_data_title' - titleEl.className = 'm-data-title' - titleEl.textContent = desiredTitle - targetContainer.appendChild(titleEl) + if (hasSelection) { + const titleEl = document.createElement('div') + titleEl.id = 'measurement_data_title' + titleEl.className = 'm-data-title' + titleEl.textContent = `Data for ${lastSelection.label}` + targetContainer.appendChild(titleEl) + } else { + const msg = document.createElement('div') + msg.className = 'measurement-info-message' + msg.textContent = 'Select a measurement to view its properties here' + targetContainer.appendChild(msg) + } targetContainer.appendChild(originalPropertiesPanel) } else { - const titleEl = targetContainer.querySelector('#measurement_data_title') - if (titleEl) titleEl.textContent = desiredTitle + const existingTitle = targetContainer.querySelector( + '#measurement_data_title' + ) + const existingInfo = targetContainer.querySelector( + '.measurement-info-message' + ) + if (hasSelection) { + if (!existingTitle) { + if (existingInfo) existingInfo.remove() + const titleEl = document.createElement('div') + titleEl.id = 'measurement_data_title' + titleEl.className = 'm-data-title' + titleEl.textContent = `Data for ${lastSelection.label}` + targetContainer.insertBefore(titleEl, originalPropertiesPanel) + } else { + existingTitle.textContent = `Data for ${lastSelection.label}` + } + } else { + if (existingTitle) existingTitle.remove() + if (!existingInfo) { + const msg = document.createElement('div') + msg.className = 'measurement-info-message' + msg.textContent = 'Select a measurement to view its properties here' + targetContainer.insertBefore(msg, originalPropertiesPanel) + } + } } } function restorePanelToOriginal() { @@ -325,19 +356,16 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { (selectedUUID = sel.data.uuid) } } catch (_e) {} - - const titleEl = targetContainer.querySelector('#measurement_data_title') if ( selectedUUID && listRoot.querySelector(`.m-row[data-uuid="${selectedUUID}"]`) ) { - // Update lastSelection + active row highlight updateActiveSelection(selectedUUID) - if (titleEl) titleEl.textContent = `Data for ${lastSelection.label}` + lastSelection.label && showPanelInMeasurements() } else { lastSelection.uuid = null lastSelection.label = '' - if (titleEl) titleEl.textContent = 'Measurement Data' + showPanelInMeasurements() } } } @@ -439,8 +467,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { if (removed && removed.uuid && removed.uuid === lastSelection.uuid) { lastSelection.uuid = null lastSelection.label = '' - const titleEl = targetContainer.querySelector('#measurement_data_title') - if (titleEl) titleEl.textContent = 'Measurement Data' + showPanelInMeasurements() } }) viewer.scene.addEventListener('volume_removed', (e) => { @@ -449,8 +476,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { if (removed && removed.uuid && removed.uuid === lastSelection.uuid) { lastSelection.uuid = null lastSelection.label = '' - const titleEl = targetContainer.querySelector('#measurement_data_title') - if (titleEl) titleEl.textContent = 'Measurement Data' + showPanelInMeasurements() } }) viewer.scene.addEventListener('profile_removed', (e) => { @@ -459,8 +485,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { if (removed && removed.uuid && removed.uuid === lastSelection.uuid) { lastSelection.uuid = null lastSelection.label = '' - const titleEl = targetContainer.querySelector('#measurement_data_title') - if (titleEl) titleEl.textContent = 'Measurement Data' + showPanelInMeasurements() } }) @@ -560,6 +585,9 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { const measOptions = document.getElementById('measurement_options_show') if (measOptions) { const measLi = measOptions.closest('li') || measOptions + if (measLi && !measLi.classList.contains('measurement-options-block')) { + measLi.classList.add('measurement-options-block') + } let prev = measLi.previousElementSibling for (let i = 0; i < 3 && prev; i++) { if ( From 3fd1d98c2be75b2a713f9037c9f863d458e307fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Sun, 5 Oct 2025 23:42:49 +0200 Subject: [PATCH 36/46] style(#5): :lipstick: Added a title above the tools --- src/MeasurementControl/measurementsPanel.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 66cc769..41b250b 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -577,6 +577,15 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { const toolsHost = document.getElementById('measurement_tools_host') const existingTools = document.getElementById('tools') if (toolsHost && existingTools) { + const parentPanel = toolsHost.parentElement + if (parentPanel) { + const toolsDivider = document.createElement('div') + toolsDivider.className = 'divider pv-tools-divider' + const span = document.createElement('span') + span.textContent = 'Tools' + toolsDivider.appendChild(span) + parentPanel.insertBefore(toolsDivider, toolsHost) + } toolsHost.appendChild(existingTools) } From 33c96bea76a3d445d150d1898fd3a408e85f66b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Sun, 5 Oct 2025 23:43:10 +0200 Subject: [PATCH 37/46] ran format --- src/MeasurementControl/measurementsPanel.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 41b250b..0e66fe5 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -579,12 +579,12 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { if (toolsHost && existingTools) { const parentPanel = toolsHost.parentElement if (parentPanel) { - const toolsDivider = document.createElement('div') - toolsDivider.className = 'divider pv-tools-divider' - const span = document.createElement('span') - span.textContent = 'Tools' - toolsDivider.appendChild(span) - parentPanel.insertBefore(toolsDivider, toolsHost) + const toolsDivider = document.createElement('div') + toolsDivider.className = 'divider pv-tools-divider' + const span = document.createElement('span') + span.textContent = 'Tools' + toolsDivider.appendChild(span) + parentPanel.insertBefore(toolsDivider, toolsHost) } toolsHost.appendChild(existingTools) } From cbe01f879bb4ec84bb6be8bc6aa73a07fe0d2330 Mon Sep 17 00:00:00 2001 From: Kleinc Date: Mon, 6 Oct 2025 08:52:07 +0200 Subject: [PATCH 38/46] refactor(#10): moved init function to main --- index.html | 3 +- .../coordinateShowing.css | 0 .../coordinateShowing.js | 29 ++++++++----------- src/main.js | 12 ++++++++ 4 files changed, 25 insertions(+), 19 deletions(-) rename src/{ => coordinateShowing}/coordinateShowing.css (100%) rename src/{ => coordinateShowing}/coordinateShowing.js (84%) diff --git a/index.html b/index.html index 98d1737..cc1443f 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,7 @@ href="/libs/jstree/themes/mixed/style.css" /> - + @@ -97,6 +97,5 @@ }) - diff --git a/src/coordinateShowing.css b/src/coordinateShowing/coordinateShowing.css similarity index 100% rename from src/coordinateShowing.css rename to src/coordinateShowing/coordinateShowing.css diff --git a/src/coordinateShowing.js b/src/coordinateShowing/coordinateShowing.js similarity index 84% rename from src/coordinateShowing.js rename to src/coordinateShowing/coordinateShowing.js index 8bd0587..8ec2d6a 100644 --- a/src/coordinateShowing.js +++ b/src/coordinateShowing/coordinateShowing.js @@ -4,8 +4,16 @@ const wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; // EPSG:4326 (geogr const posCanvas = document.getElementById('posCanvas'); // lat/lon const elevationCanvas = document.getElementById('elevationCanvas'); -let posCtx; -let elevationCtx; +export let posCtx; +export let elevationCtx; + +/** + * Initializes the canvases and their contexts. + */ +export function initCanvases() { + posCtx = resizeCanvas(posCanvas); + elevationCtx = resizeCanvas(elevationCanvas); +} /** * Resizes the canvas and its context to account for device pixel ratio. @@ -41,7 +49,7 @@ function drawText(ctx, text, canvas) { /** * Updates the lat/lon position. */ -function updatePosition() { +export function updatePosition() { const cam = window.viewer.scene.view.position; const [lon, lat] = proj4(ecef, wgs84, [cam.x, cam.y, cam.z]); drawText(posCtx, `lat = ${lat.toFixed(2)}˚ lon = ${lon.toFixed(2)}˚`, posCanvas); @@ -50,7 +58,7 @@ function updatePosition() { /** * Shows target elevations if camera is in orbit mode. */ -function updateTargetElevation() { +export function updateTargetElevation() { const pivot = window.viewer.scene.view.getPivot(); const controls = window.viewer.getControls(); const height = proj4(ecef, wgs84, [pivot.x, pivot.y, pivot.z])[2]; @@ -64,20 +72,7 @@ function updateTargetElevation() { } -function init() { - posCtx = resizeCanvas(posCanvas); - elevationCtx = resizeCanvas(elevationCanvas); - - viewer.addEventListener("update", updatePosition); - viewer.addEventListener("update", updateTargetElevation); - - window.addEventListener('resize', () => { - posCtx = resizeCanvas(posCanvas); - elevationCtx = resizeCanvas(elevationCanvas); - }); -} -init(); diff --git a/src/main.js b/src/main.js index 8b13789..2355872 100644 --- a/src/main.js +++ b/src/main.js @@ -1 +1,13 @@ +import { initCanvases, updatePosition, updateTargetElevation } from "./coordinateShowing/coordinateShowing.js"; + +function init() { + initCanvases(); + + viewer.addEventListener("update", updatePosition); + viewer.addEventListener("update", updateTargetElevation); + + window.addEventListener('resize', initCanvases); +} + +init(); From 0b5b038ec5433ad9e31affa1788f14dfcd5cc989 Mon Sep 17 00:00:00 2001 From: Kleinc Date: Mon, 6 Oct 2025 10:59:54 +0200 Subject: [PATCH 39/46] refactor(#10): Added more decimals to lat/lon and elevation target Added more decimals to lat/lon and elevation target for more precission --- src/coordinateShowing/coordinateShowing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coordinateShowing/coordinateShowing.js b/src/coordinateShowing/coordinateShowing.js index 8ec2d6a..941afb3 100644 --- a/src/coordinateShowing/coordinateShowing.js +++ b/src/coordinateShowing/coordinateShowing.js @@ -52,7 +52,7 @@ function drawText(ctx, text, canvas) { export function updatePosition() { const cam = window.viewer.scene.view.position; const [lon, lat] = proj4(ecef, wgs84, [cam.x, cam.y, cam.z]); - drawText(posCtx, `lat = ${lat.toFixed(2)}˚ lon = ${lon.toFixed(2)}˚`, posCanvas); + drawText(posCtx, `lat = ${lat.toFixed(5)}˚ lon = ${lon.toFixed(5)}˚`, posCanvas); } /** @@ -65,7 +65,7 @@ export function updateTargetElevation() { if (controls === window.viewer.orbitControls) { elevationCanvas.style.display = 'inline'; - drawText(elevationCtx, `Target elevation = ${height.toFixed(2)}m`, elevationCanvas); + drawText(elevationCtx, `Target elevation = ${height.toFixed(4)}m`, elevationCanvas); } else { elevationCanvas.style.display = 'none'; } From 74016ea9a276bbb019e9bb95962025fcec2108a3 Mon Sep 17 00:00:00 2001 From: Kleinc Date: Mon, 6 Oct 2025 11:15:19 +0200 Subject: [PATCH 40/46] refactor(#10): Fix merge conflict and apply new changes --- src/coordinateShowing/coordinateShowing.css | 11 ++- src/coordinateShowing/coordinateShowing.js | 88 +++++++++++---------- src/main.js | 15 ++-- 3 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/coordinateShowing/coordinateShowing.css b/src/coordinateShowing/coordinateShowing.css index da616a2..07f2133 100644 --- a/src/coordinateShowing/coordinateShowing.css +++ b/src/coordinateShowing/coordinateShowing.css @@ -1,12 +1,11 @@ - #posCanvas { position: absolute; left: 10px; bottom: 10px; width: 300px; height: 50px; - pointer-events: none; - background-color: #19282C; + pointer-events: none; + background-color: #19282c; z-index: 10; border-radius: 5px; } @@ -17,8 +16,8 @@ top: 15px; width: 300px; height: 50px; - pointer-events: none; - background-color: #19282C; + pointer-events: none; + background-color: #19282c; z-index: 10; border-radius: 5px; -} \ No newline at end of file +} diff --git a/src/coordinateShowing/coordinateShowing.js b/src/coordinateShowing/coordinateShowing.js index 941afb3..7019138 100644 --- a/src/coordinateShowing/coordinateShowing.js +++ b/src/coordinateShowing/coordinateShowing.js @@ -1,18 +1,18 @@ -const ecef = "+proj=geocent +datum=WGS84 +units=m +no_defs"; // EPSG:4978 (geocentric coordinates) -const wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; // EPSG:4326 (geographic coordinates) +const ecef = '+proj=geocent +datum=WGS84 +units=m +no_defs' // EPSG:4978 (geocentric coordinates) +const wgs84 = '+proj=longlat +datum=WGS84 +no_defs' // EPSG:4326 (geographic coordinates) -const posCanvas = document.getElementById('posCanvas'); // lat/lon -const elevationCanvas = document.getElementById('elevationCanvas'); +const posCanvas = document.getElementById('posCanvas') // lat/lon +const elevationCanvas = document.getElementById('elevationCanvas') -export let posCtx; -export let elevationCtx; +export let posCtx +export let elevationCtx -/** +/** * Initializes the canvases and their contexts. */ export function initCanvases() { - posCtx = resizeCanvas(posCanvas); - elevationCtx = resizeCanvas(elevationCanvas); + posCtx = resizeCanvas(posCanvas) + elevationCtx = resizeCanvas(elevationCanvas) } /** @@ -21,58 +21,60 @@ export function initCanvases() { * @returns {*} - The resized canvas context. */ function resizeCanvas(canvas) { - const dpr = window.devicePixelRatio || 1; - const ctx = canvas.getContext('2d'); + const dpr = window.devicePixelRatio || 1 + const ctx = canvas.getContext('2d') - canvas.width = canvas.clientWidth * dpr; - canvas.height = canvas.clientHeight * dpr; + canvas.width = canvas.clientWidth * dpr + canvas.height = canvas.clientHeight * dpr - // Scale context so drawing uses CSS pixels - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - return ctx; + // Scale context so drawing uses CSS pixels + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + return ctx } /** * Draw the text on a given canvas. */ function drawText(ctx, text, canvas) { - const centerX = canvas.clientWidth / 2; - const centerY = canvas.clientHeight / 2; - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = '#cccccc'; - ctx.font = '20px Arial'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(text, centerX, centerY); + const centerX = canvas.clientWidth / 2 + const centerY = canvas.clientHeight / 2 + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.fillStyle = '#cccccc' + ctx.font = '20px Arial' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(text, centerX, centerY) } /** * Updates the lat/lon position. */ export function updatePosition() { - const cam = window.viewer.scene.view.position; - const [lon, lat] = proj4(ecef, wgs84, [cam.x, cam.y, cam.z]); - drawText(posCtx, `lat = ${lat.toFixed(5)}˚ lon = ${lon.toFixed(5)}˚`, posCanvas); + const cam = window.potreeViewer.scene.view.position + const [lon, lat] = proj4(ecef, wgs84, [cam.x, cam.y, cam.z]) + drawText( + posCtx, + `lat = ${lat.toFixed(5)}˚ lon = ${lon.toFixed(5)}˚`, + posCanvas + ) } /** * Shows target elevations if camera is in orbit mode. */ export function updateTargetElevation() { - const pivot = window.viewer.scene.view.getPivot(); - const controls = window.viewer.getControls(); - const height = proj4(ecef, wgs84, [pivot.x, pivot.y, pivot.z])[2]; - - if (controls === window.viewer.orbitControls) { - elevationCanvas.style.display = 'inline'; - drawText(elevationCtx, `Target elevation = ${height.toFixed(4)}m`, elevationCanvas); - } else { - elevationCanvas.style.display = 'none'; - } + const pivot = window.potreeViewer.scene.view.getPivot() + const controls = window.potreeViewer.getControls() + const height = proj4(ecef, wgs84, [pivot.x, pivot.y, pivot.z])[2] + + if (controls === window.potreeViewer.orbitControls) { + elevationCanvas.style.display = 'inline' + drawText( + elevationCtx, + `Target elevation = ${height.toFixed(4)}m`, + elevationCanvas + ) + } else { + elevationCanvas.style.display = 'none' + } } - - - - - - diff --git a/src/main.js b/src/main.js index f736949..e1670e1 100644 --- a/src/main.js +++ b/src/main.js @@ -2,8 +2,11 @@ import { POTREE_POINTCLOUD_URL, POTREE_SETTINGS } from './config.js' import { createCesiumViewer } from './cesiumViewer.js' import { createPotreeViewer } from './potreeViewer.js' import { syncCameras } from './cameraSync.js' -import { initCanvases, updatePosition, updateTargetElevation } from "./coordinateShowing/coordinateShowing.js"; - +import { + initCanvases, + updatePosition, + updateTargetElevation +} from './coordinateShowing/coordinateShowing.js' async function init() { window.cesiumViewer = createCesiumViewer('cesiumContainer') @@ -14,12 +17,12 @@ async function init() { POTREE_SETTINGS ) - initCanvases(); + initCanvases() - viewer.addEventListener("update", updatePosition); - viewer.addEventListener("update", updateTargetElevation); + potreeViewer.addEventListener('update', updatePosition) + potreeViewer.addEventListener('update', updateTargetElevation) - window.addEventListener('resize', initCanvases); + window.addEventListener('resize', initCanvases) function loop(timestamp) { requestAnimationFrame(loop) From b24f3aaa4ca4790b26a55fd8dc8bf1d3ca9ea215 Mon Sep 17 00:00:00 2001 From: Kleinc Date: Mon, 6 Oct 2025 11:54:56 +0200 Subject: [PATCH 41/46] refactor(#10): Refactored code according to comments --- index.html | 6 ++++-- src/config.js | 3 +++ src/coordinateShowing/coordinateShowing.css | 18 +++++++++++------- src/coordinateShowing/coordinateShowing.js | 10 +++++----- src/main.js | 12 ++++++------ src/potreeViewer.js | 3 ++- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/index.html b/index.html index 73306f0..695a3bc 100644 --- a/index.html +++ b/index.html @@ -67,8 +67,10 @@ >
- - +
+ + +
diff --git a/src/config.js b/src/config.js index fa55e71..16ac6ec 100644 --- a/src/config.js +++ b/src/config.js @@ -5,3 +5,6 @@ export const POTREE_SETTINGS = { fov: 60, pointBudget: 1_000_000 } + +export const ecef = '+proj=geocent +datum=WGS84 +units=m +no_defs' // EPSG:4978 (geocentric coordinates) +export const wgs84 = '+proj=longlat +datum=WGS84 +no_defs' // EPSG:4326 (geographic coordinates) \ No newline at end of file diff --git a/src/coordinateShowing/coordinateShowing.css b/src/coordinateShowing/coordinateShowing.css index 07f2133..dd46e72 100644 --- a/src/coordinateShowing/coordinateShowing.css +++ b/src/coordinateShowing/coordinateShowing.css @@ -1,23 +1,27 @@ -#posCanvas { +#canvasContainer { + display: flex; + flex-direction: column; position: absolute; - left: 10px; + right: 10px; bottom: 10px; +} + +#posCanvas { + position: relative; width: 300px; height: 50px; - pointer-events: none; background-color: #19282c; z-index: 10; border-radius: 5px; } #elevationCanvas { - position: absolute; - right: 100px; - top: 15px; + position: relative; width: 300px; height: 50px; - pointer-events: none; background-color: #19282c; z-index: 10; + margin-bottom: 10px; border-radius: 5px; } + diff --git a/src/coordinateShowing/coordinateShowing.js b/src/coordinateShowing/coordinateShowing.js index 7019138..bb7f360 100644 --- a/src/coordinateShowing/coordinateShowing.js +++ b/src/coordinateShowing/coordinateShowing.js @@ -1,5 +1,5 @@ -const ecef = '+proj=geocent +datum=WGS84 +units=m +no_defs' // EPSG:4978 (geocentric coordinates) -const wgs84 = '+proj=longlat +datum=WGS84 +no_defs' // EPSG:4326 (geographic coordinates) +import { ecef } from "../config.js"; +import { wgs84 } from "../config.js"; const posCanvas = document.getElementById('posCanvas') // lat/lon const elevationCanvas = document.getElementById('elevationCanvas') @@ -10,7 +10,7 @@ export let elevationCtx /** * Initializes the canvases and their contexts. */ -export function initCanvases() { +export function initCoordinateCanvases() { posCtx = resizeCanvas(posCanvas) elevationCtx = resizeCanvas(elevationCanvas) } @@ -47,9 +47,9 @@ function drawText(ctx, text, canvas) { } /** - * Updates the lat/lon position. + * Updates the lat/lon coordinates. */ -export function updatePosition() { +export function updateCoordinateText() { const cam = window.potreeViewer.scene.view.position const [lon, lat] = proj4(ecef, wgs84, [cam.x, cam.y, cam.z]) drawText( diff --git a/src/main.js b/src/main.js index e1670e1..af6efff 100644 --- a/src/main.js +++ b/src/main.js @@ -3,10 +3,10 @@ import { createCesiumViewer } from './cesiumViewer.js' import { createPotreeViewer } from './potreeViewer.js' import { syncCameras } from './cameraSync.js' import { - initCanvases, - updatePosition, + initCoordinateCanvases, + updateCoordinateText, updateTargetElevation -} from './coordinateShowing/coordinateShowing.js' +} from "./CoordinateShowing/coordinateShowing.js"; async function init() { window.cesiumViewer = createCesiumViewer('cesiumContainer') @@ -17,12 +17,12 @@ async function init() { POTREE_SETTINGS ) - initCanvases() + initCoordinateCanvases() - potreeViewer.addEventListener('update', updatePosition) + potreeViewer.addEventListener('update', updateCoordinateText) potreeViewer.addEventListener('update', updateTargetElevation) - window.addEventListener('resize', initCanvases) + window.addEventListener('resize', initCoordinateCanvases) function loop(timestamp) { requestAnimationFrame(loop) diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 3f04686..4644388 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,4 +1,5 @@ import { initElevationControls } from './ElevationControl/elevationControl.js' +import { ecef } from './config.js' /** * Initializes the Potree viewer used to visualize the point cloud. @@ -55,7 +56,7 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { pc.material.activeAttributeName = 'elevation' pc.material.gradient = Potree.Gradients['VIRIDIS'] - e.pointcloud.projection = '+proj=geocent +datum=WGS84 +units=m +no_defs' + e.pointcloud.projection = ecef // Initialize camera position and target point (manually chosen) viewer.scene.view.setView( From 7bc12dd3d3c2b6284dbd78da127d972432ab0e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 6 Oct 2025 12:26:27 +0200 Subject: [PATCH 42/46] fix(#5): :bug: Rounded coordinates to one decimal so that they are displayed neatly --- src/MeasurementControl/measurementsPanel.js | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 0e66fe5..8f5f12b 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -196,6 +196,10 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { } } } + requestAnimationFrame(() => { + roundCoordinates(originalPropertiesPanel) + initCoordObserver() + }) } function restorePanelToOriginal() { if (!originalPropertiesPanel || !placeholder || !originalParent) return @@ -222,6 +226,58 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { targetContainer.appendChild(msg2) } + // Helper: round only the coordinates table (x,y,z header row) to 1 decimal + let coordRoundObserver = null + function initCoordObserver() { + if (!originalPropertiesPanel || coordRoundObserver) return + coordRoundObserver = new MutationObserver(() => { + requestAnimationFrame(() => roundCoordinates(originalPropertiesPanel)) + }) + coordRoundObserver.observe(originalPropertiesPanel, { + childList: true, + subtree: true + }) + } + + function roundCoordinates(rootEl) { + if (!rootEl) return + // Find first table that has a header row with th: x y z + const tables = rootEl.querySelectorAll('table.measurement_value_table') + let coordTable = null + for (const tbl of tables) { + const headerRow = tbl.querySelector('tr') + if (!headerRow) continue + const ths = [...headerRow.querySelectorAll('th')].map(th => + (th.textContent || '').trim().toLowerCase() + ) + if (ths.length >= 3 && ths[0] === 'x' && ths[1] === 'y' && ths[2] === 'z') { + coordTable = tbl + break + } + } + if (!coordTable) return + + const dataRows = [...coordTable.querySelectorAll('tr')].slice(1) + dataRows.forEach(row => { + row.querySelectorAll('td').forEach(td => { + if (td.querySelector('button, input, select')) return + const raw = (td.textContent || '').trim() + if (!raw) return + // Allow formats with commas, spaces, possible degree sign, trailing labels + // Extract leading numeric with optional sign & decimal + const cleaned = raw + .replace(/[^\d+.\-]/g, c => (c === ',' ? '' : '')) // clean data + .replace(/,+/g, '') + if (!cleaned || !/[-+]?\d*\.?\d+/.test(cleaned)) return + const num = Number(cleaned) + if (!Number.isFinite(num)) return + const rounded = num.toFixed(1) + if (td.textContent !== rounded) { + td.textContent = rounded + } + }) + }) + } // Helper to decide if a uuid is a measurement-like object function isMeasurementUUID(uuid) { if (!uuid) return false From 7f6d2a8102abcbe448b64d8e32dcaa630cc203a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 6 Oct 2025 12:27:10 +0200 Subject: [PATCH 43/46] ran format --- src/MeasurementControl/measurementsPanel.js | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 8f5f12b..c4f8183 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -197,9 +197,9 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { } } requestAnimationFrame(() => { - roundCoordinates(originalPropertiesPanel) - initCoordObserver() - }) + roundCoordinates(originalPropertiesPanel) + initCoordObserver() + }) } function restorePanelToOriginal() { if (!originalPropertiesPanel || !placeholder || !originalParent) return @@ -226,7 +226,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { targetContainer.appendChild(msg2) } - // Helper: round only the coordinates table (x,y,z header row) to 1 decimal + // Helper: round only the coordinates table (x,y,z header row) to 1 decimal let coordRoundObserver = null function initCoordObserver() { if (!originalPropertiesPanel || coordRoundObserver) return @@ -247,10 +247,15 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { for (const tbl of tables) { const headerRow = tbl.querySelector('tr') if (!headerRow) continue - const ths = [...headerRow.querySelectorAll('th')].map(th => + const ths = [...headerRow.querySelectorAll('th')].map((th) => (th.textContent || '').trim().toLowerCase() ) - if (ths.length >= 3 && ths[0] === 'x' && ths[1] === 'y' && ths[2] === 'z') { + if ( + ths.length >= 3 && + ths[0] === 'x' && + ths[1] === 'y' && + ths[2] === 'z' + ) { coordTable = tbl break } @@ -258,15 +263,15 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { if (!coordTable) return const dataRows = [...coordTable.querySelectorAll('tr')].slice(1) - dataRows.forEach(row => { - row.querySelectorAll('td').forEach(td => { + dataRows.forEach((row) => { + row.querySelectorAll('td').forEach((td) => { if (td.querySelector('button, input, select')) return const raw = (td.textContent || '').trim() if (!raw) return // Allow formats with commas, spaces, possible degree sign, trailing labels // Extract leading numeric with optional sign & decimal const cleaned = raw - .replace(/[^\d+.\-]/g, c => (c === ',' ? '' : '')) // clean data + .replace(/[^\d+.\-]/g, (c) => (c === ',' ? '' : '')) // clean data .replace(/,+/g, '') if (!cleaned || !/[-+]?\d*\.?\d+/.test(cleaned)) return const num = Number(cleaned) From 5fb1a1fba809567f9b7badf23840c02cccf94706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 6 Oct 2025 13:11:42 +0200 Subject: [PATCH 44/46] refactor(#5): :recycle: fix some confilcts with dev --- index.html | 51 --------------------- src/MeasurementControl/measurementsPanel.js | 6 ++- src/potreeViewer.js | 2 + 3 files changed, 7 insertions(+), 52 deletions(-) diff --git a/index.html b/index.html index 251e0f9..e915c48 100644 --- a/index.html +++ b/index.html @@ -79,57 +79,6 @@
- - - - diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index c4f8183..507a73e 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -4,7 +4,7 @@ * entries with per-type numbering, syncs selection with Potree's jsTree, and * dynamically mounts the native properties panel when a measurement is active. */ -window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { +window.createMeasurementsPanel = function createMeasurementsPanel(viewer) { // Track last selected measurement label for dynamic data title const lastSelection = { uuid: null, label: '' } // Resolve or create measurements container in Potree menu @@ -676,3 +676,7 @@ window.initMeasurementsPanel = function initMeasurementsPanel(viewer) { } } } + +export function initMeasurementsPanel(viewer) { + return window.createMeasurementsPanel(viewer) +} diff --git a/src/potreeViewer.js b/src/potreeViewer.js index 4644388..fb709f0 100644 --- a/src/potreeViewer.js +++ b/src/potreeViewer.js @@ -1,4 +1,5 @@ import { initElevationControls } from './ElevationControl/elevationControl.js' +import { initMeasurementsPanel } from './MeasurementControl/measurementsPanel.js' import { ecef } from './config.js' /** @@ -35,6 +36,7 @@ export async function createPotreeViewer(containerId, pointcloudUrl, settings) { viewer.toggleSidebar() initElevationControls(viewer) + initMeasurementsPanel(viewer) }) const e = await Potree.loadPointCloud(pointcloudUrl) From f04fabb50c33d3151a60f211f1a9faac46667712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 6 Oct 2025 13:13:18 +0200 Subject: [PATCH 45/46] ran format --- index.html | 3 +-- src/config.js | 2 +- src/coordinateShowing/coordinateShowing.css | 1 - src/coordinateShowing/coordinateShowing.js | 4 ++-- src/main.js | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index e915c48..9aee3f9 100644 --- a/index.html +++ b/index.html @@ -57,7 +57,6 @@ -
-
+
diff --git a/src/config.js b/src/config.js index 16ac6ec..09c17aa 100644 --- a/src/config.js +++ b/src/config.js @@ -7,4 +7,4 @@ export const POTREE_SETTINGS = { } export const ecef = '+proj=geocent +datum=WGS84 +units=m +no_defs' // EPSG:4978 (geocentric coordinates) -export const wgs84 = '+proj=longlat +datum=WGS84 +no_defs' // EPSG:4326 (geographic coordinates) \ No newline at end of file +export const wgs84 = '+proj=longlat +datum=WGS84 +no_defs' // EPSG:4326 (geographic coordinates) diff --git a/src/coordinateShowing/coordinateShowing.css b/src/coordinateShowing/coordinateShowing.css index dd46e72..0a8ac75 100644 --- a/src/coordinateShowing/coordinateShowing.css +++ b/src/coordinateShowing/coordinateShowing.css @@ -24,4 +24,3 @@ margin-bottom: 10px; border-radius: 5px; } - diff --git a/src/coordinateShowing/coordinateShowing.js b/src/coordinateShowing/coordinateShowing.js index bb7f360..8045cd0 100644 --- a/src/coordinateShowing/coordinateShowing.js +++ b/src/coordinateShowing/coordinateShowing.js @@ -1,5 +1,5 @@ -import { ecef } from "../config.js"; -import { wgs84 } from "../config.js"; +import { ecef } from '../config.js' +import { wgs84 } from '../config.js' const posCanvas = document.getElementById('posCanvas') // lat/lon const elevationCanvas = document.getElementById('elevationCanvas') diff --git a/src/main.js b/src/main.js index af6efff..aaa415a 100644 --- a/src/main.js +++ b/src/main.js @@ -6,7 +6,7 @@ import { initCoordinateCanvases, updateCoordinateText, updateTargetElevation -} from "./CoordinateShowing/coordinateShowing.js"; +} from './CoordinateShowing/coordinateShowing.js' async function init() { window.cesiumViewer = createCesiumViewer('cesiumContainer') From 7af180794d1d17e79d834b60dd9331fcb99e4916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gaute=20Fl=C3=A6gstad?= Date: Mon, 6 Oct 2025 13:23:42 +0200 Subject: [PATCH 46/46] refactor(#5): :recycle: Small cleanup --- src/MeasurementControl/measurementsPanel.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/MeasurementControl/measurementsPanel.js b/src/MeasurementControl/measurementsPanel.js index 507a73e..6b70ac2 100644 --- a/src/MeasurementControl/measurementsPanel.js +++ b/src/MeasurementControl/measurementsPanel.js @@ -4,7 +4,7 @@ * entries with per-type numbering, syncs selection with Potree's jsTree, and * dynamically mounts the native properties panel when a measurement is active. */ -window.createMeasurementsPanel = function createMeasurementsPanel(viewer) { +export function initMeasurementsPanel(viewer) { // Track last selected measurement label for dynamic data title const lastSelection = { uuid: null, label: '' } // Resolve or create measurements container in Potree menu @@ -676,7 +676,3 @@ window.createMeasurementsPanel = function createMeasurementsPanel(viewer) { } } } - -export function initMeasurementsPanel(viewer) { - return window.createMeasurementsPanel(viewer) -}