Skip to content

40 visualise multiple point clouds simultaneously #41

Merged
merged 10 commits into from
Oct 27, 2025

Conversation

adriahso
Copy link
Member

@adriahso adriahso commented Oct 25, 2025

Issue number

Closes #40

Description

Makes it possible to load and visualise multiple point clouds at once in order to enable grouping of data into smaller chunks. This is done to avoid floating point precision problems.

Testing steps

  • This should be tested on data sent through the new converter which groups/crops surveys by level 2 H3 cells: 3 group surveys to fix floating point issue lasConverter#4
  • Make sure the point cloud paths in src/config.js match your point cloud folders
  • Play around in the application and test that tools work as they should

Summary by CodeRabbit

  • New Features

    • Load and display multiple point clouds; viewer applies elevation, gradient and filtering controls across all loaded clouds
    • Default background option changed from "None" to "Globe"
  • Bug Fixes / UI

    • Panel headers toggle by click; works with or without jQuery
    • Annotations panel collapse/expand now safely falls back to pure-DOM handling
  • Documentation

    • Updated point‑cloud data folder instructions and path guidance for configuration

@adriahso adriahso self-assigned this Oct 25, 2025
@adriahso adriahso linked an issue Oct 25, 2025 that may be closed by this pull request
@coderabbit
Copy link

coderabbit bot commented Oct 25, 2025

Walkthrough

This PR adds multi-point-cloud support and related UI hook wiring: config now exports an array of point-cloud metadata URLs; the viewer loads and coordinates multiple clouds (elevation, gradient, accepted filtering); elevation slider and panel code were updated to accept hooks and notify on range changes; README path guidance was adjusted.

Changes

Cohort / File(s) Summary
Configuration
src/config.js, src/main.js
Replaced POTREE_POINTCLOUD_URL with POTREE_POINTCLOUD_URLS (array of 15 metadata.json URLs); updated imports and argument passing in src/main.js.
Multi-cloud viewer
src/potreeViewer.js
createPotreeViewer signature changed to accept pointcloudUrls; loads multiple clouds, configures per-cloud materials/projections, collects clouds for coordinated updates, and propagates elevation/gradient/accepted-filter changes across all clouds.
Elevation UI & hooks
src/AcceptedFiltering/threePanels.js
Renamed rebindElevationLabelsetUpElevationSlider(hooks), threaded hooks (including onElevationRangeChange) through relocation/initialization/self-healing flows, and added click-to-toggle panel header behavior with jQuery UI fallback.
Annotation panel robustness
src/AnnotationControl/annotationPanel.js
Made collapse/toggle logic resilient when jQuery is absent (uses optional chaining, DOM fallback for show/hide).
Docs
README.md
Updated point-cloud folder guidance from public/pointclouds/data_converted to public/pointclouds and reminded to ensure folder names match paths in src/config.js; retains note that point cloud files should not be committed.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant Main as main.js
    participant Viewer as potreeViewer.js
    participant UI as threePanels.js
    participant Cloud as PointCloud (repeated)

    Main->>Viewer: createPotreeViewer(containerId, pointcloudUrls, settings)
    loop for each url in pointcloudUrls
        Viewer->>Cloud: load metadata.json & add point cloud
        Viewer->>Cloud: apply material, projection, set initial elevation/gradient
    end
    Viewer-->>Main: return viewer with multiple clouds

    User->>UI: adjust elevation slider
    UI->>Viewer: hooks.onElevationRangeChange([low,high])
    par propagate
        Viewer->>Cloud: updateAllCloudsElevation([low,high])
    end
Loading
sequenceDiagram
    autonumber
    participant User
    participant UI as threePanels.js
    participant Viewer as potreeViewer.js
    participant Clouds as All Point Clouds

    User->>UI: choose gradient
    UI->>Viewer: overrideGradientSchemeClick(gradientName)
    Viewer->>Clouds: updateAllCloudsGradient(gradientName)
    Note over Clouds: Gradient applied uniformly to each cloud
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay special attention to:
    • src/potreeViewer.js: multi-cloud loading order, aggregation into the pointclouds collection, and centralized state like lastElevationGradient.
    • src/AcceptedFiltering/threePanels.js: correct propagation of hooks to all call sites and backward compatibility when hooks is absent.
    • src/config.js / src/main.js: ensure no remaining references to the removed POTREE_POINTCLOUD_URL.

Possibly related PRs

Suggested reviewers

  • kleinc
  • mariewah

Poem

🐰 I hop from cloud to cloud with glee,

sliders sing and gradients agree,
many surveys stitched into one view,
I nibble bugs and stitch the hue,
joyous thumps — the viewer's new.

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "40 visualise multiple point clouds simultaneously" directly relates to the main objective of the changeset, which is to enable loading and visualizing multiple point clouds at once. The title is specific and clearly communicates the primary change, despite including the issue number "40" as a prefix (which could be considered minor noise). The changes across src/config.js, src/main.js, src/potreeViewer.js, and supporting files all align with implementing this capability.
Linked Issues Check ✅ Passed The code changes successfully implement both objectives from issue #40. The PR enables multiple point cloud visualization through a refactored createPotreeViewer function that accepts an array of point cloud URLs (src/potreeViewer.js), updated configuration to support 15 point cloud URLs (src/config.js), and proper orchestration of multi-cloud state management including elevation ranges and filtering across all clouds. The changes support loading and displaying multiple point clouds simultaneously, which directly addresses the goal of preventing floating point precision problems through data chunking.
Out of Scope Changes Check ✅ Passed All code changes are directly related to the objective of supporting multiple point cloud visualization. The modifications span configuration updates (src/config.js), main entry point adaptation (src/main.js), core viewer refactoring (src/potreeViewer.js), UI panel enhancements for multi-cloud state management (src/AcceptedFiltering/threePanels.js), defensive jQuery compatibility improvements (src/AnnotationControl/annotationPanel.js), and documentation updates (README.md). Each change either implements core functionality for multi-cloud support or provides supporting infrastructure to enable coordinated interaction across multiple clouds.
Docstring Coverage ✅ Passed Docstring coverage is 89.47% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 40-visualise-multiple-point-clouds-simultaneously

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbit coderabbit bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/AcceptedFiltering/threePanels.js (1)

407-419: Blocker: hooks is not in scope inside attachSelfHealing

moveElevationContainer(hooks) references an undefined hooks. Add a parameter and thread it through.

-function attachSelfHealing(activeGetter) {
+function attachSelfHealing(activeGetter, hooks) {
   const root = byId('potree_menu') || document.body
   const obs = new MutationObserver(() => {
     const mode = activeGetter()
     if (mode === 'elevation') {
       const src = byId('materials.elevation_container')
       const { body } = ensurePanelScaffold('elevation2_list')
-      if (src && body && src.parentNode !== body) moveElevationContainer(hooks)
+      if (src && body && src.parentNode !== body) moveElevationContainer(hooks)
     }
   })
   obs.observe(root, { childList: true, subtree: true })
   return obs
 }
🧹 Nitpick comments (6)
src/AcceptedFiltering/threePanels.js (2)

193-213: Throttle elevation updates to avoid UI jank across many clouds

The slider fires on every slide/change; calling hooks for N clouds can stutter. Throttle with rAF.

-function setUpElevationSlider(hooks) {
+function setUpElevationSlider(hooks) {
   const $ = window.jQuery || window.$
   const slider = $ ? $('#sldHeightRange') : null
   const label = byId('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)}`
-    hooks?.onElevationRangeChange([low, high])
-  }
+  let raf = 0
+  const update = () => {
+    if (raf) return
+    raf = requestAnimationFrame(() => {
+      raf = 0
+      const low = slider.slider('values', 0)
+      const high = slider.slider('values', 1)
+      label.textContent = `${low.toFixed(2)} to ${high.toFixed(2)}`
+      hooks?.onElevationRangeChange?.([low, high])
+    })
+  }
 
   slider.slider({ min: -10000, max: 0, values: [-10000, 0] })
   slider.off('slide.custom slidestop.custom change.custom')
   slider.on('slide.custom slidestop.custom change.custom', update)
   update()
 }

145-147: Nit: fix typos in comments

  • "wverything" → "everything"
  • "Does appearnlty not work without this" → "Apparently does not work without this"
- * Polls the DOM for an element id so that wverything is ensured displayed.
+ * Polls the DOM for an element id so that everything is ensured displayed.
...
- * Ensures a UL list host exists inside the Accepted section (Does appearnlty not work without this).
+ * Ensures a UL list host exists inside the Accepted section (apparently does not work without this).

Also applies to: 324-327

README.md (1)

9-9: Consider BASE_URL for subpath deployments

If the site is served under a subpath, leading slashes in src/config.js paths may break loads. Consider documenting that paths can be prefixed with import.meta.env.BASE_URL or made relative.

src/config.js (1)

1-17: Optional: make URLs configurable and avoid leading slash

To support multiple environments, consider:

  • Reading base path from import.meta.env.BASE_URL.
  • Allow overriding via env (e.g., Vite env vars).
-export const POTREE_POINTCLOUD_URLS = [
-  '/pointclouds/cell_1/metadata.json',
+const BASE = import.meta?.env?.BASE_URL ?? '/'
+export const POTREE_POINTCLOUD_URLS = [
+  `${BASE}pointclouds/cell_1/metadata.json`,
   // ...
 ]
src/potreeViewer.js (2)

131-137: Duplicate background rename logic

You already call makeGlobeBackgroundOption(). The jQuery block re-renames the same input later and likely no-ops after the first rename. Remove the duplicate for clarity.

-  // Change name of default background from 'None' to 'Globe"'
-  $('#background_options_none')
-    .text('Globe')
-    .attr('id', 'background_options_globe')
-    .val('globe')
-
-  viewer.setBackground('globe')
+  // `makeGlobeBackgroundOption()` already renames and selects 'Globe'
+  viewer.setBackground('globe')

Also applies to: 105-106


111-129: Optional: parallelize multi-cloud loading

Sequential await in loop delays first render. Consider parallel loading with a bounded concurrency or Promise.all.

-  for (const url of pointcloudUrls) {
-    const e = await Potree.loadPointCloud(url)
-    const pc = e.pointcloud
-    viewer.scene.addPointCloud(pc)
-    // ...
-    pointclouds.push(pc)
-  }
+  const loaded = await Promise.all(pointcloudUrls.map((url) => Potree.loadPointCloud(url)))
+  for (const e of loaded) {
+    const pc = e.pointcloud
+    viewer.scene.addPointCloud(pc)
+    pc.material.pointSizeType = Potree.PointSizeType.ADAPTIVE
+    pc.material.shape = Potree.PointShape.CIRCLE
+    overrideShaderForGradient(pc)
+    pc.material.elevationRange = [-10000, 0]
+    pc.material.activeAttributeName = 'elevation'
+    pc.material.gradient = Potree.Gradients['VIRIDIS']
+    e.pointcloud.projection = ecef
+    pointclouds.push(pc)
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9284341 and d1580ad.

📒 Files selected for processing (5)
  • README.md (1 hunks)
  • src/AcceptedFiltering/threePanels.js (8 hunks)
  • src/config.js (1 hunks)
  • src/main.js (2 hunks)
  • src/potreeViewer.js (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/potreeViewer.js (2)
src/AcceptedFiltering/threePanels.js (4)
  • initThreePanels (427-450)
  • $ (197-197)
  • slider (198-198)
  • toggleAcceptedLegend (318-321)
src/config.js (2)
  • ecef (25-25)
  • ecef (25-25)
src/main.js (1)
src/config.js (2)
  • POTREE_POINTCLOUD_URLS (1-17)
  • POTREE_POINTCLOUD_URLS (1-17)
🔇 Additional comments (6)
src/AcceptedFiltering/threePanels.js (4)

219-230: LGTM: container move now rebinds slider with hooks

Relocating the elevation container and immediately wiring the slider is correct for multi-cloud propagation.


242-245: LGTM: observer forwards hooks into moveElevationContainer

Ensures re-bind on rebuilds. Good.


375-377: LGTM: ensurePanelCaptured threads hooks to mover

Keeps the slider/hook wiring consistent after UI nudges.


425-426: LGTM: hooks documented and passed to self-healing

Signature docs match usage; passing hooks works once the function signature is fixed.

After applying the signature change above, re-run the app to ensure no console ReferenceError remains for attachSelfHealing.

Also applies to: 446-446

src/main.js (1)

1-1: LGTM: config boundary updated to URL array

Import and callsite align with multi-cloud API.

Also applies to: 21-22

src/potreeViewer.js (1)

85-90: Confirm 'Accepted' attribute availability across all clouds

Setting activeAttributeName = 'Accepted' assumes the attribute exists uniformly. If any cloud lacks it, colors may be undefined.

Would you like a guard that falls back to elevation if 'Accepted' is missing on a cloud?

Copy link

@coderabbit coderabbit bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1580ad and f19fd56.

📒 Files selected for processing (3)
  • src/AcceptedFiltering/threePanels.js (8 hunks)
  • src/AnnotationControl/annotationPanel.js (1 hunks)
  • src/potreeViewer.js (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/AcceptedFiltering/threePanels.js (1)
src/AnnotationControl/annotationPanel.js (3)
  • header (27-27)
  • menu (25-25)
  • panel (33-33)
src/potreeViewer.js (2)
src/AcceptedFiltering/threePanels.js (4)
  • initThreePanels (429-452)
  • $ (199-199)
  • slider (200-200)
  • toggleAcceptedLegend (320-323)
src/config.js (2)
  • ecef (25-25)
  • ecef (25-25)
src/AnnotationControl/annotationPanel.js (1)
src/AcceptedFiltering/threePanels.js (1)
  • $ (199-199)
🔇 Additional comments (7)
src/AnnotationControl/annotationPanel.js (1)

59-67: LGTM: jQuery-optional toggle with proper fallbacks.

The refactor correctly guards against missing jQuery by aliasing $ = window.jQuery || window.$, checking for the accordion plugin via optional chaining ($.fn?.accordion), and providing a vanilla DOM fallback when jQuery is unavailable. This aligns with the pattern used in src/AcceptedFiltering/threePanels.js at line 198.

src/potreeViewer.js (4)

13-21: Signature change supports multi-cloud loading.

The updated signature now accepts pointcloudUrls (array) instead of a single pointcloudUrl, enabling the viewer to load and coordinate multiple point clouds as intended by the PR objectives.


38-38: Early declaration resolves TDZ.

Declaring pointclouds before the loadGUI callback fixes the Temporal Dead Zone issue flagged in the previous review, ensuring helper functions can safely reference the variable without a ReferenceError.


312-332: Gradient override improved with null-guard and non-destructive handlers.

The refactor addresses the previous review comment by adding a null-guard at line 316 (if (!gradientContainer) return) and using addEventListener at line 320 instead of onclick assignment, preventing handler clobbering and runtime errors when the container is absent.


113-130: No race condition exists; the original concern is incorrect.

The code is properly structured. The viewer.loadGUI() callback (line 40) runs before the point cloud loop (lines 113-130), but crucially:

  • Event handlers defined in the callback (onActivateElevation, onActivateAccepted, onElevationRangeChange) are closures that execute on user interaction, not immediately
  • overrideGradientSchemeClick() sets up event listeners at line 105; the actual pointclouds array processing occurs inside the click handler closure (line 324-326), which fires after user interaction
  • All event handlers capture the pointclouds array by reference, so they will access the fully populated array when invoked

Execution order is correct: GUI initialization with deferred handlers → point cloud loading → user interaction with populated data. No awaiting changes needed.

Likely an incorrect or invalid review comment.

src/AcceptedFiltering/threePanels.js (2)

49-58: LGTM: Consistent jQuery-optional toggle pattern.

The toggle handler follows the same defensive pattern introduced in annotationPanel.js (lines 59-67), aliasing jQuery, checking for the accordion plugin via optional chaining, and providing a vanilla DOM fallback. This ensures robust behavior across environments with or without jQuery.


196-209: Well-structured hook propagation for elevation range updates.

The refactor from rebindElevationLabel to setUpElevationSlider adds support for onElevationRangeChange, enabling the viewer to synchronize elevation settings across multiple point clouds. The hooks are consistently propagated through moveElevationContainer (line 221) and related call sites (lines 244, 377, 416), with updated JSDoc at line 427 documenting the new callback.

Also applies to: 221-221, 427-427

Copy link

@coderabbit coderabbit bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/potreeViewer.js (2)

17-21: Consider adding input validation for the pointcloudUrls parameter.

The function doesn't validate that pointcloudUrls is a non-empty array. If undefined, null, or a non-iterable is passed, the for-of loop will throw. If an empty array is passed, the viewer will initialize with no clouds, which may not be the intended behavior.

Apply this diff to add defensive validation:

 export async function createPotreeViewer(
   containerId,
   pointcloudUrls,
   settings
 ) {
+  if (!Array.isArray(pointcloudUrls) || pointcloudUrls.length === 0) {
+    throw new Error('pointcloudUrls must be a non-empty array')
+  }
+
   const viewer = new Potree.Viewer(document.getElementById(containerId), {
     useDefaultRenderLoop: false
   })

38-56: Consider error handling and loading strategy for multiple clouds.

The current implementation loads clouds sequentially and will reject entirely if any single cloud fails. Depending on requirements, you might want to:

  1. Add try-catch per cloud to handle partial failures gracefully
  2. Load clouds in parallel with Promise.allSettled() for better performance
  3. Continue with successfully loaded clouds rather than failing completely

Example with parallel loading and partial success handling:

const pointclouds = []
const results = await Promise.allSettled(
  pointcloudUrls.map(url => Potree.loadPointCloud(url))
)

for (const [idx, result] of results.entries()) {
  if (result.status === 'rejected') {
    console.error(`Failed to load cloud ${pointcloudUrls[idx]}:`, result.reason)
    continue
  }
  
  const e = result.value
  const pc = e.pointcloud
  viewer.scene.addPointCloud(pc)
  
  // ... rest of setup
  pointclouds.push(pc)
}

if (pointclouds.length === 0) {
  throw new Error('No point clouds could be loaded')
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f19fd56 and 8a0bd38.

📒 Files selected for processing (1)
  • src/potreeViewer.js (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/potreeViewer.js (2)
src/config.js (2)
  • ecef (25-25)
  • ecef (25-25)
src/AcceptedFiltering/threePanels.js (5)
  • initThreePanels (429-452)
  • $ (199-199)
  • slider (200-200)
  • toggleAcceptedLegend (320-323)
  • icon (140-142)
🔇 Additional comments (6)
src/potreeViewer.js (6)

66-90: LGTM! Clean helper functions for coordinated cloud updates.

The helper functions properly encapsulate the logic for updating all point clouds consistently. The use of forEach is safe even if pointclouds is empty, and the functions provide good abstraction for the hook wiring.


93-103: Good defensive programming with slider value fallbacks.

The hook properly handles cases where jQuery or the slider might not be available, with sensible defaults that match the initial elevation range. The type checks ensure numeric values before use.


104-110: LGTM! Hook wiring is correct and straightforward.

The hooks properly coordinate UI state with point cloud visualization, correctly applying the GRAYSCALE gradient for accepted filtering and delegating elevation range changes to the helper function.


311-331: LGTM! Past review concerns addressed.

The function now includes a null guard for the missing container case and uses addEventListener instead of direct onclick assignment, preventing handler clobbering. The logic correctly applies gradients to all clouds and tracks the selection via the callback.


336-347: Functional DOM manipulation with proper guards.

The function correctly repurposes the "None" background option into a "Globe" option with appropriate null guards. While the approach of modifying existing DOM elements is somewhat unconventional, it's practical and safe with the guards in place.


190-196: Gradient texture lookup pattern is confirmed as intentional—appears consistently across all Potree shader functions.

The search results reveal that texture2D(gradient, vec2(w, 1.0-w)) is not an isolated occurrence but a consistent pattern used throughout the Potree shader codebase. The pattern appears in:

  • Multiple shader functions (getGpsTime, getElevation, getIntensity, getSourceID, getExtra)
  • Both source files (src/potreeViewer.js) and built files (public/build/shaders/shaders.js, public/build/potree/potree.js)
  • All instances with identical coordinate semantics

This uniform usage across different rendering modes and functions strongly suggests the pattern is intentional and specific to Potree's gradient texture format, rather than an error. While standard Three.js gradient maps use vec2(w, 0.0), Potree's implementation appears to leverage the 2D nature of the gradient texture with both X and Y coordinates, which may be part of Potree's color mapping design.

No issues detected.

Copy link
Member

@gautegf gautegf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there changes to the filtering and annotation control sections? Is it only exception handling or are there new features to them?

Copy link
Member

@gautegf gautegf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only had one dataset to test with!

My testing steps:

  • Convert a csv file to .las with the new lasConverter
  • Used PotreeConverter to convert the .las to potree octree data
  • The pointcloud is displayed as expected, also checked the pattern of the points, they are no longer in pair or triplets as far as I can see
  • Tested normal features like gradient, measurements, saving locations. Everything worked as expected

@adriahso
Copy link
Member Author

Why are there changes to the filtering and annotation control sections? Is it only exception handling or are there new features to them?

When loading multiple point clouds the elevation control and accepted filtering stopped working (or only worked on one of the clouds), so had to make changes in order to fix this. While fixing the elevation control I also fixed a bug, where the elevation control and accepted filtering panels/accordions could not be closed, by using some code from the annotationPanel. CodeRabbit then commented on this logic and suggested making it safer, so I changed it both places.

Copy link
Member

@kleinc kleinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

Copy link
Member

@kleinc kleinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@adriahso adriahso merged commit 5f93b94 into dev Oct 27, 2025
2 checks passed
@adriahso adriahso deleted the 40-visualise-multiple-point-clouds-simultaneously branch October 27, 2025 09:01
Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Visualise multiple point clouds simultaneously
3 participants