@ifc-lite/renderer 1.38.1 → 1.39.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.
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++) {
@@ -3098,6 +3102,15 @@ export class Scene {
3098
3102
  * The per-frame uniform (viewProj/section/flags + colour tint) is written by
3099
3103
  * the renderer each frame, mirroring how colour batches are driven.
3100
3104
  */
3105
+ /** True when the mesh can render through the textured pipeline: UVs plus
3106
+ * either a Rust-decoded image (#961) or a viewer-resolved ImageBitmap for
3107
+ * an external `IfcImageTexture` reference (#1781). A `textureRef` whose
3108
+ * image was NOT resolved (missing zip sibling) renders as ordinary
3109
+ * flat-colour geometry instead. */
3110
+ static hasRenderableTexture(meshData) {
3111
+ return Boolean(meshData.uvs) &&
3112
+ Boolean(meshData.texture || (meshData.textureRef && meshData.textureBitmap));
3113
+ }
3101
3114
  /**
3102
3115
  * Interleave a textured mesh's vertices into the stride-36 layout
3103
3116
  * `[px,py,pz, nx,ny,nz, entityId(u32), u,v]`. Shared by initial upload and
@@ -3106,7 +3119,7 @@ export class Scene {
3106
3119
  */
3107
3120
  interleaveTexturedVertices(meshData) {
3108
3121
  const uvs = meshData.uvs;
3109
- if (!meshData.texture || !uvs)
3122
+ if (!Scene.hasRenderableTexture(meshData) || !uvs)
3110
3123
  return null;
3111
3124
  const positions = meshData.positions;
3112
3125
  const normals = meshData.normals;
@@ -3137,8 +3150,10 @@ export class Scene {
3137
3150
  }
3138
3151
  createTexturedMesh(meshData, device, pipeline) {
3139
3152
  const tex = meshData.texture;
3153
+ const ref = meshData.textureRef;
3154
+ const bitmap = meshData.textureBitmap;
3140
3155
  const interleaved = this.interleaveTexturedVertices(meshData);
3141
- if (!tex || !interleaved)
3156
+ if (!interleaved || !(tex || (ref && bitmap)))
3142
3157
  return;
3143
3158
  this.texturedDevice = device; // reused by translateMeshesForEntity re-upload
3144
3159
  const vertexBuffer = device.createBuffer({
@@ -3151,17 +3166,48 @@ export class Scene {
3151
3166
  usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
3152
3167
  });
3153
3168
  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 });
3169
+ let texture;
3170
+ let sharedTextureKey;
3171
+ if (tex) {
3172
+ // #961: upload the Rust-decoded RGBA8 verbatim — no image decoding in JS.
3173
+ texture = device.createTexture({
3174
+ size: { width: tex.width, height: tex.height },
3175
+ format: 'rgba8unorm',
3176
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
3177
+ });
3178
+ device.queue.writeTexture({ texture }, tex.rgba, { bytesPerRow: tex.width * 4, rowsPerImage: tex.height }, { width: tex.width, height: tex.height });
3179
+ }
3180
+ else {
3181
+ // #1781: external image texture — the viewer decoded the `.ifcZIP`
3182
+ // sibling to an ImageBitmap once per textureId; upload it ONCE and share
3183
+ // the GPU texture across every mesh sampling it (real files map one
3184
+ // 4096² image from dozens of face sets — per-mesh copies would be GBs).
3185
+ const refKey = ref.textureId;
3186
+ const bmp = bitmap;
3187
+ let entry = this.sharedTextures.get(refKey);
3188
+ if (!entry) {
3189
+ const gpuTex = device.createTexture({
3190
+ size: { width: bmp.width, height: bmp.height },
3191
+ format: 'rgba8unorm',
3192
+ // RENDER_ATTACHMENT is required by copyExternalImageToTexture.
3193
+ usage: GPUTextureUsage.TEXTURE_BINDING |
3194
+ GPUTextureUsage.COPY_DST |
3195
+ GPUTextureUsage.RENDER_ATTACHMENT,
3196
+ });
3197
+ device.queue.copyExternalImageToTexture({ source: bmp }, { texture: gpuTex }, { width: bmp.width, height: bmp.height });
3198
+ entry = { texture: gpuTex, refs: 0 };
3199
+ this.sharedTextures.set(refKey, entry);
3200
+ }
3201
+ entry.refs++;
3202
+ texture = entry.texture;
3203
+ sharedTextureKey = refKey;
3204
+ }
3205
+ const repeatS = tex ? tex.repeatS : ref.repeatS;
3206
+ const repeatT = tex ? tex.repeatT : ref.repeatT;
3161
3207
  const wrap = (repeat) => (repeat ? 'repeat' : 'clamp-to-edge');
3162
3208
  const sampler = device.createSampler({
3163
- addressModeU: wrap(tex.repeatS),
3164
- addressModeV: wrap(tex.repeatT),
3209
+ addressModeU: wrap(repeatS),
3210
+ addressModeV: wrap(repeatT),
3165
3211
  magFilter: 'linear',
3166
3212
  minFilter: 'linear',
3167
3213
  mipmapFilter: 'linear',
@@ -3181,8 +3227,26 @@ export class Scene {
3181
3227
  sampler,
3182
3228
  bindGroup,
3183
3229
  color: meshData.color,
3230
+ ...(sharedTextureKey !== undefined ? { sharedTextureKey } : {}),
3184
3231
  });
3185
3232
  }
3233
+ /** Release a textured mesh's GPU texture: shared (#1781) entries decrement
3234
+ * the registry refcount and die with their LAST reference; per-mesh (#961)
3235
+ * uploads are destroyed outright. */
3236
+ releaseTexturedMeshTexture(tm) {
3237
+ if (tm.sharedTextureKey === undefined) {
3238
+ tm.texture.destroy();
3239
+ return;
3240
+ }
3241
+ const entry = this.sharedTextures.get(tm.sharedTextureKey);
3242
+ if (!entry)
3243
+ return;
3244
+ entry.refs--;
3245
+ if (entry.refs <= 0) {
3246
+ entry.texture.destroy();
3247
+ this.sharedTextures.delete(tm.sharedTextureKey);
3248
+ }
3249
+ }
3186
3250
  clear() {
3187
3251
  for (const mesh of this.meshes)
3188
3252
  destroyGpuResources(mesh);
@@ -3192,9 +3256,14 @@ export class Scene {
3192
3256
  tm.vertexBuffer.destroy();
3193
3257
  tm.indexBuffer.destroy();
3194
3258
  tm.uniformBuffer.destroy();
3195
- tm.texture.destroy();
3259
+ this.releaseTexturedMeshTexture(tm);
3196
3260
  }
3197
3261
  this.texturedMeshes = [];
3262
+ // Belt-and-braces: refcounting above should have emptied the registry;
3263
+ // destroy any straggler so clear() can never leak a shared GPU texture.
3264
+ for (const entry of this.sharedTextures.values())
3265
+ entry.texture.destroy();
3266
+ this.sharedTextures.clear();
3198
3267
  // GPU-instancing templates own their vertex/index/instance buffers.
3199
3268
  for (const it of this.instancedTemplates) {
3200
3269
  it.vertexBuffer.destroy();