@ifc-lite/renderer 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/edl-pass.d.ts +52 -0
  2. package/dist/edl-pass.d.ts.map +1 -0
  3. package/dist/edl-pass.js +204 -0
  4. package/dist/edl-pass.js.map +1 -0
  5. package/dist/index.d.ts +77 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +314 -19
  8. package/dist/index.js.map +1 -1
  9. package/dist/picker.d.ts +22 -3
  10. package/dist/picker.d.ts.map +1 -1
  11. package/dist/picker.js +48 -8
  12. package/dist/picker.js.map +1 -1
  13. package/dist/picking-manager.d.ts +14 -1
  14. package/dist/picking-manager.d.ts.map +1 -1
  15. package/dist/picking-manager.js +13 -1
  16. package/dist/picking-manager.js.map +1 -1
  17. package/dist/point-picker.d.ts +61 -0
  18. package/dist/point-picker.d.ts.map +1 -0
  19. package/dist/point-picker.js +223 -0
  20. package/dist/point-picker.js.map +1 -0
  21. package/dist/pointcloud/point-cloud-node.d.ts +56 -0
  22. package/dist/pointcloud/point-cloud-node.d.ts.map +1 -0
  23. package/dist/pointcloud/point-cloud-node.js +112 -0
  24. package/dist/pointcloud/point-cloud-node.js.map +1 -0
  25. package/dist/pointcloud/point-cloud-renderer.d.ts +135 -0
  26. package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -0
  27. package/dist/pointcloud/point-cloud-renderer.js +296 -0
  28. package/dist/pointcloud/point-cloud-renderer.js.map +1 -0
  29. package/dist/pointcloud/point-pipeline.d.ts +25 -0
  30. package/dist/pointcloud/point-pipeline.d.ts.map +1 -0
  31. package/dist/pointcloud/point-pipeline.js +111 -0
  32. package/dist/pointcloud/point-pipeline.js.map +1 -0
  33. package/dist/pointcloud/point-shader.wgsl.d.ts +22 -0
  34. package/dist/pointcloud/point-shader.wgsl.d.ts.map +1 -0
  35. package/dist/pointcloud/point-shader.wgsl.js +212 -0
  36. package/dist/pointcloud/point-shader.wgsl.js.map +1 -0
  37. package/dist/types.d.ts +15 -4
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -26,6 +26,10 @@ export { ZeroCopyGpuUploader, createZeroCopyUploader, } from './zero-copy-upload
26
26
  // Extracted manager classes
27
27
  export { PickingManager } from './picking-manager.js';
28
28
  export { RaycastEngine } from './raycast-engine.js';
29
+ export { PointPicker, decodePickSample } from './point-picker.js';
30
+ // Point cloud rendering (Phase 0: IFCx inline; Phase 1+: streaming LAS/LAZ)
31
+ export { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
32
+ export { PointRenderPipeline } from './pointcloud/point-pipeline.js';
29
33
  import { WebGPUDevice } from './device.js';
30
34
  import { RenderPipeline, InstancedRenderPipeline } from './pipeline.js';
31
35
  import { Camera } from './camera.js';
@@ -40,6 +44,8 @@ import { DEFAULT_CAP_STYLE, HATCH_PATTERN_IDS } from './section-cap-style.js';
40
44
  import { PickingManager } from './picking-manager.js';
41
45
  import { RaycastEngine } from './raycast-engine.js';
42
46
  import { PostProcessor } from './post-processor.js';
47
+ import { EdlPass } from './edl-pass.js';
48
+ import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
43
49
  const MAX_ENCODED_ENTITY_ID = 0xFFFFFF;
44
50
  let warnedEntityIdRange = false;
45
51
  /**
@@ -56,6 +62,14 @@ export class Renderer {
56
62
  sectionPlaneRenderer = null;
57
63
  section2DOverlayRenderer = null;
58
64
  postProcessor = null;
65
+ edlPass = null;
66
+ edlOptions = {
67
+ enabled: false,
68
+ strength: 1,
69
+ radiusPx: 1,
70
+ highQuality: true,
71
+ };
72
+ pointCloudRenderer = null;
59
73
  visualEnhancementState = {
60
74
  enabled: true,
61
75
  edgeContrast: { enabled: true, intensity: 1.0 },
@@ -113,9 +127,217 @@ export class Renderer {
113
127
  contactRadius: 1.0,
114
128
  contactIntensity: 0.3,
115
129
  }, this.pipeline.getSampleCount());
130
+ this.pointCloudRenderer = new PointCloudRenderer(this.device.getDevice(), this.device.getFormat(), 'depth24plus-stencil8', this.pipeline.getSampleCount());
131
+ this.edlPass = new EdlPass(this.device, this.pipeline.getSampleCount());
116
132
  this.camera.setAspect(width / height);
117
133
  // Update picking manager with initialized picker
118
134
  this.pickingManager.setPicker(this.picker);
135
+ // Provide a snapshot of pickable point nodes per pick. The
136
+ // sizing must mirror the live splat shader so click hit-testing
137
+ // matches what the user actually sees on screen.
138
+ this.pickingManager.setPointPickProvider(() => {
139
+ const pcr = this.pointCloudRenderer;
140
+ if (!pcr || !pcr.hasAssets())
141
+ return null;
142
+ const opts = pcr.getOptions();
143
+ const sizeMode = opts.sizeMode === 'fixed-px' ? 0 : opts.sizeMode === 'adaptive-world' ? 1 : 2;
144
+ return {
145
+ nodes: pcr.getPickNodes(),
146
+ sizing: {
147
+ sizeMode: sizeMode,
148
+ worldRadius: opts.worldRadius,
149
+ pointSizePx: opts.pointSize,
150
+ clickTolerancePx: 2,
151
+ },
152
+ };
153
+ });
154
+ }
155
+ /**
156
+ * Replace all loaded point clouds with `assets`.
157
+ *
158
+ * Phase 0 entry point — single-chunk inline assets from IFCx
159
+ * (`pcd::base64`, `points::array`, `points::base64`). Future phases
160
+ * accept streaming sources via a different overload.
161
+ */
162
+ setPointClouds(assets) {
163
+ if (!this.pointCloudRenderer) {
164
+ throw new Error('Renderer not initialized. Call init() first.');
165
+ }
166
+ this.pointCloudRenderer.setAssets(assets);
167
+ // Replace, not append — bounds may have shrunk (e.g. an IFCx
168
+ // reload with a smaller scan). `expandModelBoundsForPointClouds`
169
+ // alone only grows; recompute from scratch to keep
170
+ // fit-to-view + section-plane sliders accurate.
171
+ this.recomputeModelBounds();
172
+ this.camera.setSceneBounds(this.modelBounds);
173
+ this.requestRender();
174
+ }
175
+ /** Append additional point clouds without clearing existing ones. */
176
+ addPointClouds(assets) {
177
+ if (!this.pointCloudRenderer) {
178
+ throw new Error('Renderer not initialized. Call init() first.');
179
+ }
180
+ for (const asset of assets) {
181
+ this.pointCloudRenderer.addAsset(asset);
182
+ }
183
+ this.expandModelBoundsForPointClouds();
184
+ this.camera.setSceneBounds(this.modelBounds);
185
+ this.requestRender();
186
+ }
187
+ /** Total number of point cloud assets currently uploaded. */
188
+ getPointCloudAssetCount() {
189
+ return this.pointCloudRenderer?.getNodeCount() ?? 0;
190
+ }
191
+ /** Total number of points across all point cloud assets. */
192
+ getPointCloudPointCount() {
193
+ return this.pointCloudRenderer?.getPointCount() ?? 0;
194
+ }
195
+ /** Drop all point cloud GPU resources. */
196
+ clearPointClouds() {
197
+ this.pointCloudRenderer?.clear();
198
+ this.recomputeModelBounds();
199
+ this.camera.setSceneBounds(this.modelBounds);
200
+ this.requestRender();
201
+ }
202
+ /**
203
+ * Streaming entry: open an empty asset that will receive chunks via
204
+ * `appendPointCloudChunk`. Call `endPointCloudStream` when no more
205
+ * chunks will arrive (currently a no-op but kept for symmetry).
206
+ */
207
+ beginPointCloudStream(meta) {
208
+ if (!this.pointCloudRenderer) {
209
+ throw new Error('Renderer not initialized. Call init() first.');
210
+ }
211
+ return this.pointCloudRenderer.beginAsset(meta);
212
+ }
213
+ appendPointCloudChunk(handle, chunk) {
214
+ if (!this.pointCloudRenderer)
215
+ return;
216
+ this.pointCloudRenderer.appendChunk(handle, chunk);
217
+ this.expandModelBoundsForPointClouds();
218
+ this.camera.setSceneBounds(this.modelBounds);
219
+ this.requestRender();
220
+ }
221
+ endPointCloudStream(handle) {
222
+ this.pointCloudRenderer?.endAsset(handle);
223
+ this.requestRender();
224
+ }
225
+ removePointCloudAsset(handle) {
226
+ this.pointCloudRenderer?.removeAsset(handle);
227
+ // Bounds may have shrunk — recompute from scratch so fit-to-view
228
+ // and section-plane sliders see fresh extents.
229
+ this.recomputeModelBounds();
230
+ this.camera.setSceneBounds(this.modelBounds);
231
+ this.requestRender();
232
+ }
233
+ /**
234
+ * Reassign a streamed point-cloud's expressId after upload. Use
235
+ * this when the federation registry assigns a new model offset and
236
+ * the renderer needs to emit the post-offset globalId in picking
237
+ * outputs. The change takes effect on the next render — no GPU
238
+ * buffer rewrite needed.
239
+ */
240
+ relabelPointCloudAsset(handle, newExpressId) {
241
+ this.pointCloudRenderer?.relabelAsset(handle, newExpressId);
242
+ this.requestRender();
243
+ }
244
+ /**
245
+ * Compute model bounds from triangle meshes + remaining point clouds.
246
+ * Called from removeAsset / clear paths so bounds shrink correctly.
247
+ * Triangle meshes still drive the bounds when present (existing
248
+ * Scene-driven path), so this only re-folds in the point cloud
249
+ * extents over whatever the mesh path left.
250
+ */
251
+ recomputeModelBounds() {
252
+ // Always recompute from scratch: take mesh bounds as the
253
+ // baseline, then fold in the CURRENT point-cloud bounds on
254
+ // top. Folding only-up via expandModelBoundsForPointClouds()
255
+ // is correct when pc bounds grow but never shrinks them when
256
+ // an asset is removed, leaving stale oversized extents until
257
+ // every point cloud is gone.
258
+ const meshBounds = this.computeMeshBounds();
259
+ const pcBounds = this.pointCloudRenderer?.getBounds() ?? null;
260
+ if (!meshBounds && !pcBounds) {
261
+ this.modelBounds = null;
262
+ return;
263
+ }
264
+ this.modelBounds = meshBounds ?? {
265
+ min: { x: pcBounds.min[0], y: pcBounds.min[1], z: pcBounds.min[2] },
266
+ max: { x: pcBounds.max[0], y: pcBounds.max[1], z: pcBounds.max[2] },
267
+ };
268
+ if (meshBounds && pcBounds) {
269
+ this.expandModelBoundsForPointClouds();
270
+ }
271
+ }
272
+ /** Aggregate bounds across all batched + individual meshes. Returns
273
+ * null if the scene has no mesh geometry. */
274
+ computeMeshBounds() {
275
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
276
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
277
+ let any = false;
278
+ for (const batch of this.scene.getBatchedMeshes()) {
279
+ if (!batch.bounds)
280
+ continue;
281
+ any = true;
282
+ if (batch.bounds.min[0] < minX)
283
+ minX = batch.bounds.min[0];
284
+ if (batch.bounds.min[1] < minY)
285
+ minY = batch.bounds.min[1];
286
+ if (batch.bounds.min[2] < minZ)
287
+ minZ = batch.bounds.min[2];
288
+ if (batch.bounds.max[0] > maxX)
289
+ maxX = batch.bounds.max[0];
290
+ if (batch.bounds.max[1] > maxY)
291
+ maxY = batch.bounds.max[1];
292
+ if (batch.bounds.max[2] > maxZ)
293
+ maxZ = batch.bounds.max[2];
294
+ }
295
+ if (!any)
296
+ return null;
297
+ return { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } };
298
+ }
299
+ /** Apply rendering options (color mode, fixed override, point size). */
300
+ setPointCloudOptions(opts) {
301
+ this.pointCloudRenderer?.setOptions(opts);
302
+ this.requestRender();
303
+ }
304
+ /**
305
+ * Toggle Eye-Dome Lighting and tune its strength.
306
+ *
307
+ * EDL adds depth perception to point clouds (and meshes) via screen-
308
+ * space depth gradient — silhouette pixels get a soft black halo.
309
+ * Cheap: ~9 texture taps per pixel. Only runs when point clouds are
310
+ * loaded.
311
+ */
312
+ setEdlOptions(opts) {
313
+ if (opts.enabled !== undefined)
314
+ this.edlOptions.enabled = opts.enabled;
315
+ if (opts.strength !== undefined)
316
+ this.edlOptions.strength = Math.max(0, Math.min(3, opts.strength));
317
+ if (opts.radiusPx !== undefined)
318
+ this.edlOptions.radiusPx = Math.max(1, Math.min(4, opts.radiusPx));
319
+ if (opts.highQuality !== undefined)
320
+ this.edlOptions.highQuality = opts.highQuality;
321
+ this.requestRender();
322
+ }
323
+ expandModelBoundsForPointClouds() {
324
+ const pcBounds = this.pointCloudRenderer?.getBounds();
325
+ if (!pcBounds)
326
+ return;
327
+ if (!this.modelBounds) {
328
+ this.modelBounds = {
329
+ min: { x: pcBounds.min[0], y: pcBounds.min[1], z: pcBounds.min[2] },
330
+ max: { x: pcBounds.max[0], y: pcBounds.max[1], z: pcBounds.max[2] },
331
+ };
332
+ return;
333
+ }
334
+ const m = this.modelBounds;
335
+ m.min.x = Math.min(m.min.x, pcBounds.min[0]);
336
+ m.min.y = Math.min(m.min.y, pcBounds.min[1]);
337
+ m.min.z = Math.min(m.min.z, pcBounds.min[2]);
338
+ m.max.x = Math.max(m.max.x, pcBounds.max[0]);
339
+ m.max.y = Math.max(m.max.y, pcBounds.max[1]);
340
+ m.max.z = Math.max(m.max.z, pcBounds.max[2]);
119
341
  }
120
342
  /**
121
343
  * Load geometry from GeometryResult or MeshData array
@@ -553,29 +775,66 @@ export class Renderer {
553
775
  const hasHiddenFilter = options.hiddenIds && options.hiddenIds.size > 0;
554
776
  const hasIsolatedFilter = options.isolatedIds !== null && options.isolatedIds !== undefined;
555
777
  const hasVisibilityFiltering = hasHiddenFilter || hasIsolatedFilter;
778
+ // Build the selected-id set once per frame so the X-Ray override paths
779
+ // can keep highlighted entities at full alpha without per-site checks.
780
+ const selectedId = options.selectedId;
781
+ const selectedIds = options.selectedIds;
782
+ const selectedModelIndex = options.selectedModelIndex;
783
+ const selectedExpressIds = new Set();
784
+ if (selectedId !== undefined && selectedId !== null) {
785
+ selectedExpressIds.add(selectedId);
786
+ }
787
+ if (selectedIds) {
788
+ for (const id of selectedIds) {
789
+ selectedExpressIds.add(id);
790
+ }
791
+ }
792
+ const hasSelected = selectedExpressIds.size > 0;
556
793
  // Per-frame alpha overrides for X-Ray mode. See RenderOptions.transparencyOverrides.
557
- // Callers supply a fresh Map when contents change (same convention as hiddenIds/isolatedIds).
558
- const txOverrides = options.transparencyOverrides;
559
- const hasTxOverrides = txOverrides != null && txOverrides.size > 0;
794
+ // Snapshot the caller's map so mid-frame mutation can't desync classification
795
+ // and uniform-write decisions for the same batch/mesh.
796
+ const txOverridesSrc = options.transparencyOverrides;
797
+ const hasTxOverrides = txOverridesSrc != null && txOverridesSrc.size > 0;
798
+ const txOverrides = hasTxOverrides ? new Map(txOverridesSrc) : null;
560
799
  const alphaForMesh = (expressId, fallback) => {
561
800
  if (!hasTxOverrides)
562
801
  return fallback;
802
+ // Selected meshes are exempt — the highlight pass renders them last,
803
+ // but exempting here also keeps mesh classification + uniform writes
804
+ // consistent so a selected mesh never enters the transparent pipeline
805
+ // because of its own override entry.
806
+ if (hasSelected && selectedExpressIds.has(expressId))
807
+ return fallback;
563
808
  const a = txOverrides.get(expressId);
564
809
  return a !== undefined ? a : fallback;
565
810
  };
566
- // alphaForBatch walks batch.expressIds per frame. For typical batch sizes
567
- // the cost is well below noise vs. the GPU work, and avoiding a cache
568
- // removes the stale-on-mutation bug class entirely.
811
+ // Cache resolved batch alpha for the frame: classification needs it
812
+ // (opaque vs transparent routing) and renderBatch needs it for the
813
+ // uniform write. Without the cache we'd walk batch.expressIds twice
814
+ // per batch per frame, which becomes the dominant JS cost in X-Ray.
815
+ const batchAlphaCache = hasTxOverrides
816
+ ? new WeakMap()
817
+ : null;
569
818
  const alphaForBatch = (batch, fallback) => {
570
819
  if (!hasTxOverrides)
571
820
  return fallback;
821
+ const cached = batchAlphaCache.get(batch);
822
+ if (cached !== undefined)
823
+ return cached;
572
824
  let minAlpha = Infinity;
573
825
  for (const eid of batch.expressIds) {
826
+ // Selected ids never drag down a batch's alpha — the highlight
827
+ // pass redraws them on top, but excluding here also means a
828
+ // batch made entirely of selected entities stays opaque.
829
+ if (hasSelected && selectedExpressIds.has(eid))
830
+ continue;
574
831
  const a = txOverrides.get(eid);
575
832
  if (a !== undefined && a < minAlpha)
576
833
  minAlpha = a;
577
834
  }
578
- return minAlpha === Infinity ? fallback : minAlpha;
835
+ const resolved = minAlpha === Infinity ? fallback : minAlpha;
836
+ batchAlphaCache.set(batch, resolved);
837
+ return resolved;
579
838
  };
580
839
  // PERFORMANCE FIX: Use batch-level visibility filtering instead of creating individual meshes
581
840
  // Only create individual meshes for selected elements (for highlighting)
@@ -645,9 +904,6 @@ export class Renderer {
645
904
  // Write uniform data to each mesh's buffer BEFORE recording commands
646
905
  // This ensures each mesh has its own color data
647
906
  const allMeshes = [...opaqueMeshes, ...transparentMeshes];
648
- const selectedId = options.selectedId;
649
- const selectedIds = options.selectedIds;
650
- const selectedModelIndex = options.selectedModelIndex;
651
907
  // Calculate section plane parameters and model bounds
652
908
  // Always calculate bounds when sectionPlane is provided (for preview and active mode)
653
909
  let sectionPlaneData;
@@ -682,6 +938,21 @@ export class Renderer {
682
938
  boundsMax.z = Math.max(boundsMax.z, batch.bounds.max[2]);
683
939
  }
684
940
  }
941
+ // Fold in point-cloud bounds too — without this, a
942
+ // pure point-cloud scene falls through to the default
943
+ // [-100,100], and a mixed scene clips against a
944
+ // smaller mesh-only range while the point pipeline
945
+ // (which honours the same sectionPlaneData) keeps
946
+ // drawing points outside the slider's reach.
947
+ const pcBoundsForSection = this.pointCloudRenderer?.getBounds();
948
+ if (pcBoundsForSection) {
949
+ boundsMin.x = Math.min(boundsMin.x, pcBoundsForSection.min[0]);
950
+ boundsMin.y = Math.min(boundsMin.y, pcBoundsForSection.min[1]);
951
+ boundsMin.z = Math.min(boundsMin.z, pcBoundsForSection.min[2]);
952
+ boundsMax.x = Math.max(boundsMax.x, pcBoundsForSection.max[0]);
953
+ boundsMax.y = Math.max(boundsMax.y, pcBoundsForSection.max[1]);
954
+ boundsMax.z = Math.max(boundsMax.z, pcBoundsForSection.max[2]);
955
+ }
685
956
  // If no batched meshes have bounds yet (streaming, degenerate
686
957
  // models), fall back to individual meshes so at least the
687
958
  // slider has a workable range.
@@ -961,15 +1232,6 @@ export class Renderer {
961
1232
  opaqueBatches.push(batch);
962
1233
  }
963
1234
  }
964
- const selectedExpressIds = new Set();
965
- if (selectedId !== undefined && selectedId !== null) {
966
- selectedExpressIds.add(selectedId);
967
- }
968
- if (selectedIds) {
969
- for (const id of selectedIds) {
970
- selectedExpressIds.add(id);
971
- }
972
- }
973
1235
  // Build a uniform template ONCE per frame — shared across all batches.
974
1236
  // Only the 4-float color (offset 32) differs per batch; everything else
975
1237
  // (viewProj, identity model, material, section plane, flags) is identical.
@@ -1308,6 +1570,19 @@ export class Renderer {
1308
1570
  }
1309
1571
  }
1310
1572
  }
1573
+ // Draw point clouds (IFCx inline + streamed LAS/LAZ).
1574
+ // Shares the depth buffer + section plane state with the mesh pipeline so
1575
+ // points occlude triangles and vice versa. The splat shader needs the
1576
+ // viewport size to convert pixel sizes into clip-space offsets.
1577
+ if (this.pointCloudRenderer && this.pointCloudRenderer.hasAssets()) {
1578
+ this.pointCloudRenderer.draw(pass, {
1579
+ viewProj,
1580
+ sectionPlane: sectionPlaneData
1581
+ ? { ...sectionPlaneData, flipped: options.sectionPlane?.flipped === true }
1582
+ : null,
1583
+ viewport: { width: this.canvas.width, height: this.canvas.height },
1584
+ });
1585
+ }
1311
1586
  // Draw section plane visual BEFORE pass.end() (within same MSAA render pass)
1312
1587
  // Always show plane when sectionPlane options are provided (as preview or active)
1313
1588
  const modelBounds = this.getModelBounds();
@@ -1380,6 +1655,21 @@ export class Renderer {
1380
1655
  enableSeparationLines: separationEnabled,
1381
1656
  });
1382
1657
  }
1658
+ // Eye-Dome Lighting — runs AFTER contact/separation so it darkens
1659
+ // every layer uniformly. Cheap (~9 depth taps), only active when
1660
+ // there are point clouds in the scene and the user has enabled it.
1661
+ if (this.edlPass
1662
+ && this.edlOptions.enabled
1663
+ && this.pointCloudRenderer?.hasAssets()) {
1664
+ this.edlPass.apply(encoder, {
1665
+ targetView: textureView,
1666
+ depthView: this.pipeline.getDepthOnlyTextureView(),
1667
+ }, {
1668
+ strength: this.edlOptions.strength,
1669
+ radiusPx: this.edlOptions.radiusPx,
1670
+ highQuality: this.edlOptions.highQuality,
1671
+ });
1672
+ }
1383
1673
  device.queue.submit([encoder.finish()]);
1384
1674
  }
1385
1675
  catch (error) {
@@ -1601,11 +1891,16 @@ export class Renderer {
1601
1891
  // Post-processor uniform buffer
1602
1892
  this.postProcessor?.destroy();
1603
1893
  this.postProcessor = null;
1894
+ this.edlPass?.destroy();
1895
+ this.edlPass = null;
1604
1896
  // Section-plane renderers
1605
1897
  this.sectionPlaneRenderer?.destroy();
1606
1898
  this.sectionPlaneRenderer = null;
1607
1899
  this.section2DOverlayRenderer?.dispose();
1608
1900
  this.section2DOverlayRenderer = null;
1901
+ // Point cloud GPU resources
1902
+ this.pointCloudRenderer?.clear();
1903
+ this.pointCloudRenderer = null;
1609
1904
  // Snap detector geometry cache
1610
1905
  this.raycastEngine.clearCaches();
1611
1906
  }