@ifc-lite/renderer 1.48.0 → 1.48.1

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.
package/dist/index.js CHANGED
@@ -84,29 +84,9 @@ import { resolveEnvironment } from './environment.js';
84
84
  import { shouldRouteMeshTransparent, shouldRouteBatchTransparent, splitVisibleIdsByPromotion, DEFAULT_GHOST_ALPHA } from './overlay-routing.js';
85
85
  import { colorSaltByte, packEntityLane } from './scene-geometry.js';
86
86
  import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
87
- import { DeviationPipeline } from './deviation/deviation-pipeline.js';
88
- import { buildTriangleBVH } from './deviation/triangle-bvh.js';
87
+ import { DeviationComputer } from './deviation/deviation-computer.js';
89
88
  const MAX_ENCODED_ENTITY_ID = 0xFFFFFF;
90
89
  let warnedEntityIdRange = false;
91
- /**
92
- * Build a deterministic fingerprint of the BVH input mesh set so
93
- * `Renderer.computeDeviations` can skip the rebuild when the source
94
- * geometry hasn't changed. Folds in expressId / modelIndex / position
95
- * + index lengths per mesh so two distinct mesh sets that happen to
96
- * share the same aggregate position-length total can't collide on the
97
- * same fingerprint and reuse a stale BVH.
98
- */
99
- function computeBvhFingerprint(meshes) {
100
- const parts = [String(meshes.length)];
101
- for (const m of meshes) {
102
- const id = m.expressId ?? -1;
103
- const mi = m.modelIndex ?? -1;
104
- const posLen = m.positions?.length ?? 0;
105
- const idxLen = m.indices?.length ?? 0;
106
- parts.push(`${id}:${mi}:${posLen}:${idxLen}`);
107
- }
108
- return parts.join('|');
109
- }
110
90
  /**
111
91
  * Is this throw the GPU device telling us it is gone?
112
92
  *
@@ -301,15 +281,8 @@ export class Renderer {
301
281
  /** Retained so a listener registered AFTER the loss still learns of it. */
302
282
  deviceLostInfo = null;
303
283
  deviceLostListeners = new Set();
304
- deviationPipeline = null;
305
- /**
306
- * Cache of which mesh-set the BVH was built from. We rebuild on
307
- * `computeDeviations` only when the cached "fingerprint" misses,
308
- * so re-running deviation against the same model is a fast
309
- * dispatch — the BVH is multi-second on big BIMs and we don't
310
- * want to pay that on every slider drag.
311
- */
312
- deviationBvhFingerprint = null;
284
+ /** BIM ↔ scan deviation: owns the compute pipeline + its BVH cache. */
285
+ deviationComputer = new DeviationComputer();
313
286
  visualEnhancementResolver = new VisualEnhancementResolver();
314
287
  // Model bounds for fitToView, section planes, camera. The value itself
315
288
  // lives in ModelBoundsTracker (issue #2425) so the four writers — point
@@ -549,7 +522,7 @@ export class Renderer {
549
522
  // Compute pipeline for the BIM↔scan deviation heatmap. Lazily
550
523
  // owns the per-triangle BVH GPU buffers; idle until the first
551
524
  // `computeDeviations` call.
552
- this.deviationPipeline = new DeviationPipeline(this.device.getDevice());
525
+ this.deviationComputer.init(this.device.getDevice());
553
526
  this.edlPass = new EdlPass(this.device, this.pipeline.getSampleCount());
554
527
  this.camera.setAspect(width / height);
555
528
  // Update picking manager with initialized picker
@@ -977,111 +950,17 @@ export class Renderer {
977
950
  /**
978
951
  * Compute BIM ↔ scan deviation for every loaded point cloud asset.
979
952
  *
980
- * Walks every triangle in the scene (individual + batched meshes,
981
- * regardless of which IFC ingest path produced them — STEP, IFCx,
982
- * GLB, or federated combinations), builds a per-triangle BVH on
983
- * the GPU, then runs a closest-point compute pass per chunk that
984
- * writes signed distance into each chunk's deviation buffer.
985
- *
986
- * Returns metadata so the UI can populate a histogram + auto-range:
987
- * the per-asset point count, the suggested ±range from the 95th
988
- * percentile, and the bbox the BVH was built from.
989
- *
990
- * Idempotent: re-running with the same mesh set reuses the GPU
991
- * BVH (the BVH build dominates wall time on big BIMs). Pass
992
- * `forceRebuild: true` to invalidate.
953
+ * Delegates to the `DeviationComputer` collaborator, which owns the
954
+ * compute pipeline and the BVH-reuse fingerprint; see
955
+ * `deviation/deviation-computer.ts` for the full contract.
993
956
  */
994
957
  async computeDeviations(opts = {}) {
995
- if (!this.deviationPipeline || !this.pointCloudRenderer) {
996
- throw new Error('Renderer not initialised — call init() first.');
997
- }
998
- const meshes = this.collectAllSceneMeshes();
999
- // Fingerprint folds in per-mesh expressId / modelIndex /
1000
- // positions length / triangle count, so two distinct meshes
1001
- // that happen to share an aggregate position-length total
1002
- // can't alias each other. A federation reload that swaps one
1003
- // model for another with the same total triangle count would
1004
- // otherwise reuse the previous BVH and report wrong distances.
1005
- const fingerprint = computeBvhFingerprint(meshes);
1006
- if (opts.forceRebuild || fingerprint !== this.deviationBvhFingerprint) {
1007
- const bvh = buildTriangleBVH(meshes);
1008
- this.deviationPipeline.uploadBvh(bvh);
1009
- this.deviationBvhFingerprint = fingerprint;
1010
- }
1011
- const stats = this.deviationPipeline.getBvhStats();
1012
- const maxRange = opts.maxRange ?? 1.0;
1013
- // Encode every chunk into a single command submit so the GPU
1014
- // can pipeline the dispatches without a CPU round-trip per
1015
- // chunk. Histogram readback is a follow-up — for v1 we emit
1016
- // the deviation buffers and let the splat shader visualise.
1017
- const encoder = this.device.getDevice().createCommandEncoder({ label: 'pointcloud-deviation' });
1018
- let chunksProcessed = 0;
1019
- let pointsProcessed = 0;
1020
- const nodes = this.pointCloudRenderer.getInternalNodes();
1021
- for (const node of nodes) {
1022
- for (const chunk of node.chunks) {
1023
- const ok = this.deviationPipeline.dispatch(encoder, {
1024
- positionsBuffer: chunk.vertexBuffer,
1025
- deviationsBuffer: chunk.deviationBuffer,
1026
- pointCount: chunk.pointCount,
1027
- maxRange,
1028
- // #1804: chunk positions are stored in the asset's
1029
- // decode-shifted local frame when IfcMapConversion
1030
- // alignment is active; the BVH triangles are world
1031
- // space, so the compute pass must apply the same
1032
- // per-asset matrix the splat shader renders with.
1033
- model: node.model,
1034
- });
1035
- if (ok) {
1036
- chunksProcessed++;
1037
- pointsProcessed += chunk.pointCount;
1038
- }
1039
- }
1040
- }
1041
- this.device.getDevice().queue.submit([encoder.finish()]);
1042
- // Wait until the GPU finishes the dispatches before resolving.
1043
- // Otherwise the caller's "compute done" callback fires before
1044
- // the deviation buffers are actually populated.
1045
- await this.device.getDevice().queue.onSubmittedWorkDone();
1046
- // The GPU is done reading each chunk's params uniform — free them.
1047
- this.deviationPipeline.releaseTransientParams();
1048
- this.requestRender();
1049
- // Suggest a default half-range = max(0.01m, max-extent / 1000).
1050
- // Tighter than the maxRange clip; gives the user a reasonable
1051
- // starting slider position without a histogram readback.
1052
- const bb = stats.bounds;
1053
- const suggestedHalfRange = bb
1054
- ? Math.max(0.01, Math.max(bb.max[0] - bb.min[0], bb.max[1] - bb.min[1], bb.max[2] - bb.min[2]) / 1000)
1055
- : 0.05;
1056
- return {
1057
- bvhTriangles: stats.triangleCount,
1058
- bvhNodes: stats.nodeCount,
1059
- chunksProcessed,
1060
- pointsProcessed,
1061
- bounds: stats.bounds,
1062
- suggestedHalfRange,
1063
- };
1064
- }
1065
- /**
1066
- * Aggregate every triangle source the scene exposes — individual
1067
- * meshes (created on demand by picking / highlights) AND batched
1068
- * meshes (the streaming geometry path's compact GPU buffers).
1069
- * Both formats arrive as `MeshData`; the BVH builder doesn't care
1070
- * which source they came from.
1071
- */
1072
- collectAllSceneMeshes() {
1073
- // The Scene keeps every CPU-side MeshData regardless of which
1074
- // ingest path produced it (STEP / IFCx / GLB). One iteration
1075
- // covers individual + batched + multi-piece + multi-model.
1076
- // `forEachMeshData` deduplicates by identity so a colour-merged
1077
- // batch is only added once even if it's indexed under multiple
1078
- // contributor expressIds.
1079
- const out = [];
1080
- this.scene.forEachMeshData((md) => {
1081
- if (md.positions && md.positions.length > 0)
1082
- out.push(md);
958
+ return this.deviationComputer.compute(opts, {
959
+ device: this.device,
960
+ scene: this.scene,
961
+ pointCloudRenderer: this.pointCloudRenderer,
962
+ requestRender: () => this.requestRender(),
1083
963
  });
1084
- return out;
1085
964
  }
1086
965
  /**
1087
966
  * Toggle Eye-Dome Lighting and tune its strength.
@@ -1939,13 +1818,14 @@ export class Renderer {
1939
1818
  stencilStoreOp: 'store',
1940
1819
  },
1941
1820
  });
1942
- // Global lighting environment: write the uniform once per frame
1943
- // and bind at group(1) every pipeline derived from the main
1944
- // shader shares this layout, and bind groups persist across
1945
- // setPipeline calls within the pass.
1821
+ // Global lighting environment: write the uniform once per frame.
1822
+ // The group(1) bind is deferred until AFTER the sky pass below —
1823
+ // the sky pipeline has an incompatible layout (its own group(0),
1824
+ // no group(1)), so drawing the sky invalidates a group(1) binding
1825
+ // on conformant WebGPU implementations (see the rebind after the
1826
+ // sky block).
1946
1827
  const environment = resolveEnvironment(options.environment);
1947
1828
  this.pipeline.updateEnvironment(options.environment);
1948
- pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
1949
1829
  // Procedural sky background — replaces the flat clear colour.
1950
1830
  // Drawn before any geometry at the reverse-Z far plane with depth
1951
1831
  // writes off, so it never occludes anything and transparent
@@ -1981,6 +1861,17 @@ export class Renderer {
1981
1861
  }, environment);
1982
1862
  }
1983
1863
  pass.setPipeline(this.pipeline.getPipeline());
1864
+ // Bind the global lighting environment at group(1) AFTER any sky
1865
+ // draw. The sky pipeline's layout (its own group(0), no group(1))
1866
+ // is incompatible with the main layout, so drawing the sky
1867
+ // invalidates the group(1) binding on strict WebGPU
1868
+ // implementations. The flat batch loops below re-set only group(0)
1869
+ // per batch (the instanced passes re-bind group(1) themselves), so
1870
+ // without this rebind every non-'default' lighting preset — the
1871
+ // only presets that enable the sky — blanked the model on those
1872
+ // drivers. Binding while the main pipeline is current keeps it
1873
+ // valid for every main-family draw that follows.
1874
+ pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
1984
1875
  // Check if we have batched meshes (preferred for performance)
1985
1876
  const allBatchedMeshes = this.scene.getBatchedMeshes();
1986
1877
  // PERFORMANCE FIX: Always use batch rendering when we have batches
@@ -3300,9 +3191,7 @@ export class Renderer {
3300
3191
  // BIM ↔ scan deviation pipeline + cached BVH GPU buffers.
3301
3192
  // Done before queue.destroy() so the GPU calls inside
3302
3193
  // `destroy()` still have a valid device.
3303
- this.deviationPipeline?.destroy();
3304
- this.deviationPipeline = null;
3305
- this.deviationBvhFingerprint = null;
3194
+ this.deviationComputer.destroy();
3306
3195
  // Snap detector geometry cache
3307
3196
  this.raycastEngine.clearCaches();
3308
3197
  // Finally, release the GPU device itself. Every buffer/pipeline/texture