@ifc-lite/renderer 1.38.1 → 1.40.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 (61) hide show
  1. package/dist/deviation/deviation-pipeline.d.ts +8 -0
  2. package/dist/deviation/deviation-pipeline.d.ts.map +1 -1
  3. package/dist/deviation/deviation-pipeline.js +20 -9
  4. package/dist/deviation/deviation-pipeline.js.map +1 -1
  5. package/dist/deviation/deviation-shader.wgsl.d.ts +1 -1
  6. package/dist/deviation/deviation-shader.wgsl.d.ts.map +1 -1
  7. package/dist/deviation/deviation-shader.wgsl.js +11 -3
  8. package/dist/deviation/deviation-shader.wgsl.js.map +1 -1
  9. package/dist/index.d.ts +45 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +78 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/picker.d.ts +9 -0
  14. package/dist/picker.d.ts.map +1 -1
  15. package/dist/picker.js +84 -2
  16. package/dist/picker.js.map +1 -1
  17. package/dist/pointcloud/point-cloud-node.d.ts +46 -0
  18. package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
  19. package/dist/pointcloud/point-cloud-node.js +71 -0
  20. package/dist/pointcloud/point-cloud-node.js.map +1 -1
  21. package/dist/pointcloud/point-cloud-ray-transform.d.ts +66 -0
  22. package/dist/pointcloud/point-cloud-ray-transform.d.ts.map +1 -0
  23. package/dist/pointcloud/point-cloud-ray-transform.js +172 -0
  24. package/dist/pointcloud/point-cloud-ray-transform.js.map +1 -0
  25. package/dist/pointcloud/point-cloud-renderer.d.ts +39 -6
  26. package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
  27. package/dist/pointcloud/point-cloud-renderer.js +53 -17
  28. package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
  29. package/dist/pointcloud/point-cloud-spatial-index.d.ts +188 -0
  30. package/dist/pointcloud/point-cloud-spatial-index.d.ts.map +1 -0
  31. package/dist/pointcloud/point-cloud-spatial-index.js +536 -0
  32. package/dist/pointcloud/point-cloud-spatial-index.js.map +1 -0
  33. package/dist/pointcloud/point-cloud-uniforms.d.ts +14 -2
  34. package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
  35. package/dist/pointcloud/point-cloud-uniforms.js +49 -8
  36. package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
  37. package/dist/pointcloud/point-pipeline.d.ts +3 -2
  38. package/dist/pointcloud/point-pipeline.d.ts.map +1 -1
  39. package/dist/pointcloud/point-pipeline.js +8 -5
  40. package/dist/pointcloud/point-pipeline.js.map +1 -1
  41. package/dist/pointcloud/point-shader.wgsl.d.ts +1 -1
  42. package/dist/pointcloud/point-shader.wgsl.d.ts.map +1 -1
  43. package/dist/pointcloud/point-shader.wgsl.js +20 -17
  44. package/dist/pointcloud/point-shader.wgsl.js.map +1 -1
  45. package/dist/raycast-engine.d.ts +8 -0
  46. package/dist/raycast-engine.d.ts.map +1 -1
  47. package/dist/raycast-engine.js +68 -23
  48. package/dist/raycast-engine.js.map +1 -1
  49. package/dist/raycast-point-cloud-query.d.ts +122 -0
  50. package/dist/raycast-point-cloud-query.d.ts.map +1 -0
  51. package/dist/raycast-point-cloud-query.js +145 -0
  52. package/dist/raycast-point-cloud-query.js.map +1 -0
  53. package/dist/scene.d.ts +18 -0
  54. package/dist/scene.d.ts.map +1 -1
  55. package/dist/scene.js +166 -63
  56. package/dist/scene.js.map +1 -1
  57. package/dist/snap-detector.d.ts +12 -1
  58. package/dist/snap-detector.d.ts.map +1 -1
  59. package/dist/snap-detector.js +9 -1
  60. package/dist/snap-detector.js.map +1 -1
  61. package/package.json +2 -2
package/dist/scene.js CHANGED
@@ -30,6 +30,10 @@ export class Scene {
30
30
  meshDataMap = new Map(); // Map expressId -> MeshData[] (for lazy buffer creation, accumulates multiple pieces)
31
31
  boundingBoxes = new Map(); // Map expressId -> bounding box (computed lazily)
32
32
  texturedMeshes = []; // #961: IFC surface-textured meshes (own buffers/texture/bindGroup)
33
+ /** #1781: GPU textures shared across meshes, keyed by `MeshTextureRef.textureId`
34
+ * (one `IfcImageTexture` → one upload, sampled by every face set mapping it).
35
+ * Refcounted: entries die when the last referencing mesh is removed / on clear(). */
36
+ sharedTextures = new Map();
33
37
  texturedDevice; // #961: cached for textured-mesh re-upload on translate
34
38
  instancedTemplates = []; // GPU-instancing: unique templates + per-occurrence buffers (fed by addInstancedShard)
35
39
  instancedVisible = true; // GPU-instancing: hidden in Types view mode (instanced geometry is class-0 occurrences)
@@ -964,10 +968,10 @@ export class Scene {
964
968
  // otherwise a flat-colour copy would be drawn over the texture. Still
965
969
  // register them in meshDataMap (addMeshData) so CPU picking/bbox/frame work.
966
970
  let renderable = meshDataArray;
967
- if (meshDataArray.some((m) => m.texture && m.uvs)) {
971
+ if (meshDataArray.some((m) => Scene.hasRenderableTexture(m))) {
968
972
  renderable = [];
969
973
  for (const meshData of meshDataArray) {
970
- if (meshData.texture && meshData.uvs) {
974
+ if (Scene.hasRenderableTexture(meshData)) {
971
975
  this.createTexturedMesh(meshData, device, pipeline);
972
976
  this.addMeshData(meshData);
973
977
  }
@@ -1135,7 +1139,7 @@ export class Scene {
1135
1139
  tm.vertexBuffer.destroy();
1136
1140
  tm.indexBuffer.destroy();
1137
1141
  tm.uniformBuffer.destroy();
1138
- tm.texture.destroy();
1142
+ this.releaseTexturedMeshTexture(tm);
1139
1143
  this.texturedMeshes.splice(i, 1);
1140
1144
  removedDedicated = true;
1141
1145
  }
@@ -1321,7 +1325,7 @@ export class Scene {
1321
1325
  // re-interleave + re-upload the moved textured parts (paired by expressId,
1322
1326
  // in creation order). Without this a moved textured entity renders stale.
1323
1327
  if (this.texturedDevice && this.texturedMeshes.length > 0) {
1324
- const texturedData = meshDataList.filter((md) => md.texture && md.uvs);
1328
+ const texturedData = meshDataList.filter((md) => Scene.hasRenderableTexture(md));
1325
1329
  if (texturedData.length > 0) {
1326
1330
  const entries = this.texturedMeshes.filter((tm) => tm.expressId === expressId);
1327
1331
  for (let i = 0; i < entries.length && i < texturedData.length; i++) {
@@ -1519,7 +1523,7 @@ export class Scene {
1519
1523
  // #961: textured meshes render from their own GPU vertex buffer — re-upload
1520
1524
  // the rotated parts so they don't render stale (mirrors the translate path).
1521
1525
  if (this.texturedDevice && this.texturedMeshes.length > 0) {
1522
- const texturedData = meshDataList.filter((md) => md.texture && md.uvs);
1526
+ const texturedData = meshDataList.filter((md) => Scene.hasRenderableTexture(md));
1523
1527
  if (texturedData.length > 0) {
1524
1528
  const entries = this.texturedMeshes.filter((tm) => tm.expressId === expressId);
1525
1529
  for (let i = 0; i < entries.length && i < texturedData.length; i++) {
@@ -1757,56 +1761,90 @@ export class Scene {
1757
1761
  const oldFragments = this.streamingFragments;
1758
1762
  const oldBatches = this.batchedMeshes;
1759
1763
  const fragmentSet = new Set(oldFragments);
1760
- this.streamingFragments = [];
1761
- // 1. Collect ALL accumulated meshData before clearing state.
1762
- // Cold buckets (issue #1682 phase 3b) hold NO meshData their
1763
- // geometry lives on disk so they are carried through the rebuild as
1764
- // sealed shells instead of being re-grouped (re-grouping would
1765
- // silently drop them).
1766
- const allMeshData = [];
1767
- const carriedCold = [];
1768
- for (const [key, bucket] of this.buckets) {
1769
- if (this.coldBuckets.has(key) && bucket.meshData.length === 0 && bucket.batchedMesh) {
1770
- carriedCold.push([key, bucket]);
1771
- continue;
1764
+ const oldBatchSet = new Set(oldBatches);
1765
+ // Steps 1-4 detach the old drawables (streamingFragments = [],
1766
+ // batchedMeshes = []) BEFORE the replacement GPU buffers exist. If a
1767
+ // createBuffer fails part-way through, callers that CONTAIN the throw to
1768
+ // keep the canvas alive would otherwise be left rendering a half-built
1769
+ // often empty — scene, turning a crash into a silently blank model.
1770
+ let rebuilt = false;
1771
+ try {
1772
+ this.streamingFragments = [];
1773
+ // 1. Collect ALL accumulated meshData before clearing state.
1774
+ // Cold buckets (issue #1682 phase 3b) hold NO meshData — their
1775
+ // geometry lives on disk — so they are carried through the rebuild as
1776
+ // sealed shells instead of being re-grouped (re-grouping would
1777
+ // silently drop them).
1778
+ const allMeshData = [];
1779
+ const carriedCold = [];
1780
+ for (const [key, bucket] of this.buckets) {
1781
+ if (this.coldBuckets.has(key) && bucket.meshData.length === 0 && bucket.batchedMesh) {
1782
+ carriedCold.push([key, bucket]);
1783
+ continue;
1784
+ }
1785
+ for (const md of bucket.meshData)
1786
+ allMeshData.push(md);
1787
+ }
1788
+ // 2. Clear all bucket/batch state for a clean rebuild
1789
+ // NOTE: batchedMeshes keeps the OLD array reference — the renderer
1790
+ // continues to draw from it until we swap in the new array below.
1791
+ this.buckets.clear();
1792
+ this.meshDataBucket = new Map();
1793
+ this.activeBucketKey.clear();
1794
+ this.lastDrawnFrame.clear();
1795
+ this.residencyRestoreQueue.clear();
1796
+ this.pendingBatchKeys.clear();
1797
+ // Re-seat the carried cold shells in the fresh bucket map (their GPU
1798
+ // shells re-enter the flat array via rebuildPendingBatches below).
1799
+ for (const [key, bucket] of carriedCold)
1800
+ this.buckets.set(key, bucket);
1801
+ // 3. Re-group ALL meshData by their CURRENT color (and grid cell).
1802
+ // meshData.color may have been mutated in-place since the mesh was
1803
+ // first bucketed, so the original bucket key is stale. Re-grouping
1804
+ // by current color ensures batches render with correct colors.
1805
+ for (const meshData of allMeshData) {
1806
+ const baseKey = this.bucketBaseKey(meshData);
1807
+ const bucketKey = this.resolveActiveBucket(baseKey, meshData);
1808
+ let bucket = this.buckets.get(bucketKey);
1809
+ if (!bucket) {
1810
+ bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
1811
+ this.buckets.set(bucketKey, bucket);
1812
+ }
1813
+ bucket.meshData.push(meshData);
1814
+ this.meshDataBucket.set(meshData, bucket);
1815
+ this.pendingBatchKeys.add(bucketKey);
1772
1816
  }
1773
- for (const md of bucket.meshData)
1774
- allMeshData.push(md);
1817
+ // 4. Build new proper batches into a fresh array
1818
+ this.batchedMeshes = [];
1819
+ this.rebuildPendingBatches(device, pipeline);
1820
+ rebuilt = true;
1821
+ // Cached partial (filtered-visibility) batches are keyed by their SOURCE
1822
+ // batch, so they only go stale once the replacement batches are live.
1823
+ // Dropping them up in step 2 destroyed their GPU resources BEFORE the
1824
+ // rebuild could fail, and the rollback cannot bring them back — an active
1825
+ // hide/isolate view lost its visible subset and had to recreate it
1826
+ // against the very device that just failed. On the failure path they now
1827
+ // survive, still matching the restored batches.
1828
+ this.dropAllPartialCaches();
1775
1829
  }
1776
- // 2. Clear all bucket/batch state for a clean rebuild
1777
- // NOTE: batchedMeshes keeps the OLD array reference — the renderer
1778
- // continues to draw from it until we swap in the new array below.
1779
- this.buckets.clear();
1780
- this.meshDataBucket = new Map();
1781
- this.activeBucketKey.clear();
1782
- this.lastDrawnFrame.clear();
1783
- this.residencyRestoreQueue.clear();
1784
- this.pendingBatchKeys.clear();
1785
- // Destroy cached partial batches — their colorKeys are now stale
1786
- this.dropAllPartialCaches();
1787
- // Re-seat the carried cold shells in the fresh bucket map (their GPU
1788
- // shells re-enter the flat array via rebuildPendingBatches below).
1789
- for (const [key, bucket] of carriedCold)
1790
- this.buckets.set(key, bucket);
1791
- // 3. Re-group ALL meshData by their CURRENT color (and grid cell).
1792
- // meshData.color may have been mutated in-place since the mesh was
1793
- // first bucketed, so the original bucket key is stale. Re-grouping
1794
- // by current color ensures batches render with correct colors.
1795
- for (const meshData of allMeshData) {
1796
- const baseKey = this.bucketBaseKey(meshData);
1797
- const bucketKey = this.resolveActiveBucket(baseKey, meshData);
1798
- let bucket = this.buckets.get(bucketKey);
1799
- if (!bucket) {
1800
- bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
1801
- this.buckets.set(bucketKey, bucket);
1830
+ finally {
1831
+ if (!rebuilt) {
1832
+ // Free ONLY what this attempt created. Carried cold shells are aliased
1833
+ // into BOTH the old and the new array, so anything that was already
1834
+ // live before the rebuild must be left alone — destroying it would
1835
+ // leave the restored arrays pointing at dead buffers.
1836
+ for (const created of this.batchedMeshes) {
1837
+ if (!oldBatchSet.has(created) && !fragmentSet.has(created)) {
1838
+ destroyGpuResources(created);
1839
+ }
1840
+ }
1841
+ // Step 5 never ran, so every old fragment/batch is still a live GPU
1842
+ // resource: putting the arrays back restores exactly what was on
1843
+ // screen.
1844
+ this.streamingFragments = oldFragments;
1845
+ this.batchedMeshes = oldBatches;
1802
1846
  }
1803
- bucket.meshData.push(meshData);
1804
- this.meshDataBucket.set(meshData, bucket);
1805
- this.pendingBatchKeys.add(bucketKey);
1806
1847
  }
1807
- // 4. Build new proper batches into a fresh array
1808
- this.batchedMeshes = [];
1809
- this.rebuildPendingBatches(device, pipeline);
1810
1848
  // 5. NOW destroy old fragment/batch GPU resources (new batches are live)
1811
1849
  for (const fragment of oldFragments)
1812
1850
  destroyGpuResources(fragment);
@@ -3098,6 +3136,15 @@ export class Scene {
3098
3136
  * The per-frame uniform (viewProj/section/flags + colour tint) is written by
3099
3137
  * the renderer each frame, mirroring how colour batches are driven.
3100
3138
  */
3139
+ /** True when the mesh can render through the textured pipeline: UVs plus
3140
+ * either a Rust-decoded image (#961) or a viewer-resolved ImageBitmap for
3141
+ * an external `IfcImageTexture` reference (#1781). A `textureRef` whose
3142
+ * image was NOT resolved (missing zip sibling) renders as ordinary
3143
+ * flat-colour geometry instead. */
3144
+ static hasRenderableTexture(meshData) {
3145
+ return Boolean(meshData.uvs) &&
3146
+ Boolean(meshData.texture || (meshData.textureRef && meshData.textureBitmap));
3147
+ }
3101
3148
  /**
3102
3149
  * Interleave a textured mesh's vertices into the stride-36 layout
3103
3150
  * `[px,py,pz, nx,ny,nz, entityId(u32), u,v]`. Shared by initial upload and
@@ -3106,7 +3153,7 @@ export class Scene {
3106
3153
  */
3107
3154
  interleaveTexturedVertices(meshData) {
3108
3155
  const uvs = meshData.uvs;
3109
- if (!meshData.texture || !uvs)
3156
+ if (!Scene.hasRenderableTexture(meshData) || !uvs)
3110
3157
  return null;
3111
3158
  const positions = meshData.positions;
3112
3159
  const normals = meshData.normals;
@@ -3137,8 +3184,10 @@ export class Scene {
3137
3184
  }
3138
3185
  createTexturedMesh(meshData, device, pipeline) {
3139
3186
  const tex = meshData.texture;
3187
+ const ref = meshData.textureRef;
3188
+ const bitmap = meshData.textureBitmap;
3140
3189
  const interleaved = this.interleaveTexturedVertices(meshData);
3141
- if (!tex || !interleaved)
3190
+ if (!interleaved || !(tex || (ref && bitmap)))
3142
3191
  return;
3143
3192
  this.texturedDevice = device; // reused by translateMeshesForEntity re-upload
3144
3193
  const vertexBuffer = device.createBuffer({
@@ -3151,17 +3200,48 @@ export class Scene {
3151
3200
  usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
3152
3201
  });
3153
3202
  device.queue.writeBuffer(indexBuffer, 0, meshData.indices);
3154
- // Upload the Rust-decoded RGBA8 verbatim — no image decoding in JS.
3155
- const texture = device.createTexture({
3156
- size: { width: tex.width, height: tex.height },
3157
- format: 'rgba8unorm',
3158
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
3159
- });
3160
- device.queue.writeTexture({ texture }, tex.rgba, { bytesPerRow: tex.width * 4, rowsPerImage: tex.height }, { width: tex.width, height: tex.height });
3203
+ let texture;
3204
+ let sharedTextureKey;
3205
+ if (tex) {
3206
+ // #961: upload the Rust-decoded RGBA8 verbatim — no image decoding in JS.
3207
+ texture = device.createTexture({
3208
+ size: { width: tex.width, height: tex.height },
3209
+ format: 'rgba8unorm',
3210
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
3211
+ });
3212
+ device.queue.writeTexture({ texture }, tex.rgba, { bytesPerRow: tex.width * 4, rowsPerImage: tex.height }, { width: tex.width, height: tex.height });
3213
+ }
3214
+ else {
3215
+ // #1781: external image texture — the viewer decoded the `.ifcZIP`
3216
+ // sibling to an ImageBitmap once per textureId; upload it ONCE and share
3217
+ // the GPU texture across every mesh sampling it (real files map one
3218
+ // 4096² image from dozens of face sets — per-mesh copies would be GBs).
3219
+ const refKey = ref.textureId;
3220
+ const bmp = bitmap;
3221
+ let entry = this.sharedTextures.get(refKey);
3222
+ if (!entry) {
3223
+ const gpuTex = device.createTexture({
3224
+ size: { width: bmp.width, height: bmp.height },
3225
+ format: 'rgba8unorm',
3226
+ // RENDER_ATTACHMENT is required by copyExternalImageToTexture.
3227
+ usage: GPUTextureUsage.TEXTURE_BINDING |
3228
+ GPUTextureUsage.COPY_DST |
3229
+ GPUTextureUsage.RENDER_ATTACHMENT,
3230
+ });
3231
+ device.queue.copyExternalImageToTexture({ source: bmp }, { texture: gpuTex }, { width: bmp.width, height: bmp.height });
3232
+ entry = { texture: gpuTex, refs: 0 };
3233
+ this.sharedTextures.set(refKey, entry);
3234
+ }
3235
+ entry.refs++;
3236
+ texture = entry.texture;
3237
+ sharedTextureKey = refKey;
3238
+ }
3239
+ const repeatS = tex ? tex.repeatS : ref.repeatS;
3240
+ const repeatT = tex ? tex.repeatT : ref.repeatT;
3161
3241
  const wrap = (repeat) => (repeat ? 'repeat' : 'clamp-to-edge');
3162
3242
  const sampler = device.createSampler({
3163
- addressModeU: wrap(tex.repeatS),
3164
- addressModeV: wrap(tex.repeatT),
3243
+ addressModeU: wrap(repeatS),
3244
+ addressModeV: wrap(repeatT),
3165
3245
  magFilter: 'linear',
3166
3246
  minFilter: 'linear',
3167
3247
  mipmapFilter: 'linear',
@@ -3181,8 +3261,26 @@ export class Scene {
3181
3261
  sampler,
3182
3262
  bindGroup,
3183
3263
  color: meshData.color,
3264
+ ...(sharedTextureKey !== undefined ? { sharedTextureKey } : {}),
3184
3265
  });
3185
3266
  }
3267
+ /** Release a textured mesh's GPU texture: shared (#1781) entries decrement
3268
+ * the registry refcount and die with their LAST reference; per-mesh (#961)
3269
+ * uploads are destroyed outright. */
3270
+ releaseTexturedMeshTexture(tm) {
3271
+ if (tm.sharedTextureKey === undefined) {
3272
+ tm.texture.destroy();
3273
+ return;
3274
+ }
3275
+ const entry = this.sharedTextures.get(tm.sharedTextureKey);
3276
+ if (!entry)
3277
+ return;
3278
+ entry.refs--;
3279
+ if (entry.refs <= 0) {
3280
+ entry.texture.destroy();
3281
+ this.sharedTextures.delete(tm.sharedTextureKey);
3282
+ }
3283
+ }
3186
3284
  clear() {
3187
3285
  for (const mesh of this.meshes)
3188
3286
  destroyGpuResources(mesh);
@@ -3192,9 +3290,14 @@ export class Scene {
3192
3290
  tm.vertexBuffer.destroy();
3193
3291
  tm.indexBuffer.destroy();
3194
3292
  tm.uniformBuffer.destroy();
3195
- tm.texture.destroy();
3293
+ this.releaseTexturedMeshTexture(tm);
3196
3294
  }
3197
3295
  this.texturedMeshes = [];
3296
+ // Belt-and-braces: refcounting above should have emptied the registry;
3297
+ // destroy any straggler so clear() can never leak a shared GPU texture.
3298
+ for (const entry of this.sharedTextures.values())
3299
+ entry.texture.destroy();
3300
+ this.sharedTextures.clear();
3198
3301
  // GPU-instancing templates own their vertex/index/instance buffers.
3199
3302
  for (const it of this.instancedTemplates) {
3200
3303
  it.vertexBuffer.destroy();