@inweb/viewer-three 27.4.7 → 27.6.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/extensions/components/AxesHelperComponent.js +3 -0
- package/dist/extensions/components/AxesHelperComponent.js.map +1 -1
- package/dist/extensions/components/AxesHelperComponent.min.js +1 -1
- package/dist/extensions/components/AxesHelperComponent.module.js +3 -0
- package/dist/extensions/components/AxesHelperComponent.module.js.map +1 -1
- package/dist/extensions/components/ExtentsHelperComponent.js +6 -2
- package/dist/extensions/components/ExtentsHelperComponent.js.map +1 -1
- package/dist/extensions/components/ExtentsHelperComponent.min.js +1 -1
- package/dist/extensions/components/ExtentsHelperComponent.module.js +6 -2
- package/dist/extensions/components/ExtentsHelperComponent.module.js.map +1 -1
- package/dist/extensions/components/GridHelperComponent.js +1 -0
- package/dist/extensions/components/GridHelperComponent.js.map +1 -1
- package/dist/extensions/components/GridHelperComponent.min.js +1 -1
- package/dist/extensions/components/GridHelperComponent.module.js +1 -0
- package/dist/extensions/components/GridHelperComponent.module.js.map +1 -1
- package/dist/extensions/components/LightHelperComponent.js +2 -1
- package/dist/extensions/components/LightHelperComponent.js.map +1 -1
- package/dist/extensions/components/LightHelperComponent.min.js +1 -1
- package/dist/extensions/components/LightHelperComponent.module.js +2 -1
- package/dist/extensions/components/LightHelperComponent.module.js.map +1 -1
- package/dist/extensions/components/StatsPanelComponent.js +0 -1
- package/dist/extensions/components/StatsPanelComponent.js.map +1 -1
- package/dist/extensions/components/StatsPanelComponent.min.js +1 -1
- package/dist/extensions/components/StatsPanelComponent.module.js +0 -1
- package/dist/extensions/components/StatsPanelComponent.module.js.map +1 -1
- package/dist/extensions/loaders/GLTFCloudLoader.js +7 -2
- package/dist/extensions/loaders/GLTFCloudLoader.js.map +1 -1
- package/dist/extensions/loaders/GLTFCloudLoader.min.js +1 -1
- package/dist/extensions/loaders/GLTFCloudLoader.module.js +7 -2
- package/dist/extensions/loaders/GLTFCloudLoader.module.js.map +1 -1
- package/dist/extensions/loaders/GLTFFileLoader.js +2 -1
- package/dist/extensions/loaders/GLTFFileLoader.js.map +1 -1
- package/dist/extensions/loaders/GLTFFileLoader.min.js +1 -1
- package/dist/extensions/loaders/GLTFFileLoader.module.js +2 -1
- package/dist/extensions/loaders/GLTFFileLoader.module.js.map +1 -1
- package/dist/extensions/loaders/IFCXLoader.js +10 -5
- package/dist/extensions/loaders/IFCXLoader.js.map +1 -1
- package/dist/extensions/loaders/IFCXLoader.min.js +1 -1
- package/dist/extensions/loaders/IFCXLoader.module.js +10 -5
- package/dist/extensions/loaders/IFCXLoader.module.js.map +1 -1
- package/dist/viewer-three.js +1901 -569
- package/dist/viewer-three.js.map +1 -1
- package/dist/viewer-three.min.js +4 -4
- package/dist/viewer-three.module.js +1366 -451
- package/dist/viewer-three.module.js.map +1 -1
- package/extensions/components/AxesHelperComponent.ts +3 -0
- package/extensions/components/ExtentsHelperComponent.ts +5 -2
- package/extensions/components/GridHelperComponent.ts +1 -0
- package/extensions/components/LightHelperComponent.ts +2 -1
- package/extensions/components/StatsPanelComponent.ts +0 -1
- package/extensions/loaders/GLTFCloudLoader.ts +8 -2
- package/extensions/loaders/GLTFFileLoader.ts +3 -2
- package/extensions/loaders/IFCX/IFCXFileLoader.ts +11 -5
- package/lib/Viewer/Viewer.d.ts +6 -8
- package/lib/Viewer/components/CameraComponent.d.ts +1 -1
- package/lib/Viewer/components/ClippingPlaneComponent.d.ts +8 -0
- package/lib/Viewer/components/HighlighterComponent.d.ts +2 -2
- package/lib/Viewer/components/InfoComponent.d.ts +1 -1
- package/lib/Viewer/components/SectionsComponent.d.ts +15 -0
- package/lib/Viewer/components/WCSHelperComponent.d.ts +2 -2
- package/lib/Viewer/draggers/CuttingPlaneDragger.d.ts +6 -6
- package/lib/Viewer/draggers/OrbitDragger.d.ts +1 -1
- package/lib/Viewer/measurement/Snapper.d.ts +4 -4
- package/package.json +5 -5
- package/src/Viewer/Viewer.ts +59 -48
- package/src/Viewer/commands/GetSelected2.ts +1 -1
- package/src/Viewer/commands/SetSelected.ts +1 -1
- package/src/Viewer/commands/index.ts +1 -1
- package/src/Viewer/components/BackgroundComponent.ts +2 -1
- package/src/Viewer/components/CameraComponent.ts +6 -7
- package/src/Viewer/components/CanvasRemoveComponent.ts +0 -1
- package/src/Viewer/{scenes/Helpers.ts → components/ClippingPlaneComponent.ts} +22 -12
- package/src/Viewer/components/HighlighterComponent.ts +9 -5
- package/src/Viewer/components/HighlighterUtils.ts +2 -2
- package/src/Viewer/components/InfoComponent.ts +4 -4
- package/src/Viewer/components/SectionsComponent.ts +119 -0
- package/src/Viewer/components/SelectionComponent.ts +5 -3
- package/src/Viewer/components/WCSHelperComponent.ts +8 -6
- package/src/Viewer/components/index.ts +4 -0
- package/src/Viewer/draggers/CuttingPlaneDragger.ts +57 -34
- package/src/Viewer/draggers/MeasureLineDragger.ts +1 -1
- package/src/Viewer/draggers/OrbitDragger.ts +3 -3
- package/src/Viewer/helpers/SectionsHelper.js +1061 -0
- package/src/Viewer/helpers/WCSHelper.ts +31 -5
- package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +417 -92
- package/src/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.ts +19 -14
- package/src/Viewer/loaders/DynamicGltfLoader/GltfStructure.js +76 -9
- package/src/Viewer/loaders/GLTFBinaryParser.ts +2 -2
- package/src/Viewer/loaders/GLTFCloudDynamicLoader.ts +3 -2
- package/src/Viewer/loaders/GLTFFileDynamicLoader.ts +6 -4
- package/src/Viewer/measurement/Snapper.ts +6 -7
- package/src/Viewer/models/ModelImpl.ts +65 -28
- package/lib/Viewer/scenes/Helpers.d.ts +0 -7
- 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
|
|
|
@@ -89,11 +113,13 @@ export class WCSHelper extends Object3D {
|
|
|
89
113
|
canvas.height = 64;
|
|
90
114
|
|
|
91
115
|
const context = canvas.getContext("2d");
|
|
92
|
-
context
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
if (context) {
|
|
117
|
+
context.clearRect(0, 0, 64, 64);
|
|
118
|
+
context.font = "24px Arial";
|
|
119
|
+
context.textAlign = "center";
|
|
120
|
+
context.fillStyle = color.getStyle();
|
|
121
|
+
context.fillText(text, 32, 41);
|
|
122
|
+
}
|
|
97
123
|
|
|
98
124
|
const texture = new CanvasTexture(canvas);
|
|
99
125
|
texture.colorSpace = SRGBColorSpace;
|
|
@@ -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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
const
|
|
553
|
-
const
|
|
554
|
-
const
|
|
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:
|
|
559
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
)
|
|
969
|
-
geometryExtents
|
|
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
|
-
|
|
975
|
-
|
|
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;
|