@inweb/viewer-three 26.7.6 → 26.8.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.
@@ -62,10 +62,8 @@ export class DynamicModelImpl extends ModelImpl {
62
62
  }
63
63
 
64
64
  override hideObjects(objects: Object3D | Object3D[]): this {
65
- this.getOwnObjects(objects)
66
- .map((object) => object.userData.handle)
67
- .forEach((handle) => this.gltfLoader.hiddenHandles.add(handle));
68
- this.gltfLoader.syncHiddenObjects();
65
+ const handles = this.getHandlesByObjects(objects);
66
+ this.gltfLoader.hideObjects(handles);
69
67
  return this;
70
68
  }
71
69
 
@@ -76,26 +74,23 @@ export class DynamicModelImpl extends ModelImpl {
76
74
  }
77
75
 
78
76
  override showObjects(objects: Object3D | Object3D[]): this {
79
- this.getOwnObjects(objects)
80
- .map((object) => object.userData.handle)
81
- .forEach((handle) => this.gltfLoader.hiddenHandles.delete(handle));
82
- this.gltfLoader.syncHiddenObjects();
77
+ const handles = this.getHandlesByObjects(objects);
78
+ this.gltfLoader.showObjects(handles);
83
79
  return this;
84
80
  }
85
81
 
86
82
  override showAllObjects(): this {
87
- this.gltfLoader.hiddenHandles.clear();
88
- this.gltfLoader.syncHiddenObjects();
83
+ this.gltfLoader.showAllHiddenObjects();
89
84
  return this;
90
85
  }
91
86
 
92
87
  override showOriginalObjects(objects: Object3D | Object3D[]): this {
93
- this.getOwnObjects(objects).forEach((object) => (object.visible = true));
88
+ this.gltfLoader.showOriginalObjects(objects);
94
89
  return this;
95
90
  }
96
91
 
97
92
  override hideOriginalObjects(objects: Object3D | Object3D[]): this {
98
- this.getOwnObjects(objects).forEach((object) => (object.visible = false));
93
+ this.gltfLoader.hideOriginalObjects(objects);
99
94
  return this;
100
95
  }
101
96
  }
@@ -1,4 +1,4 @@
1
- import { TextureLoader, BufferAttribute, MeshStandardMaterial, DoubleSide, FrontSide } from "three";
1
+ import { TextureLoader, BufferAttribute, Color, DoubleSide, MeshPhongMaterial } from "three";
2
2
 
3
3
  export const GL_COMPONENT_TYPES = {
4
4
  5120: Int8Array,
@@ -31,24 +31,20 @@ export const GL_CONSTANTS = {
31
31
  UNSIGNED_SHORT: 5123,
32
32
  };
33
33
 
34
+ const MAX_GAP = 128 * 1024; // 128 KB
35
+ const MAX_CHUNK = 30 * 1024 * 1024; // 100 MB
36
+
34
37
  export class GltfStructure {
35
38
  constructor(id) {
36
39
  this.id = `${id}`;
37
40
  this.json = null;
38
41
  this.baseUrl = "";
39
-
40
- // Binary manager properties
41
42
  this.loadController = null;
42
- // Request batching parameters
43
43
  this.batchDelay = 10;
44
44
  this.maxBatchSize = 5 * 1024 * 1024;
45
45
  this.maxRangesPerRequest = 512;
46
-
47
- // Request queue
48
46
  this.pendingRequests = [];
49
47
  this.batchTimeout = null;
50
-
51
- // Material and texture properties
52
48
  this.textureLoader = new TextureLoader();
53
49
  this.materials = new Map();
54
50
  this.textureCache = new Map();
@@ -80,57 +76,110 @@ export class GltfStructure {
80
76
  return this.json;
81
77
  }
82
78
 
83
- // Schedule a request for processing
84
79
  scheduleRequest(request) {
85
- this.pendingRequests.push(request);
86
-
87
- // Clear existing timeout
88
- if (this.batchTimeout) {
89
- clearTimeout(this.batchTimeout);
90
- }
91
-
92
- // Set new timeout for batch processing
93
- this.batchTimeout = setTimeout(() => this.processBatch(), this.batchDelay);
94
-
95
- // Return a promise that will resolve when the data is available
96
80
  return new Promise((resolve, reject) => {
97
- request.resolve = resolve;
98
- request.reject = reject;
81
+ this.pendingRequests.push({
82
+ ...request,
83
+ _resolve: resolve,
84
+ _reject: reject,
85
+ });
99
86
  });
100
87
  }
101
88
 
102
- async processBatch() {
103
- if (this.pendingRequests.length === 0) return;
104
-
105
- // Take current batch of requests and clear timeout
106
- const currentBatch = [...this.pendingRequests];
89
+ async flushBufferRequests() {
90
+ if (!this.pendingRequests || this.pendingRequests.length === 0) return;
91
+ const requests = [...this.pendingRequests];
107
92
  this.pendingRequests = [];
108
93
 
109
- if (this.batchTimeout) {
110
- clearTimeout(this.batchTimeout);
111
- this.batchTimeout = null;
94
+ requests.sort((a, b) => a.offset - b.offset);
95
+ const mergedRanges = [];
96
+ let current = {
97
+ start: requests[0].offset,
98
+ end: requests[0].offset + requests[0].length,
99
+ requests: [requests[0]],
100
+ };
101
+ for (let i = 1; i < requests.length; i++) {
102
+ const req = requests[i];
103
+ const gap = req.offset - current.end;
104
+ const newEnd = Math.max(current.end, req.offset + req.length);
105
+ const projectedSize = newEnd - current.start;
106
+ if (gap <= MAX_GAP && projectedSize <= MAX_CHUNK) {
107
+ current.end = newEnd;
108
+ current.requests.push(req);
109
+ } else {
110
+ mergedRanges.push(current);
111
+ current = {
112
+ start: req.offset,
113
+ end: req.offset + req.length,
114
+ requests: [req],
115
+ };
116
+ }
112
117
  }
113
- try {
114
- // Split requests into smaller groups
115
- for (let i = 0; i < currentBatch.length; i += this.maxRangesPerRequest) {
116
- const batchRequests = currentBatch.slice(i, i + this.maxRangesPerRequest);
117
- const buffer = await this.loadController.loadBinaryData(batchRequests);
118
-
119
- let currentOffset = 0;
120
- batchRequests.forEach((request) => {
121
- const view = this.createTypedArray(buffer, currentOffset, request.length, request.componentType);
122
- request.resolve(view);
123
- currentOffset += request.length;
118
+ mergedRanges.push(current);
119
+ const finalRanges = [];
120
+ for (const range of mergedRanges) {
121
+ let { start, end, requests } = range;
122
+ while (end - start > MAX_CHUNK) {
123
+ let splitIdx = 0;
124
+ for (let i = 0; i < requests.length; i++) {
125
+ if (requests[i].offset + requests[i].length - start > MAX_CHUNK) {
126
+ break;
127
+ }
128
+ splitIdx = i;
129
+ }
130
+ const chunkRequests = requests.slice(0, splitIdx + 1);
131
+ const chunkEnd =
132
+ chunkRequests[chunkRequests.length - 1].offset + chunkRequests[chunkRequests.length - 1].length;
133
+ finalRanges.push({
134
+ start,
135
+ end: chunkEnd,
136
+ requests: chunkRequests,
124
137
  });
138
+ requests = requests.slice(splitIdx + 1);
139
+ if (requests.length > 0) {
140
+ start = requests[0].offset;
141
+ end = requests[0].offset + requests[0].length;
142
+ for (let i = 1; i < requests.length; i++) {
143
+ end = Math.max(end, requests[i].offset + requests[i].length);
144
+ }
145
+ }
146
+ }
147
+ if (requests.length > 0) {
148
+ finalRanges.push({ start, end, requests });
125
149
  }
126
- } catch (error) {
127
- console.error("Error processing batch:", error);
128
- currentBatch.forEach((request) => request.reject(error));
129
150
  }
130
-
131
- if (this.pendingRequests.length > 0) {
132
- this.batchTimeout = setTimeout(() => this.processBatch(), this.batchDelay);
151
+ /*
152
+ for (const range of finalRanges) {
153
+ const length = range.end - range.start;
154
+ const buffer = await this.loadController.loadBinaryData([
155
+ { offset: range.start, length: length }
156
+ ]);
157
+ for (const req of range.requests) {
158
+ const relOffset = req.offset - range.start;
159
+ try {
160
+ req._resolve({ buffer, relOffset, length: req.length });
161
+ } catch (e) {
162
+ req._reject(e);
163
+ }
164
+ }
133
165
  }
166
+ */
167
+
168
+ const promises = finalRanges.map(async (range) => {
169
+ const length = range.end - range.start;
170
+ const buffer = await this.loadController.loadBinaryData([{ offset: range.start, length }]);
171
+ for (const req of range.requests) {
172
+ const relOffset = req.offset - range.start;
173
+ try {
174
+ req._resolve({ buffer, relOffset, length: req.length });
175
+ } catch (e) {
176
+ req._reject(e);
177
+ }
178
+ }
179
+ });
180
+ await Promise.all(promises);
181
+
182
+ this.pendingRequests = [];
134
183
  }
135
184
 
136
185
  getBufferView(byteOffset, byteLength, componentType) {
@@ -143,12 +192,10 @@ export class GltfStructure {
143
192
 
144
193
  createTypedArray(buffer, offset, length, componentType) {
145
194
  try {
146
- // Validate parameters
147
195
  if (!buffer || !(buffer instanceof ArrayBuffer)) {
148
196
  throw new Error("Invalid buffer");
149
197
  }
150
198
 
151
- // Calculate element size for given type
152
199
  let elementSize;
153
200
  switch (componentType) {
154
201
  case 5120:
@@ -167,18 +214,15 @@ export class GltfStructure {
167
214
  throw new Error(`Unsupported component type: ${componentType}`);
168
215
  }
169
216
 
170
- // Check if requested length is correct
171
217
  const numElements = length / elementSize;
172
218
  if (!Number.isInteger(numElements)) {
173
219
  throw new Error(`Invalid length ${length} for component type ${componentType}`);
174
220
  }
175
221
 
176
- // Check if buffer is large enough
177
222
  if (length > buffer.byteLength) {
178
223
  throw new Error(`Buffer too small: need ${length} bytes, but buffer is ${buffer.byteLength} bytes`);
179
224
  }
180
225
 
181
- // Create appropriate typed array
182
226
  const ArrayType = GL_COMPONENT_TYPES[componentType];
183
227
  return new ArrayType(buffer, offset, numElements);
184
228
  } catch (error) {
@@ -275,7 +319,6 @@ export class GltfStructure {
275
319
  const image = this.json.images[imageIndex];
276
320
 
277
321
  if (image.uri) {
278
- // Handle base64 or URL
279
322
  if (image.uri.startsWith("data:")) {
280
323
  return await this.textureLoader.loadAsync(image.uri);
281
324
  } else {
@@ -283,23 +326,17 @@ export class GltfStructure {
283
326
  return await this.textureLoader.loadAsync(fullUrl);
284
327
  }
285
328
  } else if (image.bufferView !== undefined) {
286
- // Handle embedded binary data
287
329
  const bufferView = this.json.bufferViews[image.bufferView];
288
- const array = await this.getBufferView(
289
- bufferView.byteOffset || 0,
290
- bufferView.byteLength,
291
- 5121 // UNSIGNED_BYTE
292
- );
330
+ const array = await this.getBufferView(bufferView.byteOffset || 0, bufferView.byteLength, 5121);
293
331
  const blob = new Blob([array], { type: image.mimeType });
294
332
  const url = URL.createObjectURL(blob);
295
333
  const texture = await this.textureLoader.loadAsync(url);
296
334
  URL.revokeObjectURL(url);
297
- texture.flipY = false; // GLTF standard
335
+ texture.flipY = false;
298
336
  return texture;
299
337
  }
300
338
  };
301
339
 
302
- // Load all textures
303
340
  const texturePromises = [];
304
341
  for (let i = 0; i < this.json.textures.length; i++) {
305
342
  texturePromises.push(
@@ -309,82 +346,56 @@ export class GltfStructure {
309
346
  await Promise.all(texturePromises);
310
347
  }
311
348
 
312
- loadMaterials() {
349
+ async loadMaterials() {
313
350
  if (!this.json.materials) return this.materials;
314
351
 
315
352
  for (let i = 0; i < this.json.materials.length; i++) {
316
353
  const materialDef = this.json.materials[i];
317
- const material = this.createMaterial(materialDef);
354
+ const material = await this.createMaterial(materialDef);
355
+ material.name = materialDef.name;
318
356
  this.materials.set(i, material);
319
357
  }
320
358
  return this.materials;
321
359
  }
322
360
 
323
361
  createMaterial(materialDef) {
324
- const material = new MeshStandardMaterial();
362
+ const params = {};
325
363
 
326
- // Base color
327
364
  if (materialDef.pbrMetallicRoughness) {
328
365
  const pbr = materialDef.pbrMetallicRoughness;
329
-
330
366
  if (pbr.baseColorFactor) {
331
- material.color.fromArray(pbr.baseColorFactor);
332
- material.opacity = pbr.baseColorFactor[3];
367
+ params.color = new Color().fromArray(pbr.baseColorFactor);
368
+ params.opacity = pbr.baseColorFactor[3];
369
+ if (params.opacity < 1.0) params.transparent = true;
333
370
  }
334
-
335
371
  if (pbr.baseColorTexture) {
336
- material.map = this.textureCache.get(pbr.baseColorTexture.index);
337
- }
338
-
339
- // Metallic and roughness
340
- if (pbr.metallicFactor !== undefined) {
341
- material.metalness = pbr.metallicFactor;
342
- }
343
- if (pbr.roughnessFactor !== undefined) {
344
- material.roughness = pbr.roughnessFactor;
345
- }
346
- if (pbr.metallicRoughnessTexture) {
347
- material.metalnessMap = this.textureCache.get(pbr.metallicRoughnessTexture.index);
348
- material.roughnessMap = material.metalnessMap;
372
+ params.map = this.textureCache.get(pbr.baseColorTexture.index);
349
373
  }
350
374
  }
351
375
 
352
- // Normal map
353
- if (materialDef.normalTexture) {
354
- material.normalMap = this.textureCache.get(materialDef.normalTexture.index);
355
- if (materialDef.normalTexture.scale !== undefined) {
356
- material.normalScale.set(materialDef.normalTexture.scale, materialDef.normalTexture.scale);
357
- }
358
- }
376
+ params.specular = 0x222222;
377
+ params.shininess = 10;
378
+ params.reflectivity = 0.05;
379
+ params.polygonOffset = true;
380
+ params.polygonOffsetFactor = 1;
381
+ params.polygonOffsetUnits = 1;
359
382
 
360
- // Emissive
361
383
  if (materialDef.emissiveFactor) {
362
- material.emissive.fromArray(materialDef.emissiveFactor);
363
- }
364
- if (materialDef.emissiveTexture) {
365
- material.emissiveMap = this.textureCache.get(materialDef.emissiveTexture.index);
384
+ params.emissive = new Color().fromArray(materialDef.emissiveFactor);
366
385
  }
367
386
 
368
- // Occlusion
369
- if (materialDef.occlusionTexture) {
370
- material.aoMap = this.textureCache.get(materialDef.occlusionTexture.index);
371
- if (materialDef.occlusionTexture.strength !== undefined) {
372
- material.aoMapIntensity = materialDef.occlusionTexture.strength;
373
- }
387
+ if (materialDef.normalTexture) {
388
+ params.normalMap = this.textureCache.get(materialDef.normalTexture.index);
374
389
  }
375
390
 
376
- // Alpha mode
377
391
  if (materialDef.alphaMode === "BLEND") {
378
- material.transparent = true;
379
- } else if (materialDef.alphaMode === "MASK") {
380
- material.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5;
392
+ params.transparent = true;
381
393
  }
382
394
 
383
- // Double sided
384
- material.side = materialDef.doubleSided ? DoubleSide : FrontSide;
385
-
386
- material.name = materialDef.name;
387
-
395
+ if (materialDef.doubleSided) {
396
+ params.side = DoubleSide;
397
+ }
398
+ const material = new MeshPhongMaterial(params);
388
399
  return material;
389
400
  }
390
401
 
@@ -417,12 +428,8 @@ export class GltfStructure {
417
428
  if (!meshDef || !meshDef.primitives) return 0;
418
429
 
419
430
  let totalSize = 0;
420
-
421
- // Estimate size for each primitive
422
431
  for (const primitive of meshDef.primitives) {
423
- // Check for attributes
424
432
  if (primitive.attributes) {
425
- // Calculate attributes size
426
433
  for (const [, accessorIndex] of Object.entries(primitive.attributes)) {
427
434
  if (accessorIndex === undefined) continue;
428
435
 
@@ -435,7 +442,6 @@ export class GltfStructure {
435
442
  }
436
443
  }
437
444
 
438
- // Calculate indices size if present
439
445
  if (primitive.indices !== undefined) {
440
446
  const accessor = this.json.accessors[primitive.indices];
441
447
  if (accessor) {
@@ -31,13 +31,11 @@ import { GltfStructure } from "./DynamicGltfLoader/GltfStructure.js";
31
31
 
32
32
  export class GLTFCloudDynamicLoader implements ILoader {
33
33
  public viewer: Viewer;
34
- public scene: Group;
35
34
  public gltfLoader: DynamicGltfLoader;
36
35
  public requestId = 0;
37
36
 
38
37
  constructor(viewer: Viewer) {
39
38
  this.viewer = viewer;
40
- this.scene = new Group();
41
39
  }
42
40
 
43
41
  dispose() {
@@ -55,23 +53,25 @@ export class GLTFCloudDynamicLoader implements ILoader {
55
53
  }
56
54
 
57
55
  async load(model: any, format?: string, params?: LoadParams): Promise<this> {
58
- this.gltfLoader = new DynamicGltfLoader(this.viewer.camera, this.viewer.scene, this.viewer.renderer);
56
+ const scene = new Group();
57
+
58
+ this.gltfLoader = new DynamicGltfLoader(this.viewer.camera, scene, this.viewer.renderer);
59
59
  this.gltfLoader.memoryLimit = this.viewer.options.memoryLimit;
60
60
 
61
61
  this.gltfLoader.addEventListener("databasechunk", (data) => {
62
- const modelImpl = new DynamicModelImpl(this.scene);
62
+ const modelImpl = new DynamicModelImpl(scene);
63
63
  modelImpl.loader = this;
64
64
  modelImpl.gltfLoader = this.gltfLoader;
65
65
  modelImpl.viewer = this.viewer;
66
66
 
67
- this.viewer.scene.add(this.scene);
67
+ this.viewer.scene.add(scene);
68
68
  this.viewer.models.push(modelImpl);
69
69
 
70
70
  this.viewer.syncOptions();
71
71
  this.viewer.syncOverlay();
72
72
  this.viewer.update();
73
73
 
74
- this.viewer.emitEvent({ type: "databasechunk", data, file: model.file, model });
74
+ this.viewer.emitEvent({ type: "databasechunk", data: scene, file: model.file, model });
75
75
  });
76
76
 
77
77
  this.gltfLoader.addEventListener("geometryprogress", (data) => {
@@ -79,10 +79,6 @@ export class GLTFCloudDynamicLoader implements ILoader {
79
79
  this.viewer.emitEvent({ type: "geometryprogress", data: progress, file: model.file, model });
80
80
  });
81
81
 
82
- this.gltfLoader.addEventListener("geometrymemory", (data) => {
83
- this.viewer.emit({ type: "geometryprogress", data });
84
- });
85
-
86
82
  this.gltfLoader.addEventListener("geometryerror", (data) => {
87
83
  this.viewer.emitEvent({ type: "geometryerror", data, file: model.file, model });
88
84
  });
@@ -138,8 +134,6 @@ export class GLTFCloudDynamicLoader implements ILoader {
138
134
  }
139
135
 
140
136
  cancel() {
141
- if (this.gltfLoader) {
142
- this.gltfLoader.abortLoading();
143
- }
137
+ if (this.gltfLoader) this.gltfLoader.abortLoading();
144
138
  }
145
139
  }
@@ -38,7 +38,7 @@ import { GLTFCloudDynamicLoader } from "./GLTFCloudDynamicLoader";
38
38
  *
39
39
  * The loader should do:
40
40
  *
41
- * - Load scene from file. The scene must be a Three.js object of type `Object3D` or a descendant of it.
41
+ * - Load raw data from file and convert it to the `Three.js` scene.
42
42
  * - Add scene to the viewer `scene`.
43
43
  * - Create `ModelImpl` for the scene and to the viewer `models` list.
44
44
  * - Synchronize viewer options and overlay.