@inweb/viewer-three 27.4.7 → 27.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 (64) hide show
  1. package/dist/extensions/components/AxesHelperComponent.js +3 -0
  2. package/dist/extensions/components/AxesHelperComponent.js.map +1 -1
  3. package/dist/extensions/components/AxesHelperComponent.min.js +1 -1
  4. package/dist/extensions/components/AxesHelperComponent.module.js +3 -0
  5. package/dist/extensions/components/AxesHelperComponent.module.js.map +1 -1
  6. package/dist/extensions/components/ExtentsHelperComponent.js +6 -2
  7. package/dist/extensions/components/ExtentsHelperComponent.js.map +1 -1
  8. package/dist/extensions/components/ExtentsHelperComponent.min.js +1 -1
  9. package/dist/extensions/components/ExtentsHelperComponent.module.js +6 -2
  10. package/dist/extensions/components/ExtentsHelperComponent.module.js.map +1 -1
  11. package/dist/extensions/components/GridHelperComponent.js +1 -0
  12. package/dist/extensions/components/GridHelperComponent.js.map +1 -1
  13. package/dist/extensions/components/GridHelperComponent.min.js +1 -1
  14. package/dist/extensions/components/GridHelperComponent.module.js +1 -0
  15. package/dist/extensions/components/GridHelperComponent.module.js.map +1 -1
  16. package/dist/extensions/components/LightHelperComponent.js +1 -0
  17. package/dist/extensions/components/LightHelperComponent.js.map +1 -1
  18. package/dist/extensions/components/LightHelperComponent.min.js +1 -1
  19. package/dist/extensions/components/LightHelperComponent.module.js +1 -0
  20. package/dist/extensions/components/LightHelperComponent.module.js.map +1 -1
  21. package/dist/viewer-three.js +1766 -438
  22. package/dist/viewer-three.js.map +1 -1
  23. package/dist/viewer-three.min.js +4 -4
  24. package/dist/viewer-three.module.js +1303 -403
  25. package/dist/viewer-three.module.js.map +1 -1
  26. package/extensions/components/AxesHelperComponent.ts +3 -0
  27. package/extensions/components/ExtentsHelperComponent.ts +5 -2
  28. package/extensions/components/GridHelperComponent.ts +1 -0
  29. package/extensions/components/LightHelperComponent.ts +1 -0
  30. package/lib/Viewer/Viewer.d.ts +5 -7
  31. package/lib/Viewer/components/CameraComponent.d.ts +1 -1
  32. package/lib/Viewer/components/ClippingPlaneComponent.d.ts +8 -0
  33. package/lib/Viewer/components/HighlighterComponent.d.ts +2 -2
  34. package/lib/Viewer/components/InfoComponent.d.ts +1 -1
  35. package/lib/Viewer/components/SectionsComponent.d.ts +15 -0
  36. package/lib/Viewer/components/WCSHelperComponent.d.ts +2 -2
  37. package/lib/Viewer/draggers/CuttingPlaneDragger.d.ts +6 -6
  38. package/lib/Viewer/draggers/OrbitDragger.d.ts +1 -1
  39. package/lib/Viewer/measurement/Snapper.d.ts +3 -3
  40. package/package.json +5 -5
  41. package/src/Viewer/Viewer.ts +50 -37
  42. package/src/Viewer/commands/index.ts +1 -1
  43. package/src/Viewer/components/BackgroundComponent.ts +1 -0
  44. package/src/Viewer/components/CameraComponent.ts +5 -6
  45. package/src/Viewer/{scenes/Helpers.ts → components/ClippingPlaneComponent.ts} +22 -12
  46. package/src/Viewer/components/HighlighterComponent.ts +9 -5
  47. package/src/Viewer/components/InfoComponent.ts +4 -4
  48. package/src/Viewer/components/SectionsComponent.ts +119 -0
  49. package/src/Viewer/components/SelectionComponent.ts +1 -1
  50. package/src/Viewer/components/WCSHelperComponent.ts +8 -6
  51. package/src/Viewer/components/index.ts +4 -0
  52. package/src/Viewer/draggers/CuttingPlaneDragger.ts +57 -34
  53. package/src/Viewer/draggers/MeasureLineDragger.ts +1 -1
  54. package/src/Viewer/draggers/OrbitDragger.ts +3 -3
  55. package/src/Viewer/helpers/SectionsHelper.js +1065 -0
  56. package/src/Viewer/helpers/WCSHelper.ts +24 -0
  57. package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +417 -92
  58. package/src/Viewer/loaders/DynamicGltfLoader/GltfStructure.js +76 -9
  59. package/src/Viewer/loaders/GLTFCloudDynamicLoader.ts +3 -2
  60. package/src/Viewer/loaders/GLTFFileDynamicLoader.ts +4 -2
  61. package/src/Viewer/measurement/Snapper.ts +4 -5
  62. package/src/Viewer/models/ModelImpl.ts +27 -3
  63. package/lib/Viewer/scenes/Helpers.d.ts +0 -7
  64. package/src/Viewer/postprocessing/SSAARenderPass.js +0 -245
@@ -1,3 +1,26 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2002-2026, Open Design Alliance (the "Alliance").
3
+ // All rights reserved.
4
+ //
5
+ // This software and its documentation and related materials are owned by
6
+ // the Alliance. The software may only be incorporated into application
7
+ // programs owned by members of the Alliance, subject to a signed
8
+ // Membership Agreement and Supplemental Software License Agreement with the
9
+ // Alliance. The structure and organization of this software are the valuable
10
+ // trade secrets of the Alliance and its suppliers. The software is also
11
+ // protected by copyright law and international treaty provisions. Application
12
+ // programs incorporating this software must include the following statement
13
+ // with their copyright notices:
14
+ //
15
+ // This application incorporates Open Design Alliance software pursuant to a
16
+ // license agreement with Open Design Alliance.
17
+ // Open Design Alliance Copyright (C) 2002-2026 by Open Design Alliance.
18
+ // All rights reserved.
19
+ //
20
+ // By use of this software, its documentation or related materials, you
21
+ // acknowledge and accept the above terms.
22
+ ///////////////////////////////////////////////////////////////////////////////
23
+
1
24
  import {
2
25
  Camera,
3
26
  CylinderGeometry,
@@ -22,6 +45,7 @@ export class WCSHelper extends Object3D {
22
45
  constructor(camera: Camera) {
23
46
  super();
24
47
 
48
+ (this as any).type = "WCSHelper";
25
49
  this.camera = camera;
26
50
  this.size = 160;
27
51
 
@@ -30,6 +30,8 @@ import {
30
30
  import { GL_CONSTANTS } from "./GltfStructure.js";
31
31
  import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
32
32
 
33
+ const DRACO_EXTENSION_NAME = "KHR_draco_mesh_compression";
34
+
33
35
  const STRUCTURE_ID_SEPARATOR = ":";
34
36
 
35
37
  //#AI-GENERATED using Gemini 2.5 Pro, Claude-4-sonnet
@@ -138,6 +140,278 @@ export class DynamicGltfLoader {
138
140
  this.transformData = null;
139
141
  this.identityTransformData = null;
140
142
  this.visibilityMaterials = new Set(); // Keep track of materials to update uniforms
143
+
144
+ // KHR_draco_mesh_compression support — DRACOLoader is injected from
145
+ // the outside via setDracoLoader() so this module doesn't carry a hard
146
+ // dependency on three/examples/jsm/loaders/DRACOLoader.js (which has no
147
+ // CommonJS build and is awkward to consume in non-ESM environments).
148
+ this._dracoLoader = null;
149
+ }
150
+
151
+ // Inject an externally-configured DRACOLoader instance. The caller is
152
+ // responsible for setting the decoder path / decoder config / worker pool
153
+ // size before passing it in. Pass `null` to detach.
154
+ //
155
+ // Usage:
156
+ // import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
157
+ // const draco = new DRACOLoader();
158
+ // draco.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
159
+ // loader.setDracoLoader(draco);
160
+ setDracoLoader(loader = null) {
161
+ this._dracoLoader = loader || null;
162
+ }
163
+
164
+ // Decode a Draco-compressed primitive into a BufferGeometry. The caller
165
+ // (loadNode) guarantees this._dracoLoader is set — see the early check at
166
+ // the top of loadNode().
167
+ //
168
+ // Important detail: DRACOLoader.decodeGeometry() uses the *keys* of
169
+ // taskConfig.attributeIDs verbatim as attribute names on the resulting
170
+ // BufferGeometry. So if we pass dracoExt.attributes directly (POSITION,
171
+ // NORMAL, TEXCOORD_0…) the geometry ends up with those uppercase names
172
+ // and downstream code that expects three.js conventions (position,
173
+ // normal, uv) breaks. We pre-translate the keys here.
174
+ async _decodeDracoPrimitive(structure, primitive, dracoBufferData) {
175
+ const dracoExt = primitive.extensions[DRACO_EXTENSION_NAME];
176
+ const loader = this._dracoLoader;
177
+
178
+ const attributeIDs = {};
179
+ const attributeTypes = {};
180
+ // glTF attribute name → three.js name, used both for re-keying the
181
+ // DRACOLoader config and for re-mapping bounds afterwards.
182
+ const gltfNameToThreeName = new Map();
183
+ // three.js attribute name → glTF accessor index (for normalized flag,
184
+ // bounds, and post-decode dequantization).
185
+ const threeNameToAccessor = new Map();
186
+
187
+ for (const [gltfAttrName, dracoUniqueId] of Object.entries(dracoExt.attributes)) {
188
+ const threeName = this._gltfAttributeNameToThreeName(gltfAttrName);
189
+ attributeIDs[threeName] = dracoUniqueId;
190
+ gltfNameToThreeName.set(gltfAttrName, threeName);
191
+
192
+ const accessorIdx = primitive.attributes[gltfAttrName];
193
+ if (accessorIdx !== undefined) {
194
+ const accessor = structure.json.accessors[accessorIdx];
195
+ attributeTypes[threeName] = this._gltfComponentTypeToTypedArrayName(accessor.componentType);
196
+ threeNameToAccessor.set(threeName, accessor);
197
+ }
198
+ }
199
+
200
+ const geometry = await loader.decodeGeometry(dracoBufferData, {
201
+ attributeIDs,
202
+ attributeTypes,
203
+ useUniqueIDs: true,
204
+ });
205
+
206
+ // DRACOLoader doesn't carry the accessor.normalized flag through, so
207
+ // we apply it manually. This also matters for KHR_mesh_quantization +
208
+ // Draco combined files (rare, but spec-compliant).
209
+ for (const [threeName, accessor] of threeNameToAccessor) {
210
+ const attribute = geometry.getAttribute(threeName);
211
+ if (!attribute) continue;
212
+ if (accessor.normalized === true) {
213
+ attribute.normalized = true;
214
+ }
215
+ // Min/max bounds — used by computeBoundingBox/Sphere fast-path.
216
+ if (accessor.min) attribute.min = accessor.min;
217
+ if (accessor.max) attribute.max = accessor.max;
218
+ }
219
+
220
+ // Mirror the dequantization done in _createGeometryAttribute() for
221
+ // ordinary glTF attributes: convert any normalized integer attributes
222
+ // into a tightly-packed Float32Array so that BufferGeometryUtils
223
+ // .mergeGeometries() (used by this loader for batched draw calls) sees
224
+ // a consistent layout across all primitives.
225
+ for (const [threeName, accessor] of threeNameToAccessor) {
226
+ const attribute = geometry.getAttribute(threeName);
227
+ if (!attribute || !attribute.normalized) continue;
228
+ const denom = this._normalizedDenominator(accessor.componentType);
229
+ if (denom <= 0) continue;
230
+ const src = attribute.array;
231
+ const inv = 1 / denom;
232
+ const isSigned = accessor.componentType === 5120 || accessor.componentType === 5122;
233
+ const out = new Float32Array(src.length);
234
+ for (let i = 0; i < src.length; i++) {
235
+ let v = src[i] * inv;
236
+ if (isSigned && v < -1) v = -1;
237
+ out[i] = v;
238
+ }
239
+ const newAttr = new BufferAttribute(out, attribute.itemSize, false);
240
+ if (accessor.min) newAttr.min = accessor.min;
241
+ if (accessor.max) newAttr.max = accessor.max;
242
+ geometry.setAttribute(threeName, newAttr);
243
+ }
244
+
245
+ return geometry;
246
+ }
247
+
248
+ _gltfComponentTypeToTypedArrayName(componentType) {
249
+ switch (componentType) {
250
+ case 5120:
251
+ return "Int8Array";
252
+ case 5121:
253
+ return "Uint8Array";
254
+ case 5122:
255
+ return "Int16Array";
256
+ case 5123:
257
+ return "Uint16Array";
258
+ case 5125:
259
+ return "Uint32Array";
260
+ case 5126:
261
+ return "Float32Array";
262
+ default:
263
+ return "Float32Array";
264
+ }
265
+ }
266
+
267
+ // glTF normalization denominators (per glTF 2.0 spec, accessor.normalized).
268
+ _normalizedDenominator(componentType) {
269
+ switch (componentType) {
270
+ case 5120:
271
+ return 127; // BYTE
272
+ case 5121:
273
+ return 255; // UBYTE
274
+ case 5122:
275
+ return 32767; // SHORT
276
+ case 5123:
277
+ return 65535; // USHORT
278
+ default:
279
+ return 0; // not normalizable
280
+ }
281
+ }
282
+
283
+ // Map glTF attribute names to three.js BufferGeometry attribute names.
284
+ _gltfAttributeNameToThreeName(name) {
285
+ switch (name) {
286
+ case "POSITION":
287
+ return "position";
288
+ case "NORMAL":
289
+ return "normal";
290
+ case "TANGENT":
291
+ return "tangent";
292
+ case "TEXCOORD_0":
293
+ return "uv";
294
+ case "TEXCOORD_1":
295
+ return "uv2";
296
+ case "COLOR_0":
297
+ return "color";
298
+ case "JOINTS_0":
299
+ return "skinIndex";
300
+ case "WEIGHTS_0":
301
+ return "skinWeight";
302
+ default:
303
+ return name.toLowerCase();
304
+ }
305
+ }
306
+
307
+ // Build a buffer request for an accessor that correctly accounts for
308
+ // bufferView.byteStride (interleaved attributes — common in quantized models
309
+ // produced via KHR_mesh_quantization / gltfpack).
310
+ _buildAccessorRequest(structure, accessorIndex, type, primIdx) {
311
+ const accessor = structure.json.accessors[accessorIndex];
312
+ const bufferView = structure.json.bufferViews[accessor.bufferView];
313
+ const components = structure.getNumComponents(accessor.type);
314
+ const componentSize = structure.getComponentSize(accessor.componentType);
315
+ const itemBytes = components * componentSize;
316
+ const accessorByteOffset = accessor.byteOffset || 0;
317
+ const bvByteOffset = bufferView.byteOffset || 0;
318
+ const byteStride = bufferView.byteStride || 0;
319
+ const interleaved = byteStride !== 0 && byteStride !== itemBytes;
320
+
321
+ const offset = bvByteOffset + accessorByteOffset;
322
+ let length;
323
+ if (interleaved) {
324
+ // For interleaved data the last element begins at (count-1)*stride
325
+ // and spans itemBytes from there. Fetch only that tight extent.
326
+ length = (accessor.count - 1) * byteStride + itemBytes;
327
+ } else {
328
+ length = accessor.count * itemBytes;
329
+ }
330
+
331
+ return {
332
+ offset,
333
+ length,
334
+ componentType: accessor.componentType,
335
+ accessorIndex,
336
+ type,
337
+ primIdx,
338
+ // Metadata used by the geometry-assembly pass below.
339
+ _accessor: accessor,
340
+ _components: components,
341
+ _componentSize: componentSize,
342
+ _itemBytes: itemBytes,
343
+ _byteStride: byteStride,
344
+ _interleaved: interleaved,
345
+ };
346
+ }
347
+
348
+ // Create a plain BufferAttribute, handling two transformations that
349
+ // KHR_mesh_quantization-encoded glTF files commonly require:
350
+ // 1. Deinterleave when the accessor lives inside a strided bufferView.
351
+ // 2. Dequantize (integer typed array + accessor.normalized=true) into
352
+ // a tightly-packed Float32Array in [-1,1] / [0,1] range.
353
+ //
354
+ // Reasons for always emitting a non-strided, non-normalized Float32Array
355
+ // attribute:
356
+ // * BufferGeometryUtils.mergeGeometries() (used heavily in this loader
357
+ // to batch draw calls) refuses to merge InterleavedBufferAttribute
358
+ // and requires identical TypedArray + `normalized` across primitives.
359
+ // Eagerly dequantizing here lets us mix quantized and non-quantized
360
+ // primitives in the same merged buffer.
361
+ // * Geometry-local bounds (boundingBox, processNodeHierarchy extents)
362
+ // and per-vertex CPU-side traversal can then assume float values.
363
+ //
364
+ // The trade-off is some extra CPU memory (4 bytes per component instead of
365
+ // 1/2). The download size benefit of quantization is preserved.
366
+ _createGeometryAttribute(req) {
367
+ const accessor = req._accessor;
368
+ const components = req._components;
369
+ const count = accessor.count;
370
+ const stride = req._interleaved ? req._byteStride / req._componentSize : components;
371
+ const normalized = accessor.normalized === true;
372
+ const componentType = req.componentType;
373
+ const src = req.data;
374
+
375
+ // Fast path: tightly-packed, non-normalized data — no copy needed.
376
+ // Covers the common cases: non-quantized FLOAT attributes and integer
377
+ // indices. The `req.data` view already has the correct length & type.
378
+ if (!req._interleaved && !normalized) {
379
+ return new BufferAttribute(src, components, false);
380
+ }
381
+
382
+ // Normalized integer attribute (KHR_mesh_quantization) → dequantize to
383
+ // Float32 in [-1,1] / [0,1] range. This keeps merge/bounds simple.
384
+ if (normalized) {
385
+ const denom = this._normalizedDenominator(componentType);
386
+ if (denom > 0) {
387
+ const out = new Float32Array(count * components);
388
+ const inv = 1 / denom;
389
+ const isSignedNormalized = componentType === 5120 || componentType === 5122;
390
+ for (let i = 0; i < count; i++) {
391
+ const srcBase = i * stride;
392
+ const dstBase = i * components;
393
+ for (let c = 0; c < components; c++) {
394
+ let v = src[srcBase + c] * inv;
395
+ if (isSignedNormalized && v < -1) v = -1; // glTF spec clamp
396
+ out[dstBase + c] = v;
397
+ }
398
+ }
399
+ return new BufferAttribute(out, components, false);
400
+ }
401
+ }
402
+
403
+ // Interleaved + non-normalized: deinterleave but preserve original
404
+ // typed-array type (rare, but handle it for completeness).
405
+ const TypedArrayCtor = src.constructor;
406
+ const out = new TypedArrayCtor(count * components);
407
+ for (let i = 0; i < count; i++) {
408
+ const srcBase = i * stride;
409
+ const dstBase = i * components;
410
+ for (let c = 0; c < components; c++) {
411
+ out[dstBase + c] = src[srcBase + c];
412
+ }
413
+ }
414
+ return new BufferAttribute(out, components, false);
141
415
  }
142
416
 
143
417
  createDummyTexture() {
@@ -241,7 +515,7 @@ export class DynamicGltfLoader {
241
515
  this.transformTexture.needsUpdate = true;
242
516
  }
243
517
 
244
- setVisibleEdges(visible) {
518
+ setVisibleEdges(visible = true) {
245
519
  this.visibleEdges = visible;
246
520
  }
247
521
 
@@ -538,83 +812,66 @@ export class DynamicGltfLoader {
538
812
  const meshDef = node.structure.getJson().meshes[node.meshIndex];
539
813
 
540
814
  try {
815
+ // Fail fast: if any primitive in this mesh requires Draco but no
816
+ // DRACOLoader was injected, abort with a single descriptive error
817
+ // *before* we fetch any buffers. Otherwise downstream code
818
+ // (estimateGeometrySize, mesh assembly, etc.) would hit secondary
819
+ // null-deref crashes that mask the real cause. The throw is caught
820
+ // by the surrounding try/catch and logged once via the standard
821
+ // "Error loading node N" channel.
822
+ if (
823
+ !this._dracoLoader &&
824
+ meshDef.primitives &&
825
+ meshDef.primitives.some((p) => p.extensions && p.extensions[DRACO_EXTENSION_NAME])
826
+ ) {
827
+ throw new Error(
828
+ "primitive uses KHR_draco_mesh_compression but no DRACOLoader is configured. " +
829
+ "Inject one via dynamicLoader.setDracoLoader(new DRACOLoader()) before opening the file."
830
+ );
831
+ }
832
+
541
833
  const bufferRequests = [];
542
834
  const primitiveReqMap = new Map();
835
+ const dracoPrimitives = new Map(); // primIdx -> { req: {offset, length}, primitive }
836
+
543
837
  for (let primIdx = 0; primIdx < meshDef.primitives.length; primIdx++) {
544
838
  const primitive = meshDef.primitives[primIdx];
545
839
  const reqs = [];
546
840
 
547
- if (primitive.attributes.POSITION !== undefined) {
548
- const accessorIndex = primitive.attributes.POSITION;
549
- const accessor = node.structure.json.accessors[accessorIndex];
550
- const bufferView = node.structure.json.bufferViews[accessor.bufferView];
551
- const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
552
- const components = node.structure.getNumComponents(accessor.type);
553
- const count = accessor.count;
554
- const byteLength = count * components * node.structure.getComponentSize(accessor.componentType);
555
- reqs.push({
841
+ // KHR_draco_mesh_compression: load the entire compressed bufferView
842
+ // and skip the per-attribute requests below.
843
+ const dracoExt = primitive.extensions && primitive.extensions[DRACO_EXTENSION_NAME];
844
+ if (dracoExt) {
845
+ const bufferView = node.structure.json.bufferViews[dracoExt.bufferView];
846
+ const byteOffset = bufferView.byteOffset || 0;
847
+ const byteLength = bufferView.byteLength;
848
+ const dracoReq = {
556
849
  offset: byteOffset,
557
850
  length: byteLength,
558
- componentType: accessor.componentType,
559
- accessorIndex,
560
- type: "position",
851
+ componentType: 5121, // raw bytes
852
+ type: "draco",
561
853
  primIdx,
562
- });
854
+ };
855
+ reqs.push(dracoReq);
856
+ dracoPrimitives.set(primIdx, { req: dracoReq, primitive });
857
+ primitiveReqMap.set(primIdx, reqs);
858
+ bufferRequests.push(...reqs);
859
+ continue;
563
860
  }
564
861
 
862
+ if (primitive.attributes.POSITION !== undefined) {
863
+ reqs.push(this._buildAccessorRequest(node.structure, primitive.attributes.POSITION, "position", primIdx));
864
+ }
565
865
  if (primitive.attributes.NORMAL !== undefined) {
566
- const accessorIndex = primitive.attributes.NORMAL;
567
- const accessor = node.structure.json.accessors[accessorIndex];
568
- const bufferView = node.structure.json.bufferViews[accessor.bufferView];
569
- const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
570
- const components = node.structure.getNumComponents(accessor.type);
571
- const count = accessor.count;
572
- const byteLength = count * components * node.structure.getComponentSize(accessor.componentType);
573
- reqs.push({
574
- offset: byteOffset,
575
- length: byteLength,
576
- componentType: accessor.componentType,
577
- accessorIndex,
578
- type: "normal",
579
- primIdx,
580
- });
866
+ reqs.push(this._buildAccessorRequest(node.structure, primitive.attributes.NORMAL, "normal", primIdx));
581
867
  }
582
-
583
868
  if (primitive.attributes.TEXCOORD_0 !== undefined) {
584
- const accessorIndex = primitive.attributes.TEXCOORD_0;
585
- const accessor = node.structure.json.accessors[accessorIndex];
586
- const bufferView = node.structure.json.bufferViews[accessor.bufferView];
587
- const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
588
- const components = node.structure.getNumComponents(accessor.type);
589
- const count = accessor.count;
590
- const byteLength = count * components * node.structure.getComponentSize(accessor.componentType);
591
- reqs.push({
592
- offset: byteOffset,
593
- length: byteLength,
594
- componentType: accessor.componentType,
595
- accessorIndex,
596
- type: "uv",
597
- primIdx,
598
- });
869
+ reqs.push(this._buildAccessorRequest(node.structure, primitive.attributes.TEXCOORD_0, "uv", primIdx));
599
870
  }
600
-
601
871
  if (primitive.indices !== undefined) {
602
- const accessorIndex = primitive.indices;
603
- const accessor = node.structure.json.accessors[accessorIndex];
604
- const bufferView = node.structure.json.bufferViews[accessor.bufferView];
605
- const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
606
- const components = node.structure.getNumComponents(accessor.type);
607
- const count = accessor.count;
608
- const byteLength = count * components * node.structure.getComponentSize(accessor.componentType);
609
- reqs.push({
610
- offset: byteOffset,
611
- length: byteLength,
612
- componentType: accessor.componentType,
613
- accessorIndex,
614
- type: "index",
615
- primIdx,
616
- });
872
+ reqs.push(this._buildAccessorRequest(node.structure, primitive.indices, "index", primIdx));
617
873
  }
874
+
618
875
  primitiveReqMap.set(primIdx, reqs);
619
876
  bufferRequests.push(...reqs);
620
877
  }
@@ -643,33 +900,45 @@ export class DynamicGltfLoader {
643
900
 
644
901
  for (let primIdx = 0; primIdx < meshDef.primitives.length; primIdx++) {
645
902
  const primitive = meshDef.primitives[primIdx];
646
- const geometry = new BufferGeometry();
647
903
  const reqs = primitiveReqMap.get(primIdx);
904
+ let geometry;
905
+
906
+ // Handle Draco-compressed primitives
907
+ if (dracoPrimitives.has(primIdx)) {
908
+ const dracoReq = reqs.find((r) => r.type === "draco");
909
+ // Build a clean ArrayBuffer for the DRACO decoder.
910
+ // dracoReq.data is a Uint8Array view into the larger shared buffer.
911
+ const dracoBytes = new Uint8Array(dracoReq.data.buffer, dracoReq.data.byteOffset, dracoReq.data.byteLength);
912
+ const dracoBuffer = dracoBytes.slice().buffer; // copy to standalone ArrayBuffer
913
+ // Any decode failure (e.g. corrupted compressed data) will bubble
914
+ // up to the outer try/catch and be reported once via the standard
915
+ // "Error loading node N" channel rather than a misleading
916
+ // "skipping primitive" warning followed by a downstream null deref.
917
+ geometry = await this._decodeDracoPrimitive(node.structure, primitive, dracoBuffer);
918
+ } else {
919
+ geometry = new BufferGeometry();
648
920
 
649
- if (primitive.attributes.POSITION !== undefined) {
650
- const req = reqs.find((r) => r.type === "position" && r.accessorIndex === primitive.attributes.POSITION);
651
- const accessor = node.structure.json.accessors[primitive.attributes.POSITION];
652
- const components = node.structure.getNumComponents(accessor.type);
653
- geometry.setAttribute("position", new BufferAttribute(req.data, components));
654
- }
921
+ if (primitive.attributes.POSITION !== undefined) {
922
+ const req = reqs.find((r) => r.type === "position" && r.accessorIndex === primitive.attributes.POSITION);
923
+ geometry.setAttribute("position", this._createGeometryAttribute(req));
924
+ }
655
925
 
656
- if (primitive.attributes.NORMAL !== undefined) {
657
- const req = reqs.find((r) => r.type === "normal" && r.accessorIndex === primitive.attributes.NORMAL);
658
- const accessor = node.structure.json.accessors[primitive.attributes.NORMAL];
659
- const components = node.structure.getNumComponents(accessor.type);
660
- geometry.setAttribute("normal", new BufferAttribute(req.data, components));
661
- }
926
+ if (primitive.attributes.NORMAL !== undefined) {
927
+ const req = reqs.find((r) => r.type === "normal" && r.accessorIndex === primitive.attributes.NORMAL);
928
+ geometry.setAttribute("normal", this._createGeometryAttribute(req));
929
+ }
662
930
 
663
- if (primitive.attributes.TEXCOORD_0 !== undefined) {
664
- const req = reqs.find((r) => r.type === "uv" && r.accessorIndex === primitive.attributes.TEXCOORD_0);
665
- const accessor = node.structure.json.accessors[primitive.attributes.TEXCOORD_0];
666
- const components = node.structure.getNumComponents(accessor.type);
667
- geometry.setAttribute("uv", new BufferAttribute(req.data, components));
668
- }
931
+ if (primitive.attributes.TEXCOORD_0 !== undefined) {
932
+ const req = reqs.find((r) => r.type === "uv" && r.accessorIndex === primitive.attributes.TEXCOORD_0);
933
+ geometry.setAttribute("uv", this._createGeometryAttribute(req));
934
+ }
669
935
 
670
- if (primitive.indices !== undefined) {
671
- const req = reqs.find((r) => r.type === "index" && r.accessorIndex === primitive.indices);
672
- geometry.setIndex(new BufferAttribute(req.data, 1));
936
+ if (primitive.indices !== undefined) {
937
+ const req = reqs.find((r) => r.type === "index" && r.accessorIndex === primitive.indices);
938
+ // Indices are never interleaved per glTF spec, but use the same
939
+ // helper for consistency (it falls back to BufferAttribute).
940
+ geometry.setIndex(this._createGeometryAttribute(req));
941
+ }
673
942
  }
674
943
 
675
944
  let material;
@@ -957,22 +1226,56 @@ export class DynamicGltfLoader {
957
1226
  const nodeMatrix = new Matrix4();
958
1227
  const uniqueNodeId = `${structure.id}_${nodeId}`;
959
1228
  const meshDef = structure.json.meshes[nodeDef.mesh];
1229
+
1230
+ // Some glTF files may reference a mesh index that has no primitives
1231
+ // (or no mesh at all). Skip this mesh node gracefully.
1232
+ if (!meshDef || !meshDef.primitives || meshDef.primitives.length === 0) {
1233
+ if (nodeDef.children) {
1234
+ for (const childId of nodeDef.children) {
1235
+ await this.processNodeHierarchy(structure, childId, nodeGroup || parentGroup);
1236
+ }
1237
+ }
1238
+ return nodeGroup;
1239
+ }
1240
+
960
1241
  const geometryExtents = new Box3();
961
1242
 
962
1243
  for (const primitive of meshDef.primitives) {
1244
+ if (!primitive.attributes) continue;
963
1245
  const positionAccessor = structure.json.accessors[primitive.attributes.POSITION];
964
1246
  if (positionAccessor && positionAccessor.min && positionAccessor.max) {
965
- const primitiveBox = new Box3(
966
- new Vector3().fromArray(positionAccessor.min),
967
- new Vector3().fromArray(positionAccessor.max)
968
- );
969
- geometryExtents.union(primitiveBox);
1247
+ // For KHR_mesh_quantization (normalized integer accessors) min/max
1248
+ // are stored in quantized integer space (e.g. -32767..32767). The
1249
+ // BufferGeometry's local space, however, is the *normalized* range
1250
+ // (-1..1 or 0..1) because the GPU performs dequantization via the
1251
+ // `normalized` attribute flag. Convert here so geometryExtents is
1252
+ // in geometry-local (post-normalization) space.
1253
+ const minVec = new Vector3().fromArray(positionAccessor.min);
1254
+ const maxVec = new Vector3().fromArray(positionAccessor.max);
1255
+ if (positionAccessor.normalized === true) {
1256
+ const denom = this._normalizedDenominator(positionAccessor.componentType);
1257
+ if (denom > 0) {
1258
+ minVec.divideScalar(denom);
1259
+ maxVec.divideScalar(denom);
1260
+ // For signed types the result is clamped to [-1, 1] per spec.
1261
+ if (positionAccessor.componentType === 5120 || positionAccessor.componentType === 5122) {
1262
+ minVec.x = Math.max(minVec.x, -1);
1263
+ minVec.y = Math.max(minVec.y, -1);
1264
+ minVec.z = Math.max(minVec.z, -1);
1265
+ maxVec.x = Math.max(maxVec.x, -1);
1266
+ maxVec.y = Math.max(maxVec.y, -1);
1267
+ maxVec.z = Math.max(maxVec.z, -1);
1268
+ }
1269
+ }
1270
+ }
1271
+ geometryExtents.union(new Box3(minVec, maxVec));
970
1272
  }
971
1273
  }
972
1274
 
973
1275
  let isEdge = false;
974
- if (meshDef.primitives[0].material !== undefined) {
975
- const material = structure.json.materials[meshDef.primitives[0].material];
1276
+ const firstPrimitive = meshDef.primitives[0];
1277
+ if (firstPrimitive && firstPrimitive.material !== undefined) {
1278
+ const material = structure.json.materials[firstPrimitive.material];
976
1279
  if (material?.name === "edges") {
977
1280
  isEdge = true;
978
1281
  }
@@ -1332,7 +1635,20 @@ export class DynamicGltfLoader {
1332
1635
  `
1333
1636
  );
1334
1637
 
1335
- // 3. Transform Normal
1638
+ // 3. Transform Normal (and Tangent, when present).
1639
+ //
1640
+ // We replace the standard `beginnormal_vertex` chunk because we need
1641
+ // to apply the per-object batching matrix to the normal. The original
1642
+ // three.js chunk also conditionally declares `objectTangent` under
1643
+ // `#ifdef USE_TANGENT` — that define is enabled automatically by
1644
+ // three.js whenever the material uses a normalMap (or clearcoat
1645
+ // normalMap) AND the geometry has a `tangent` attribute (e.g.
1646
+ // Avocado/BoomBox glTF samples). If our replacement omits that
1647
+ // declaration, downstream chunks (`defaultnormal_vertex`, etc.) fail
1648
+ // to compile with "objectTangent: undeclared identifier".
1649
+ //
1650
+ // We mirror three.js' own `skinnormal_vertex` convention: the same
1651
+ // per-vertex matrix that rotates the normal also rotates the tangent.
1336
1652
  if (shader.vertexShader.includes("#include <beginnormal_vertex>")) {
1337
1653
  shader.vertexShader = shader.vertexShader.replace(
1338
1654
  "#include <beginnormal_vertex>",
@@ -1340,6 +1656,10 @@ export class DynamicGltfLoader {
1340
1656
  vec3 objectNormal = vec3( normal );
1341
1657
  mat3 bm = mat3( batchingMatrix );
1342
1658
  objectNormal = bm * objectNormal;
1659
+ #ifdef USE_TANGENT
1660
+ vec3 objectTangent = vec3( tangent.xyz );
1661
+ objectTangent = bm * objectTangent;
1662
+ #endif
1343
1663
  `
1344
1664
  );
1345
1665
  }
@@ -1504,6 +1824,11 @@ export class DynamicGltfLoader {
1504
1824
  this.objectVisibility = new Float32Array();
1505
1825
  this.meshToNodeMap = null;
1506
1826
  this.visibilityMaterials.clear();
1827
+
1828
+ // NOTE: do *not* drop this._dracoLoader here. The DRACOLoader is owned
1829
+ // by the caller (injected via setDracoLoader) and should survive across
1830
+ // clear()/reload cycles — the loader keeps its worker pool warm and is
1831
+ // typically configured once at app startup.
1507
1832
  }
1508
1833
 
1509
1834
  setStructureTransform(structureId, matrix) {
@@ -2820,7 +3145,7 @@ export class DynamicGltfLoader {
2820
3145
  return extent;
2821
3146
  }
2822
3147
 
2823
- setMaxConcurrentChunks(maxChunks) {
3148
+ setMaxConcurrentChunks(maxChunks = 6) {
2824
3149
  if (maxChunks < 1) {
2825
3150
  console.warn("Max concurrent chunks must be at least 1");
2826
3151
  return;