@ifc-lite/renderer 6.3.3 → 6.5.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 (223) hide show
  1. package/dist/camera-animation.d.ts +7 -1
  2. package/dist/camera-animation.d.ts.map +1 -1
  3. package/dist/camera-animation.js +19 -3
  4. package/dist/camera-animation.js.map +1 -1
  5. package/dist/camera-fit-geometry.d.ts +47 -0
  6. package/dist/camera-fit-geometry.d.ts.map +1 -0
  7. package/dist/camera-fit-geometry.js +44 -0
  8. package/dist/camera-fit-geometry.js.map +1 -0
  9. package/dist/camera-framing.d.ts +27 -21
  10. package/dist/camera-framing.d.ts.map +1 -1
  11. package/dist/camera-framing.js +47 -44
  12. package/dist/camera-framing.js.map +1 -1
  13. package/dist/camera-preset-view.d.ts +18 -0
  14. package/dist/camera-preset-view.d.ts.map +1 -1
  15. package/dist/camera-preset-view.js +42 -0
  16. package/dist/camera-preset-view.js.map +1 -1
  17. package/dist/camera.d.ts +14 -1
  18. package/dist/camera.d.ts.map +1 -1
  19. package/dist/camera.js +14 -2
  20. package/dist/camera.js.map +1 -1
  21. package/dist/clip-types.d.ts +35 -0
  22. package/dist/clip-types.d.ts.map +1 -0
  23. package/dist/clip-types.js +5 -0
  24. package/dist/clip-types.js.map +1 -0
  25. package/dist/device-recovery.js +11 -1
  26. package/dist/device-recovery.js.map +1 -1
  27. package/dist/device.d.ts +4 -4
  28. package/dist/device.js +9 -9
  29. package/dist/device.js.map +1 -1
  30. package/dist/extra-clip-planes.d.ts +57 -0
  31. package/dist/extra-clip-planes.d.ts.map +1 -0
  32. package/dist/extra-clip-planes.js +161 -0
  33. package/dist/extra-clip-planes.js.map +1 -0
  34. package/dist/gpu-counters.d.ts +4 -2
  35. package/dist/gpu-counters.d.ts.map +1 -1
  36. package/dist/gpu-counters.js +9 -6
  37. package/dist/gpu-counters.js.map +1 -1
  38. package/dist/index.d.ts +19 -5
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +69 -90
  41. package/dist/index.js.map +1 -1
  42. package/dist/mesh-rte-uniforms.d.ts +7 -3
  43. package/dist/mesh-rte-uniforms.d.ts.map +1 -1
  44. package/dist/mesh-rte-uniforms.js +16 -2
  45. package/dist/mesh-rte-uniforms.js.map +1 -1
  46. package/dist/opaque-visibility-commands.d.ts +3 -0
  47. package/dist/opaque-visibility-commands.d.ts.map +1 -0
  48. package/dist/opaque-visibility-commands.js +11 -0
  49. package/dist/opaque-visibility-commands.js.map +1 -0
  50. package/dist/opaque-visibility-pass.d.ts +37 -0
  51. package/dist/opaque-visibility-pass.d.ts.map +1 -0
  52. package/dist/opaque-visibility-pass.js +230 -0
  53. package/dist/opaque-visibility-pass.js.map +1 -0
  54. package/dist/pick-types.d.ts +5 -1
  55. package/dist/pick-types.d.ts.map +1 -1
  56. package/dist/picker-rte-uniforms.d.ts +1 -1
  57. package/dist/picker-rte-uniforms.d.ts.map +1 -1
  58. package/dist/picker-rte-uniforms.js +4 -2
  59. package/dist/picker-rte-uniforms.js.map +1 -1
  60. package/dist/picker-shaders.d.ts +5 -0
  61. package/dist/picker-shaders.d.ts.map +1 -0
  62. package/dist/picker-shaders.js +157 -0
  63. package/dist/picker-shaders.js.map +1 -0
  64. package/dist/picker.d.ts +3 -0
  65. package/dist/picker.d.ts.map +1 -1
  66. package/dist/picker.js +43 -133
  67. package/dist/picker.js.map +1 -1
  68. package/dist/pipeline.d.ts +5 -9
  69. package/dist/pipeline.d.ts.map +1 -1
  70. package/dist/pipeline.js +34 -29
  71. package/dist/pipeline.js.map +1 -1
  72. package/dist/point-picker.d.ts +2 -1
  73. package/dist/point-picker.d.ts.map +1 -1
  74. package/dist/point-picker.js +12 -4
  75. package/dist/point-picker.js.map +1 -1
  76. package/dist/pointcloud/point-cloud-renderer.d.ts +4 -4
  77. package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
  78. package/dist/pointcloud/point-cloud-renderer.js +1 -1
  79. package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
  80. package/dist/pointcloud/point-cloud-uniforms.d.ts +3 -1
  81. package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
  82. package/dist/pointcloud/point-cloud-uniforms.js +3 -1
  83. package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
  84. package/dist/pointcloud/point-pipeline.d.ts +3 -1
  85. package/dist/pointcloud/point-pipeline.d.ts.map +1 -1
  86. package/dist/pointcloud/point-pipeline.js +4 -1
  87. package/dist/pointcloud/point-pipeline.js.map +1 -1
  88. package/dist/pointcloud/point-shader.wgsl.d.ts +1 -1
  89. package/dist/pointcloud/point-shader.wgsl.d.ts.map +1 -1
  90. package/dist/pointcloud/point-shader.wgsl.js +8 -0
  91. package/dist/pointcloud/point-shader.wgsl.js.map +1 -1
  92. package/dist/post-pass-chain.d.ts +1 -0
  93. package/dist/post-pass-chain.d.ts.map +1 -1
  94. package/dist/post-pass-chain.js +1 -0
  95. package/dist/post-pass-chain.js.map +1 -1
  96. package/dist/quantized-lifecycle.d.ts +6 -0
  97. package/dist/quantized-lifecycle.d.ts.map +1 -0
  98. package/dist/quantized-lifecycle.js +19 -0
  99. package/dist/quantized-lifecycle.js.map +1 -0
  100. package/dist/raycast-engine.d.ts.map +1 -1
  101. package/dist/raycast-engine.js +3 -2
  102. package/dist/raycast-engine.js.map +1 -1
  103. package/dist/render-section-draw.d.ts +5 -0
  104. package/dist/render-section-draw.d.ts.map +1 -1
  105. package/dist/render-section-draw.js +59 -25
  106. package/dist/render-section-draw.js.map +1 -1
  107. package/dist/render-section-plane.d.ts +5 -33
  108. package/dist/render-section-plane.d.ts.map +1 -1
  109. package/dist/render-section-plane.js +8 -175
  110. package/dist/render-section-plane.js.map +1 -1
  111. package/dist/renderer-frame-timing.d.ts +1 -0
  112. package/dist/renderer-frame-timing.d.ts.map +1 -1
  113. package/dist/renderer-frame-timing.js +8 -1
  114. package/dist/renderer-frame-timing.js.map +1 -1
  115. package/dist/renderer-overlays.d.ts +8 -2
  116. package/dist/renderer-overlays.d.ts.map +1 -1
  117. package/dist/renderer-overlays.js +22 -45
  118. package/dist/renderer-overlays.js.map +1 -1
  119. package/dist/scene-raycaster.d.ts +1 -1
  120. package/dist/scene-raycaster.d.ts.map +1 -1
  121. package/dist/scene-raycaster.js +18 -44
  122. package/dist/scene-raycaster.js.map +1 -1
  123. package/dist/scene-rect-select.d.ts.map +1 -1
  124. package/dist/scene-rect-select.js +2 -1
  125. package/dist/scene-rect-select.js.map +1 -1
  126. package/dist/section-2d-lift.d.ts +25 -0
  127. package/dist/section-2d-lift.d.ts.map +1 -1
  128. package/dist/section-2d-lift.js +35 -0
  129. package/dist/section-2d-lift.js.map +1 -1
  130. package/dist/section-2d-overlay.d.ts +41 -12
  131. package/dist/section-2d-overlay.d.ts.map +1 -1
  132. package/dist/section-2d-overlay.js +121 -179
  133. package/dist/section-2d-overlay.js.map +1 -1
  134. package/dist/section-2d-types.d.ts +2 -0
  135. package/dist/section-2d-types.d.ts.map +1 -1
  136. package/dist/section-cap-geometry.d.ts +33 -0
  137. package/dist/section-cap-geometry.d.ts.map +1 -0
  138. package/dist/section-cap-geometry.js +137 -0
  139. package/dist/section-cap-geometry.js.map +1 -0
  140. package/dist/section-cap-set.d.ts +60 -0
  141. package/dist/section-cap-set.d.ts.map +1 -0
  142. package/dist/section-cap-set.js +212 -0
  143. package/dist/section-cap-set.js.map +1 -0
  144. package/dist/section-cap-style.d.ts +3 -0
  145. package/dist/section-cap-style.d.ts.map +1 -1
  146. package/dist/section-cap-style.js.map +1 -1
  147. package/dist/section-cap-upload.d.ts +16 -0
  148. package/dist/section-cap-upload.d.ts.map +1 -0
  149. package/dist/section-cap-upload.js +52 -0
  150. package/dist/section-cap-upload.js.map +1 -0
  151. package/dist/section-exclusions.d.ts +26 -0
  152. package/dist/section-exclusions.d.ts.map +1 -0
  153. package/dist/section-exclusions.js +95 -0
  154. package/dist/section-exclusions.js.map +1 -0
  155. package/dist/section-plane-frames.d.ts +48 -0
  156. package/dist/section-plane-frames.d.ts.map +1 -0
  157. package/dist/section-plane-frames.js +49 -0
  158. package/dist/section-plane-frames.js.map +1 -0
  159. package/dist/section-plane-resolve.d.ts +63 -0
  160. package/dist/section-plane-resolve.d.ts.map +1 -0
  161. package/dist/section-plane-resolve.js +200 -0
  162. package/dist/section-plane-resolve.js.map +1 -0
  163. package/dist/selection-mask-pass.d.ts +2 -0
  164. package/dist/selection-mask-pass.d.ts.map +1 -1
  165. package/dist/selection-mask-pass.js +16 -4
  166. package/dist/selection-mask-pass.js.map +1 -1
  167. package/dist/selection-mask-pipelines.d.ts +5 -1
  168. package/dist/selection-mask-pipelines.d.ts.map +1 -1
  169. package/dist/selection-mask-pipelines.js +27 -3
  170. package/dist/selection-mask-pipelines.js.map +1 -1
  171. package/dist/selection-outline-frame.d.ts +3 -1
  172. package/dist/selection-outline-frame.d.ts.map +1 -1
  173. package/dist/selection-outline-frame.js +2 -1
  174. package/dist/selection-outline-frame.js.map +1 -1
  175. package/dist/shaders/depth-nudge.wgsl.d.ts +2 -2
  176. package/dist/shaders/depth-nudge.wgsl.d.ts.map +1 -1
  177. package/dist/shaders/depth-nudge.wgsl.js +1 -18
  178. package/dist/shaders/depth-nudge.wgsl.js.map +1 -1
  179. package/dist/shaders/main.wgsl.d.ts +1 -1
  180. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  181. package/dist/shaders/main.wgsl.js +9 -1
  182. package/dist/shaders/main.wgsl.js.map +1 -1
  183. package/dist/shaders/mesh-uniforms.wgsl.d.ts +1 -1
  184. package/dist/shaders/mesh-uniforms.wgsl.d.ts.map +1 -1
  185. package/dist/shaders/mesh-uniforms.wgsl.js +11 -3
  186. package/dist/shaders/mesh-uniforms.wgsl.js.map +1 -1
  187. package/dist/shaders/opaque-visibility.wgsl.d.ts +11 -0
  188. package/dist/shaders/opaque-visibility.wgsl.d.ts.map +1 -0
  189. package/dist/shaders/opaque-visibility.wgsl.js +55 -0
  190. package/dist/shaders/opaque-visibility.wgsl.js.map +1 -0
  191. package/dist/shaders/section-2d-overlay.wgsl.d.ts +11 -5
  192. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
  193. package/dist/shaders/section-2d-overlay.wgsl.js +31 -5
  194. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
  195. package/dist/shaders/selection-mask.wgsl.d.ts +1 -1
  196. package/dist/shaders/selection-mask.wgsl.d.ts.map +1 -1
  197. package/dist/shaders/selection-mask.wgsl.js +10 -4
  198. package/dist/shaders/selection-mask.wgsl.js.map +1 -1
  199. package/dist/shaders/shadow.wgsl.d.ts +1 -1
  200. package/dist/shaders/shadow.wgsl.d.ts.map +1 -1
  201. package/dist/shaders/shadow.wgsl.js +22 -7
  202. package/dist/shaders/shadow.wgsl.js.map +1 -1
  203. package/dist/shaders/symbolic-overlay.wgsl.d.ts +1 -1
  204. package/dist/shaders/symbolic-overlay.wgsl.d.ts.map +1 -1
  205. package/dist/shadow-occluders.d.ts.map +1 -1
  206. package/dist/shadow-occluders.js +4 -0
  207. package/dist/shadow-occluders.js.map +1 -1
  208. package/dist/shadow-pass.d.ts +2 -5
  209. package/dist/shadow-pass.d.ts.map +1 -1
  210. package/dist/shadow-pass.js +17 -17
  211. package/dist/shadow-pass.js.map +1 -1
  212. package/dist/shadow-types.d.ts +7 -0
  213. package/dist/shadow-types.d.ts.map +1 -1
  214. package/dist/shadow-types.js.map +1 -1
  215. package/dist/source-curve-clipping.d.ts.map +1 -1
  216. package/dist/source-curve-clipping.js +7 -1
  217. package/dist/source-curve-clipping.js.map +1 -1
  218. package/dist/source-curve-snap.d.ts.map +1 -1
  219. package/dist/source-curve-snap.js +2 -1
  220. package/dist/source-curve-snap.js.map +1 -1
  221. package/dist/types.d.ts +12 -16
  222. package/dist/types.d.ts.map +1 -1
  223. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /* This Source Code Form is subject to the terms of the Mozilla Public
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ import { sectionExclusions } from './section-exclusions.js';
4
5
  /**
5
6
  * @ifc-lite/renderer - WebGPU renderer
6
7
  */
@@ -10,6 +11,7 @@ export { Camera } from './camera.js';
10
11
  export { appearanceSourceTriangle, expandAppearanceCorners, equivalentAppearanceGeometry } from './appearance-uvs.js';
11
12
  export { sameCompanionParts } from './appearance-companions.js';
12
13
  export { invertAppearancePartition, validateAppearancePartition } from './appearance-preview.js';
14
+ import { deferOpaqueVisibilityFrame } from './opaque-visibility-pass.js';
13
15
  import { createReferenceImageManager } from './reference-image-host.js';
14
16
  import { MAX_DRAWING_BUFFER_PIXEL_RATIO, measureDrawingBuffer, resizeRendererViewport } from './renderer-viewport.js';
15
17
  export { pickFitPolicy } from './camera-fit-policy.js';
@@ -35,6 +37,7 @@ export { SymbolicTextAtlas } from './symbolic-text-atlas.js';
35
37
  // holds the styling primitives shared with the store and UI.
36
38
  export { DEFAULT_CAP_STYLE, HATCH_PATTERN_IDS } from './section-cap-style.js';
37
39
  export { planeBasis, nearestCardinalAxis } from './section-plane-basis.js';
40
+ export { capHighlightKey } from './section-2d-overlay.js';
38
41
  export { LINE_OVERLAY_CHANNELS } from './section-2d-overlay.js';
39
42
  export { Raycaster } from './raycaster.js';
40
43
  export { SnapDetector, SnapType } from './snap-detector.js';
@@ -42,6 +45,7 @@ export { BVH } from './bvh.js';
42
45
  export { hostsOtherEntities } from './mesh-entity-hosting.js';
43
46
  export { FederationRegistry, federationRegistry } from './federation-registry.js';
44
47
  export * from './types.js';
48
+ export { MAX_EXTRA_CLIP_PLANES } from './extra-clip-planes.js';
45
49
  export { resolveEnvironment, deriveSkyGradient, packEnvironmentUniforms, ENVIRONMENT_UNIFORM_SIZE, } from './environment.js';
46
50
  export { DEFAULT_OVERLAY_THEME } from './overlay-theme.js';
47
51
  import { DEFAULT_OVERLAY_THEME } from './overlay-theme.js';
@@ -58,6 +62,7 @@ export { isEntityVisible } from './entity-visibility.js';
58
62
  // Alpha constants the Cesium world view must match, since it renders the model
59
63
  // through its own glTF pipeline rather than this one (#2591).
60
64
  export { DEFAULT_GHOST_ALPHA, OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
65
+ export { DEFAULT_MIN_CAST_ALPHA } from './shadow-occluders.js';
61
66
  export { VisibilityEpochTracker } from './visibility-epoch.js';
62
67
  export { decideTimingMode, passDurationsMs, frameTotalMs, aggregateFrameTimings } from './frame-timing.js';
63
68
  export { computeDurationStats, nsToMs, isNegativeDelta } from './frame-timing-stats.js';
@@ -81,7 +86,8 @@ import { viewBasis } from './math.js';
81
86
  import { isRteAabbVisible, rteFrustum, sourceFrustumFromRte } from './rte-frustum.js';
82
87
  import { VisualEnhancementResolver, livePostEffects } from './visual-enhancement.js';
83
88
  import { packClipBox } from './clip-box.js';
84
- import { MESH_FLAGS_BYTE_OFFSET, MESH_FLAG_RTE_DRAWABLE, MESH_UNIFORM_FLOATS, MESH_UNIFORM_OFFSET, packRteCameraOrigin, packRteFragmentSpace, } from './mesh-rte-uniforms.js';
89
+ import { resolveSectionPlaneFrames } from './section-plane-frames.js';
90
+ import { MESH_FLAGS_BYTE_OFFSET, MESH_FLAG_RTE_DRAWABLE, MESH_UNIFORM_FLOATS, MESH_UNIFORM_OFFSET, packRteCameraOrigin, packRteFragmentSpace, packSectionPlaneLanes, } from './mesh-rte-uniforms.js';
85
91
  import { packMeshMaterial } from './mesh-material.js';
86
92
  import { RendererOverlays } from './renderer-overlays.js';
87
93
  import { resolveSectionPlaneFrame } from './render-section-plane.js';
@@ -117,6 +123,7 @@ import { appendPointCloudChunk as appendPointCloudChunkImpl, beginPointCloudStre
117
123
  import { DeviationComputer } from './deviation/deviation-computer.js';
118
124
  export { computeDeviationStatistics, computeDeviationStatisticsAsync, countWithinToleranceAsync, deviationHistogramAsync, summarizeDeviationAssetsAsync } from './deviation/deviation-statistics.js';
119
125
  import { runGuardedGpuUpload, isDeviceLossThrow } from './gpu-upload-guard.js';
126
+ import { enableRendererQuantizedBatches } from './quantized-lifecycle.js';
120
127
  import { recoverRendererDevice, rendererDeviceLostError } from './device-recovery.js';
121
128
  /**
122
129
  * The reason `whenReady()` rejects when the renderer is destroyed.
@@ -405,13 +412,17 @@ export class Renderer {
405
412
  // One 336-byte buffer serves all frame batches/meshes (84 floats including RTE lanes).
406
413
  uniformScratch = new Float32Array(MESH_UNIFORM_FLOATS);
407
414
  uniformScratchU32 = new Uint32Array(this.uniformScratch.buffer, MESH_FLAGS_BYTE_OFFSET, 4);
408
- // overrideParams lanes (#6076): zero except inside a painting renderBatch call.
415
+ // overrideParams: full entity anchor for clipping, plus colour-override mode (#6076).
409
416
  uniformOverrideU32 = new Uint32Array(this.uniformScratch.buffer, MESH_UNIFORM_OFFSET.overrideParams * 4, 4);
410
417
  // What the last render() actually clipped, so the GPU picker can mirror it and
411
418
  // section/crop-clipped geometry stays unpickable, not just invisible. Updated
412
419
  // every render; read by pick()/pickRect(). null = nothing clipped that frame.
413
420
  _activePickSection = null;
414
421
  _activePickClipBox = null;
422
+ _sectionExcludedIds = new Set();
423
+ /** Snapshot of the extra planes the last frame clipped against (#7545), mirrored by the pickers. */
424
+ _activePickExtraPlanes = null;
425
+ _sectionFrameInput = null; // what the last frame resolved the section planes from
415
426
  constructor(canvas) {
416
427
  this.canvas = canvas;
417
428
  this.device = new WebGPUDevice();
@@ -451,6 +462,7 @@ export class Renderer {
451
462
  // init there is nothing to invalidate and this is a no-op.
452
463
  const generation = ++this.initGeneration;
453
464
  this.ready = false;
465
+ this.scene.setQuantizedBatches(false); // #7655: admission belongs to the retired GPU lifetime.
454
466
  // Re-arm `whenReady()`: this instance is being brought back up, so a
455
467
  // wait requested from here on is "not ready yet" again, not "destroyed".
456
468
  this.destroyed = false;
@@ -633,8 +645,8 @@ export class Renderer {
633
645
  w.reject(error);
634
646
  }
635
647
  /**
636
- * Subscribe to non-intentional GPU device loss (driver reset / VRAM
637
- * exhaustion — NOT an intentional `destroy()`). Fired at most once per
648
+ * Subscribe to GPU device loss outside owned teardown (including browser
649
+ * destruction, driver reset or VRAM exhaustion). Fired at most once per
638
650
  * device. After it fires, `render()` is a no-op and the renderer reports
639
651
  * itself un-ready (`isReady()` false, `whenReady()` rejecting with
640
652
  * `RendererDeviceLostError`) until it is re-initialised, so the typical
@@ -1317,13 +1329,7 @@ export class Renderer {
1317
1329
  * rejecting pipeline creation) batches stay on the f32 path.
1318
1330
  */
1319
1331
  async enableQuantizedBatches() {
1320
- this.recovery.quantizedBatchesRequested = true;
1321
- if (!this.pipeline)
1322
- return false;
1323
- const ok = await this.pipeline.ensureQuantizedPipelines();
1324
- if (ok)
1325
- this.scene.setQuantizedBatches(true);
1326
- return ok;
1332
+ return enableRendererQuantizedBatches(this);
1327
1333
  }
1328
1334
  /** Draw a frame through the shared submission path, preserving the void API. */
1329
1335
  render(options = {}) { this.renderWithResult(options); }
@@ -1569,10 +1575,6 @@ export class Renderer {
1569
1575
  // would never show it. See issue #677. Pure routing decision lives in
1570
1576
  // overlay-routing.ts and is unit-tested there.
1571
1577
  const colorOverrides = this.scene.getColorOverrides();
1572
- // PERFORMANCE FIX: Use batch-level visibility filtering instead of creating individual meshes
1573
- // Only create individual meshes for selected elements (for highlighting)
1574
- // Batches are filtered at render time - fully visible batches render normally,
1575
- // partially visible batches are skipped (their visible elements will be in other batches or individual meshes)
1576
1578
  // Ensure all existing meshes have GPU resources
1577
1579
  this.ensureMeshResources();
1578
1580
  // Frustum culling (if enabled and spatial index available)
@@ -1666,6 +1668,9 @@ export class Renderer {
1666
1668
  spendLogLatch: () => { this._loggedSectionBounds = true; },
1667
1669
  });
1668
1670
  const sectionPlaneData = sectionFrame.sectionPlaneData;
1671
+ const extraClipPlanes = sectionFrame.extraPlanes;
1672
+ const { bounds: frameBounds } = sectionFrame;
1673
+ this._sectionFrameInput = frameBounds && { primary: options.sectionPlane ?? null, extras: options.extraSectionPlanes ?? [], bounds: frameBounds, buildingRotation: options.buildingRotation };
1669
1674
  if (sectionFrame.bounds) {
1670
1675
  // Store bounds for section plane visual and camera near/far.
1671
1676
  // Two wrappers over the same min/max, exactly as before the
@@ -1688,6 +1693,9 @@ export class Renderer {
1688
1693
  flipped: !!options.sectionPlane?.flipped,
1689
1694
  }
1690
1695
  : null;
1696
+ this._sectionExcludedIds = new Set(options.sectionExcludedIds);
1697
+ sectionExclusions(device).update(this._sectionExcludedIds);
1698
+ this._activePickExtraPlanes = extraClipPlanes.length > 0 ? extraClipPlanes.map((p) => ({ normal: [...p.normal], distance: p.distance, flipped: p.flipped })) : null;
1691
1699
  this._activePickClipBox = options.clipBox?.enabled
1692
1700
  ? {
1693
1701
  enabled: true,
@@ -1722,18 +1730,7 @@ export class Renderer {
1722
1730
  meshBuf[35] = isSelected ? mesh.color[3] : alphaForMesh(mesh.expressId, mesh.color[3]);
1723
1731
  packMeshMaterial(meshBuf, mesh.color[3], mesh.finish ?? mesh.material);
1724
1732
  // Section plane data (offset 40-43)
1725
- if (sectionPlaneData) {
1726
- meshBuf[40] = sectionPlaneData.normal[0];
1727
- meshBuf[41] = sectionPlaneData.normal[1];
1728
- meshBuf[42] = sectionPlaneData.normal[2];
1729
- meshBuf[43] = sectionPlaneData.distance;
1730
- }
1731
- else {
1732
- meshBuf[40] = 0;
1733
- meshBuf[41] = 0;
1734
- meshBuf[42] = 0;
1735
- meshBuf[43] = 0;
1736
- }
1733
+ packSectionPlaneLanes(sectionPlaneData, meshBuf);
1737
1734
  // Clip box (offset 48-55: min.xyz + pad, max.xyz + pad) → enable bit
1738
1735
  const clipBit = packClipBox(options.clipBox, meshBuf, 48);
1739
1736
  // Flags (offset 44-47 as u32)
@@ -1747,7 +1744,7 @@ export class Renderer {
1747
1744
  meshFlags[3] = 0;
1748
1745
  // Individual meshes retain their origin and share the RTE
1749
1746
  // fragment contract with colour batches.
1750
- packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, meshBuf);
1747
+ packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, meshBuf, extraClipPlanes);
1751
1748
  const meshOrigin = mesh.rteOrigin
1752
1749
  ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
1753
1750
  if (!relativeToEyeFrame.tryPackDrawableOrigin(meshOrigin, meshBuf, MESH_UNIFORM_OFFSET.drawableDelta)) {
@@ -1755,6 +1752,7 @@ export class Renderer {
1755
1752
  continue;
1756
1753
  }
1757
1754
  meshFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
1755
+ new Uint32Array(meshBuf.buffer)[MESH_UNIFORM_OFFSET.overrideParams] = mesh.expressId;
1758
1756
  device.queue.writeBuffer(mesh.uniformBuffer, 0, meshBuf);
1759
1757
  }
1760
1758
  }
@@ -1848,6 +1846,7 @@ export class Renderer {
1848
1846
  // floor it no longer covers. `sectionPlaneData` already
1849
1847
  // folds in the terrain clip, so that is covered as well.
1850
1848
  this.shadowPass.render(encoder, fit.lightViewProj, occluders, {
1849
+ sectionExcludedIds: this._sectionExcludedIds,
1851
1850
  section: sectionPlaneData?.enabled
1852
1851
  ? {
1853
1852
  normal: sectionPlaneData.normal,
@@ -1858,6 +1857,7 @@ export class Renderer {
1858
1857
  box: options.clipBox?.enabled
1859
1858
  ? { min: options.clipBox.min, max: options.clipBox.max }
1860
1859
  : null,
1860
+ extraPlanes: extraClipPlanes,
1861
1861
  }, { cameraWorld: shadowEye });
1862
1862
  // Shadow uniform: light matrix + sampling params. The kernel
1863
1863
  // width follows the sun's angular size (physical, ~0.53°
@@ -1925,7 +1925,7 @@ export class Renderer {
1925
1925
  clearValue: { r: 0, g: 0, b: 0, a: 0 },
1926
1926
  storeOp: (needsObjectIdPass ? 'store' : 'discard'),
1927
1927
  });
1928
- const pass = encoder.beginRenderPass({
1928
+ const pass = this.pipeline.visibility.begin(encoder, {
1929
1929
  timestampWrites: renderPassTimestampWrites(encoder, 'main'),
1930
1930
  colorAttachments,
1931
1931
  depthStencilAttachment: {
@@ -1939,7 +1939,7 @@ export class Renderer {
1939
1939
  stencilLoadOp: 'clear',
1940
1940
  stencilStoreOp: 'store',
1941
1941
  },
1942
- });
1942
+ }, this.camera.getProjectionMode() === 'orthographic', this.canvas.width, this.canvas.height);
1943
1943
  // Global lighting environment: write the uniform once per frame.
1944
1944
  // The group(1) bind is deferred until AFTER the sky pass below —
1945
1945
  // the sky pipeline has an incompatible layout (its own group(0),
@@ -1995,6 +1995,7 @@ export class Renderer {
1995
1995
  // valid for every main-family draw that follows.
1996
1996
  this.pipeline.setEntityColorTableBuffer(this.scene.getEntityColorTable().getBuffer());
1997
1997
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
1998
+ pass.setBindGroup(3, sectionExclusions(device).bindGroup);
1998
1999
  // Selected meshes drawn this frame, for the selection-mask pass
1999
2000
  // (#5390) after `pass.end()` below. Populated inside the batched
2000
2001
  // branch, where the highlight draw itself happens; empty in the
@@ -2183,9 +2184,6 @@ export class Renderer {
2183
2184
  if (partialEpochChanged) {
2184
2185
  this.scene.retireUnusedAlphaSlots(partialRequests.requestedKeys());
2185
2186
  }
2186
- // Build a uniform template ONCE per frame — shared across all batches.
2187
- // Only the 4-float color (offset 32) differs per batch; everything else
2188
- // (viewProj, identity model, material, section plane, flags) is identical.
2189
2187
  const tpl = this.uniformScratch;
2190
2188
  const tplFlags = this.uniformScratchU32;
2191
2189
  tpl.set(viewProj, 0);
@@ -2211,18 +2209,7 @@ export class Renderer {
2211
2209
  // Colour + material row (tpl[32..39]) are patched per draw; the
2212
2210
  // instanced passes keep this opaque default.
2213
2211
  packMeshMaterial(tpl);
2214
- if (sectionPlaneData) {
2215
- tpl[40] = sectionPlaneData.normal[0];
2216
- tpl[41] = sectionPlaneData.normal[1];
2217
- tpl[42] = sectionPlaneData.normal[2];
2218
- tpl[43] = sectionPlaneData.distance;
2219
- }
2220
- else {
2221
- tpl[40] = 0;
2222
- tpl[41] = 0;
2223
- tpl[42] = 0;
2224
- tpl[43] = 0;
2225
- }
2212
+ packSectionPlaneLanes(sectionPlaneData, tpl);
2226
2213
  // Clip box (offset 48-55: min.xyz + pad, max.xyz + pad) → enable bit
2227
2214
  const tplClipBit = packClipBox(options.clipBox, tpl, 48);
2228
2215
  // flags layout (main shader):
@@ -2243,7 +2230,7 @@ export class Renderer {
2243
2230
  // camera-relative frame. Their fragment clip inputs must use
2244
2231
  // that same frame; mixing a local vertex with a 5,000 km f32
2245
2232
  // world plane loses centimetre cuts before the comparison.
2246
- packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2233
+ packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl, extraClipPlanes);
2247
2234
  // Helper function to render a batch — patches color into the shared template.
2248
2235
  // `paint` = a depth-writing opaque draw: colour overrides shade it from the
2249
2236
  // entity colour table (#6076); transparent draws keep their own colour.
@@ -2257,12 +2244,7 @@ export class Renderer {
2257
2244
  tpl[34] = batch.color[2];
2258
2245
  tpl[35] = alphaForBatch(batch, batch.color[3]);
2259
2246
  packMeshMaterial(tpl, batch.color[3], batch.finish);
2260
- // Per-batch local frame: the batch's vertices are stored
2261
- // RELATIVE to batch.origin (f32-small), so set the model
2262
- // matrix translation column to origin (world = origin + pos).
2263
- // The 12 rotation/scale floats stay identity from the template;
2264
- // the translation lives at column-major indices 12/13/14 →
2265
- // tpl[28/29/30]. [0,0,0] for legacy absolute batches.
2247
+ // Rebase batch-local vertices; translation lives in tpl[28..30].
2266
2248
  const o = batch.origin;
2267
2249
  tpl[28] = o ? o[0] : 0;
2268
2250
  tpl[29] = o ? o[1] : 0;
@@ -2281,7 +2263,8 @@ export class Renderer {
2281
2263
  tpl[58] = qz ? qz.min[2] : 0;
2282
2264
  tpl[59] = qz ? qz.step : 0;
2283
2265
  const painted = paint && batchHasColorOverride(batch, colorOverrides, colorOverrideGen);
2284
- packOverrideParams(this.uniformOverrideU32, painted ? batchEntityIdAnchor(batch) : null, painted, options.emphasizeOverrides === true);
2266
+ packOverrideParams(this.uniformOverrideU32, batchEntityIdAnchor(batch), painted, options.emphasizeOverrides === true);
2267
+ this.uniformOverrideU32[0] = batchEntityIdAnchor(batch) ?? 0xFFFFFFFF;
2285
2268
  device.queue.writeBuffer(batch.uniformBuffer, 0, tpl);
2286
2269
  this.uniformOverrideU32.fill(0);
2287
2270
  // Single draw call for entire batch! LOD1-selected batches
@@ -2385,6 +2368,7 @@ export class Renderer {
2385
2368
  pass.setPipeline(this.pipeline.getInstancedPipeline());
2386
2369
  pass.setBindGroup(0, this.pipeline.getBindGroup());
2387
2370
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
2371
+ pass.setBindGroup(3, sectionExclusions(device).bindGroup);
2388
2372
  for (const [i, it] of visibleInstanced.entries()) {
2389
2373
  pass.setVertexBuffer(0, it.vertexBuffer);
2390
2374
  pass.setVertexBuffer(1, it.instanceBuffer);
@@ -2460,6 +2444,7 @@ export class Renderer {
2460
2444
  // CHOICE in the shader follows the same rule as everywhere
2461
2445
  // else, for the same reason `tm.material` does.
2462
2446
  packMeshMaterial(tpl, tm.color[3], tm.finish ?? tm.material);
2447
+ this.uniformOverrideU32[0] = tm.expressId;
2463
2448
  device.queue.writeBuffer(tm.uniformBuffer, 0, tpl);
2464
2449
  pass.setBindGroup(0, tm.bindGroup);
2465
2450
  pass.setVertexBuffer(0, tm.vertexBuffer);
@@ -2585,6 +2570,7 @@ export class Renderer {
2585
2570
  pass.setPipeline(instancedTransparentPipeline);
2586
2571
  pass.setBindGroup(0, this.pipeline.getBindGroup());
2587
2572
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
2573
+ pass.setBindGroup(3, sectionExclusions(device).bindGroup);
2588
2574
  for (const [i, it] of visibleInstanced.entries()) {
2589
2575
  pass.setVertexBuffer(0, it.vertexBuffer);
2590
2576
  pass.setVertexBuffer(1, it.instanceBuffer);
@@ -2616,18 +2602,7 @@ export class Renderer {
2616
2602
  tpl[34] = mesh.color[2];
2617
2603
  tpl[35] = alphaForMesh(mesh.expressId, mesh.color[3]);
2618
2604
  packMeshMaterial(tpl, mesh.color[3], mesh.finish ?? mesh.material);
2619
- if (sectionPlaneData) {
2620
- tpl[40] = sectionPlaneData.normal[0];
2621
- tpl[41] = sectionPlaneData.normal[1];
2622
- tpl[42] = sectionPlaneData.normal[2];
2623
- tpl[43] = sectionPlaneData.distance;
2624
- }
2625
- else {
2626
- tpl[40] = 0;
2627
- tpl[41] = 0;
2628
- tpl[42] = 0;
2629
- tpl[43] = 0;
2630
- }
2605
+ packSectionPlaneLanes(sectionPlaneData, tpl);
2631
2606
  tplFlags[0] = 0;
2632
2607
  tplFlags[1] =
2633
2608
  (sectionPlaneData?.enabled ? 1 : 0) |
@@ -2635,11 +2610,12 @@ export class Renderer {
2635
2610
  tplClipBit;
2636
2611
  tplFlags[2] = 0;
2637
2612
  tplFlags[3] = 0;
2638
- packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2613
+ packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl, extraClipPlanes);
2639
2614
  const transparentOrigin = mesh.rteOrigin ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
2640
2615
  if (!relativeToEyeFrame.tryPackDrawableOrigin(transparentOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2641
2616
  continue;
2642
2617
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2618
+ this.uniformOverrideU32[0] = mesh.expressId;
2643
2619
  device.queue.writeBuffer(mesh.uniformBuffer, 0, tpl);
2644
2620
  pass.setBindGroup(0, mesh.bindGroup);
2645
2621
  pass.setVertexBuffer(0, mesh.vertexBuffer);
@@ -2678,18 +2654,7 @@ export class Renderer {
2678
2654
  tpl[34] = mesh.color[2];
2679
2655
  tpl[35] = mesh.color[3];
2680
2656
  packMeshMaterial(tpl, mesh.color[3], mesh.finish ?? mesh.material);
2681
- if (sectionPlaneData) {
2682
- tpl[40] = sectionPlaneData.normal[0];
2683
- tpl[41] = sectionPlaneData.normal[1];
2684
- tpl[42] = sectionPlaneData.normal[2];
2685
- tpl[43] = sectionPlaneData.distance;
2686
- }
2687
- else {
2688
- tpl[40] = 0;
2689
- tpl[41] = 0;
2690
- tpl[42] = 0;
2691
- tpl[43] = 0;
2692
- }
2657
+ packSectionPlaneLanes(sectionPlaneData, tpl);
2693
2658
  tplFlags[0] = 1; // isSelected
2694
2659
  tplFlags[1] =
2695
2660
  (sectionPlaneData?.enabled ? 1 : 0) |
@@ -2697,11 +2662,12 @@ export class Renderer {
2697
2662
  tplClipBit;
2698
2663
  tplFlags[2] = 0;
2699
2664
  tplFlags[3] = 0;
2700
- packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2665
+ packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl, extraClipPlanes);
2701
2666
  const selectedOrigin = mesh.rteOrigin ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
2702
2667
  if (!relativeToEyeFrame.tryPackDrawableOrigin(selectedOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2703
2668
  continue;
2704
2669
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2670
+ this.uniformOverrideU32[0] = mesh.expressId;
2705
2671
  device.queue.writeBuffer(mesh.uniformBuffer, 0, tpl);
2706
2672
  pass.setPipeline(this.pipeline.getSelectionPipeline());
2707
2673
  pass.setBindGroup(0, mesh.bindGroup);
@@ -2759,6 +2725,7 @@ export class Renderer {
2759
2725
  ? { ...sectionPlaneData, flipped: options.sectionPlane?.flipped === true }
2760
2726
  : null,
2761
2727
  clipBox: options.clipBox,
2728
+ extraPlanes: extraClipPlanes,
2762
2729
  viewport: { width: this.canvas.width / this.pixelRatio, height: this.canvas.height / this.pixelRatio },
2763
2730
  });
2764
2731
  }
@@ -2776,6 +2743,8 @@ export class Renderer {
2776
2743
  relativeToEyeFrame,
2777
2744
  rteViewProj: relativeToEyeFrame.getViewProjection().m,
2778
2745
  rteCamera: relativeToEyeFrame.getCameraWorld(),
2746
+ extraPlanes: extraClipPlanes,
2747
+ primaryPlane: sectionPlaneData && options.sectionPlane?.enabled ? { ...sectionPlaneData, flipped: options.sectionPlane.flipped === true } : null,
2779
2748
  });
2780
2749
  pass.end();
2781
2750
  // Selection/hover outline input (#5390): built from the meshes the
@@ -2790,7 +2759,7 @@ export class Renderer {
2790
2759
  : (this.hoverMeshes.release(), []);
2791
2760
  const selectionOutline = buildSelectionOutlineFrame({
2792
2761
  uniformBufferSize: this.pipeline.getUniformBufferSize(), viewProj, relativeToEyeFrame,
2793
- section: sectionPlaneData, sectionFlipped: options.sectionPlane?.flipped, clipBox: options.clipBox,
2762
+ section: sectionPlaneData, sectionFlipped: options.sectionPlane?.flipped, clipBox: options.clipBox, extraPlanes: extraClipPlanes,
2794
2763
  selectedMeshes: selectedMeshesForMask, hoverPieces, hoveredId: hoverId, selectedModelIndex: hoverModel,
2795
2764
  instancedTemplates: this.scene.getInstancedTemplates(),
2796
2765
  instancedHovered: hoverId != null ? this.scene.getInstancedTemplatesOf(hoverId, hoverModel) : [],
@@ -2812,7 +2781,7 @@ export class Renderer {
2812
2781
  pixelRatio: this.pixelRatio,
2813
2782
  // EDL only when point clouds are loaded and the user enabled it.
2814
2783
  edl: this.edlOptions.enabled && this.pointCloudRenderer?.hasAssets() ? this.edlOptions : null,
2815
- selectionOutline,
2784
+ selectionOutline, geometricVisibility: this.pipeline.visibility.getBinding(),
2816
2785
  });
2817
2786
  colorReadback = colorCapture && encodeRendererColorFrameCapture(device, encoder, colorCapture);
2818
2787
  submitRendererGpuTiming(this, device, encoder);
@@ -2880,8 +2849,10 @@ export class Renderer {
2880
2849
  // `getOrCreatePartialBatch`'s CPU-side geometry merge running out of
2881
2850
  // host memory, and that throws a `RangeError` — which is exactly why
2882
2851
  // the discriminator keys on the TYPE and not on "a frame threw".
2883
- this.containFrameThrow(error, 'encode');
2884
2852
  cancelRendererColorFrame(this);
2853
+ if (deferOpaqueVisibilityFrame(error, () => this.requestRender()))
2854
+ return false;
2855
+ this.containFrameThrow(error, 'encode');
2885
2856
  return false;
2886
2857
  }
2887
2858
  }
@@ -2926,7 +2897,7 @@ export class Renderer {
2926
2897
  * same fragments and clipped-away geometry can't be selected.
2927
2898
  */
2928
2899
  activePickClip() {
2929
- return { sectionPlane: this._activePickSection, clipBox: this._activePickClipBox };
2900
+ return { sectionExcludedIds: this._sectionExcludedIds, sectionPlane: this._activePickSection, extraPlanes: this._activePickExtraPlanes, clipBox: this._activePickClipBox };
2930
2901
  }
2931
2902
  /**
2932
2903
  * GPU-based rectangle pick. Drag-select returns the set of
@@ -2946,7 +2917,7 @@ export class Renderer {
2946
2917
  }
2947
2918
  /** Whether the last rendered frame clipped surfaces (section, terrain or box). */
2948
2919
  hasActiveClipping() {
2949
- return this._activePickSection !== null || this._activePickClipBox !== null;
2920
+ return this._activePickSection !== null || this._activePickExtraPlanes !== null || this._activePickClipBox !== null;
2950
2921
  }
2951
2922
  /** Exact visible-surface raycast in CSS canvas coordinates. */
2952
2923
  raycastScene(x, y, options) {
@@ -3059,14 +3030,21 @@ export class Renderer {
3059
3030
  */
3060
3031
  uploadSection2DOverlay(polygons, lines, axis, position, // 0-100 percentage
3061
3032
  sectionRange, // Same storey-based range as section plane
3062
- flipped = false, customPlane) {
3063
- this.overlays.uploadSection2DOverlay(polygons, lines, axis, position, sectionRange, flipped, customPlane);
3033
+ flipped = false, customPlane, planeId) {
3034
+ this.overlays.uploadSection2DOverlay(polygons, lines, axis, position, sectionRange, flipped, customPlane, planeId);
3064
3035
  }
3065
3036
  /**
3066
- * Clear the 2D section overlay
3037
+ * Clear the 2D section overlay: the primary plane's cap, or with `planeId`
3038
+ * the cap of that cutting plane beyond the first (#7545).
3067
3039
  */
3068
- clearSection2DOverlay() {
3069
- this.overlays.clearSection2DOverlay();
3040
+ clearSection2DOverlay(planeId) {
3041
+ this.overlays.clearSection2DOverlay(planeId);
3042
+ }
3043
+ /** Every section plane's world frame (#7545), for the UI's tags and handle: the last frame's bounds and rotation, and `planes` (the caller's newest) when given. */
3044
+ getSectionPlaneFrames(planes) { const i = this._sectionFrameInput; return i ? resolveSectionPlaneFrames(planes ? { ...i, ...planes } : i) : []; }
3045
+ /** Clear the caps of every cutting plane beyond the first whose id is not in `keep` (#7545). */
3046
+ clearSection2DExtraOverlays(keep = []) {
3047
+ this.overlays.clearSection2DExtraOverlays(keep);
3070
3048
  }
3071
3049
  /**
3072
3050
  * Set the one colour vocabulary every overlay draws in (#5484): the selection
@@ -3300,6 +3278,7 @@ export class Renderer {
3300
3278
  // Render pipelines (textures + uniform buffers)
3301
3279
  this.pipeline?.destroy();
3302
3280
  this.pipeline = null;
3281
+ this.scene.setQuantizedBatches(false);
3303
3282
  // Picker GPU resources. The manager holds its own reference, so clear
3304
3283
  // that too — otherwise its `if (!this.picker) return null` guard keeps
3305
3284
  // pointing at a destroyed picker and a stray click still drives dead
@@ -3334,7 +3313,7 @@ export class Renderer {
3334
3313
  // above was created from it and has already been destroyed, so nothing
3335
3314
  // will touch the device after this. Without it, an app that spins up a
3336
3315
  // renderer per model keeps N live devices (and their VRAM) alive. The
3337
- // lost-handler special-cases reason 'destroyed' so this is not reported
3316
+ // lost-handler excludes this retired lifetime so this is not reported
3338
3317
  // as a fault. render() early-returns while the device is uninitialised.
3339
3318
  this.device.destroy();
3340
3319
  }