@inweb/viewer-three 26.10.6 → 26.11.1

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 (84) hide show
  1. package/dist/plugins/components/AxesHelperComponent.js +23 -1
  2. package/dist/plugins/components/AxesHelperComponent.js.map +1 -1
  3. package/dist/plugins/components/AxesHelperComponent.min.js +1 -1
  4. package/dist/plugins/components/AxesHelperComponent.module.js +24 -2
  5. package/dist/plugins/components/AxesHelperComponent.module.js.map +1 -1
  6. package/dist/plugins/components/ExtentsHelperComponent.js +18 -0
  7. package/dist/plugins/components/ExtentsHelperComponent.js.map +1 -1
  8. package/dist/plugins/components/ExtentsHelperComponent.min.js +1 -1
  9. package/dist/plugins/components/ExtentsHelperComponent.module.js +19 -1
  10. package/dist/plugins/components/ExtentsHelperComponent.module.js.map +1 -1
  11. package/dist/plugins/loaders/GLTFCloudLoader.js +2 -3
  12. package/dist/plugins/loaders/GLTFCloudLoader.js.map +1 -1
  13. package/dist/plugins/loaders/GLTFCloudLoader.min.js +1 -1
  14. package/dist/plugins/loaders/GLTFCloudLoader.module.js +2 -3
  15. package/dist/plugins/loaders/GLTFCloudLoader.module.js.map +1 -1
  16. package/dist/plugins/loaders/GLTFFileLoader.js +2499 -0
  17. package/dist/plugins/loaders/GLTFFileLoader.js.map +1 -0
  18. package/dist/plugins/loaders/GLTFFileLoader.min.js +24 -0
  19. package/dist/plugins/loaders/GLTFFileLoader.module.js +74 -0
  20. package/dist/plugins/loaders/GLTFFileLoader.module.js.map +1 -0
  21. package/dist/plugins/loaders/IFCXLoader.js +5 -7
  22. package/dist/plugins/loaders/IFCXLoader.js.map +1 -1
  23. package/dist/plugins/loaders/IFCXLoader.min.js +1 -1
  24. package/dist/plugins/loaders/IFCXLoader.module.js +5 -7
  25. package/dist/plugins/loaders/IFCXLoader.module.js.map +1 -1
  26. package/dist/plugins/loaders/PotreeLoader.js +1 -2
  27. package/dist/plugins/loaders/PotreeLoader.js.map +1 -1
  28. package/dist/plugins/loaders/PotreeLoader.min.js +1 -1
  29. package/dist/plugins/loaders/PotreeLoader.module.js +1 -2
  30. package/dist/plugins/loaders/PotreeLoader.module.js.map +1 -1
  31. package/dist/viewer-three.js +770 -2777
  32. package/dist/viewer-three.js.map +1 -1
  33. package/dist/viewer-three.min.js +3 -3
  34. package/dist/viewer-three.module.js +609 -206
  35. package/dist/viewer-three.module.js.map +1 -1
  36. package/lib/Viewer/Viewer.d.ts +27 -20
  37. package/lib/Viewer/commands/GetSelected2.d.ts +2 -0
  38. package/lib/Viewer/commands/SelectModel.d.ts +1 -1
  39. package/lib/Viewer/commands/SetSelected2.d.ts +2 -0
  40. package/lib/Viewer/components/index.d.ts +6 -6
  41. package/lib/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.d.ts +0 -1
  42. package/lib/Viewer/loaders/GLTFBinaryExtension.d.ts +5 -0
  43. package/lib/Viewer/loaders/GLTFCloudDynamicLoader.d.ts +2 -2
  44. package/lib/Viewer/loaders/{GLTFFileLoader.d.ts → GLTFFileDynamicLoader.d.ts} +7 -1
  45. package/lib/Viewer/loaders/GLTFLoadingManager.d.ts +4 -3
  46. package/lib/Viewer/loaders/RangesLoader.d.ts +15 -0
  47. package/lib/Viewer/loaders/index.d.ts +22 -14
  48. package/lib/Viewer/models/IModelImpl.d.ts +6 -8
  49. package/lib/Viewer/models/ModelImpl.d.ts +3 -5
  50. package/package.json +5 -5
  51. package/plugins/components/AxesHelperComponent.ts +31 -2
  52. package/plugins/components/ExtentsHelperComponent.ts +25 -0
  53. package/plugins/loaders/GLTFCloudLoader.ts +2 -3
  54. package/{src/Viewer → plugins}/loaders/GLTFFileLoader.ts +21 -12
  55. package/plugins/loaders/IFCX/IFCXCloudLoader.ts +5 -5
  56. package/plugins/loaders/IFCX/IFCXFileLoader.ts +3 -4
  57. package/plugins/loaders/Potree/PotreeFileLoader.ts +3 -4
  58. package/src/Viewer/Viewer.ts +120 -88
  59. package/src/Viewer/commands/ClearSelected.ts +3 -1
  60. package/src/Viewer/commands/GetModels.ts +1 -1
  61. package/src/Viewer/commands/GetSelected.ts +2 -2
  62. package/src/Viewer/commands/GetSelected2.ts +34 -0
  63. package/src/Viewer/commands/HideSelected.ts +3 -1
  64. package/src/Viewer/commands/SelectModel.ts +5 -5
  65. package/src/Viewer/commands/SetSelected.ts +9 -10
  66. package/src/Viewer/commands/SetSelected2.ts +42 -0
  67. package/src/Viewer/commands/ZoomToObjects.ts +5 -6
  68. package/src/Viewer/commands/ZoomToSelected.ts +3 -1
  69. package/src/Viewer/commands/index.ts +4 -0
  70. package/src/Viewer/components/CameraComponent.ts +6 -1
  71. package/src/Viewer/components/ExtentsComponent.ts +4 -1
  72. package/src/Viewer/components/SelectionComponent.ts +2 -0
  73. package/src/Viewer/components/index.ts +6 -6
  74. package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +253 -22
  75. package/src/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.ts +20 -10
  76. package/src/Viewer/loaders/DynamicGltfLoader/GltfStructure.js +3 -1
  77. package/src/Viewer/loaders/GLTFBinaryExtension.ts +91 -0
  78. package/src/Viewer/loaders/GLTFCloudDynamicLoader.ts +13 -19
  79. package/src/Viewer/loaders/GLTFFileDynamicLoader.ts +145 -0
  80. package/src/Viewer/loaders/GLTFLoadingManager.ts +5 -4
  81. package/src/Viewer/loaders/RangesLoader.ts +95 -0
  82. package/src/Viewer/loaders/index.ts +24 -16
  83. package/src/Viewer/models/IModelImpl.ts +8 -9
  84. package/src/Viewer/models/ModelImpl.ts +30 -17
@@ -26,13 +26,12 @@ import type { Viewer } from "../Viewer";
26
26
  import { zoomTo } from "./ZoomTo";
27
27
 
28
28
  export function zoomToObjects(viewer: Viewer, handles: string[] = []): void {
29
- const handleSet = new Set(handles);
30
- const objects = [];
31
- viewer.scene.traverseVisible((child) => {
32
- if (handleSet.has(child.userData?.handle)) objects.push(child);
33
- });
29
+ const extents = new Box3();
34
30
 
35
- const extents = objects.reduce((result: Box3, object) => result.expandByObject(object), new Box3());
31
+ viewer.models.forEach((model) => {
32
+ const objects = model.getObjectsByHandles(handles);
33
+ objects.forEach((object) => extents.expandByObject(object));
34
+ });
36
35
  if (extents.isEmpty()) extents.copy(viewer.extents);
37
36
 
38
37
  zoomTo(viewer, extents);
@@ -26,7 +26,9 @@ import type { Viewer } from "../Viewer";
26
26
  import { zoomTo } from "./ZoomTo";
27
27
 
28
28
  export function zoomToSelected(viewer: Viewer): void {
29
- const extents = viewer.selected.reduce((result: Box3, object) => result.expandByObject(object), new Box3());
29
+ const extents = new Box3();
30
+
31
+ viewer.selected.forEach((object) => extents.expandByObject(object));
30
32
  if (extents.isEmpty()) extents.copy(viewer.extents);
31
33
 
32
34
  zoomTo(viewer, extents);
@@ -32,6 +32,7 @@ import { explode, collect } from "./Explode";
32
32
  import { getDefaultViewPositions } from "./GetDefaultViewPositions";
33
33
  import { getModels } from "./GetModels";
34
34
  import { getSelected } from "./GetSelected";
35
+ import { getSelected2 } from "./GetSelected2";
35
36
  import { hideSelected } from "./HideSelected";
36
37
  import { isolateSelected } from "./IsolateSelected";
37
38
  import { regenerateAll } from "./RegenerateAll";
@@ -41,6 +42,7 @@ import { setActiveDragger } from "./SetActiveDragger";
41
42
  import { setDefaultViewPosition } from "./SetDefaultViewPosition";
42
43
  import { setMarkupColor } from "./SetMarkupColor";
43
44
  import { setSelected } from "./SetSelected";
45
+ import { setSelected2 } from "./SetSelected2";
44
46
  import { showAll } from "./ShowAll";
45
47
  import { zoomToExtents } from "./ZoomToExtents";
46
48
  import { zoomToObjects } from "./ZoomToObjects";
@@ -86,6 +88,7 @@ commands.registerCommand("explode", explode);
86
88
  commands.registerCommand("getDefaultViewPositions", getDefaultViewPositions);
87
89
  commands.registerCommand("getModels", getModels);
88
90
  commands.registerCommand("getSelected", getSelected);
91
+ commands.registerCommand("getSelected2", getSelected2);
89
92
  commands.registerCommand("hideSelected", hideSelected);
90
93
  commands.registerCommand("isolateSelected", isolateSelected);
91
94
  commands.registerCommand("regenerateAll", regenerateAll);
@@ -95,6 +98,7 @@ commands.registerCommand("setActiveDragger", setActiveDragger);
95
98
  commands.registerCommand("setDefaultViewPosition", setDefaultViewPosition);
96
99
  commands.registerCommand("setMarkupColor", setMarkupColor);
97
100
  commands.registerCommand("setSelected", setSelected);
101
+ commands.registerCommand("setSelected2", setSelected2);
98
102
  commands.registerCommand("showAll", showAll);
99
103
  commands.registerCommand("zoomToExtents", zoomToExtents);
100
104
  commands.registerCommand("zoomToObjects", zoomToObjects);
@@ -119,9 +119,14 @@ export class CameraComponent implements IComponent {
119
119
  };
120
120
 
121
121
  geometryEnd = () => {
122
+ // do not change the camera after opening the second file in assembly
123
+ if (this.viewer.models.length > 1) {
124
+ this.switchCamera(this.viewer.camera);
125
+ return;
126
+ }
127
+
122
128
  let camera: any;
123
129
 
124
- // TODO: do not change the camera and target after opening the second model in "append" mode
125
130
  this.viewer.scene.traverse((object: any) => {
126
131
  if (object.isCamera)
127
132
  if (!camera) camera = object;
@@ -53,8 +53,11 @@ export class ExtentsComponent implements IComponent {
53
53
  syncExtents = () => {
54
54
  const extents = new Box3();
55
55
  this.viewer.models.forEach((model) => model.getExtents(extents));
56
-
57
56
  this.viewer.extents.copy(extents);
57
+
58
+ // do not change target after opening the second file in assembly
59
+ if (this.viewer.models.length > 1) return;
60
+
58
61
  this.viewer.extents.getCenter(this.viewer.target);
59
62
  };
60
63
  }
@@ -84,7 +84,9 @@ export class SelectionComponent implements IComponent {
84
84
  }
85
85
 
86
86
  this.viewer.update();
87
+
87
88
  this.viewer.emitEvent({ type: "select", data: undefined, handles: this.viewer.getSelected() });
89
+ this.viewer.emitEvent({ type: "select2", data: undefined, handles: this.viewer.getSelected2() });
88
90
  };
89
91
 
90
92
  onDoubleClick = (event: MouseEvent) => {
@@ -41,8 +41,8 @@ import { ResetComponent } from "./ResetComponent";
41
41
  *
42
42
  * 1. Define a component class implements {@link IComponent}.
43
43
  * 2. Define a constructor with a `viewer` parameter and add mouse event listeners for the specified viewer.
44
- * 3. Define the component logic in the event listeners. For example, listen for the `mousedown` event and
45
- * select objects when the left mouse button is pressed.
44
+ * 3. Define the component logic in the event listeners. For example, listen for the `geometryend` event and
45
+ * implement post-processing logic for the model.
46
46
  * 4. Override {@link IComponent.dispose} and remove mouse event listeners from the viewer.
47
47
  * 5. Register component provider in the components registry by calling the
48
48
  * {@link components.registerComponent}.
@@ -57,15 +57,15 @@ import { ResetComponent } from "./ResetComponent";
57
57
  *
58
58
  * constructor(viewer: Viewer) {
59
59
  * this.viewer = viewer;
60
- * this.viewer.addEventListener("mousedown", this.onMouseDown);
60
+ * this.viewer.addEventListener("geometryend", this.onGeometryEnd);
61
61
  * }
62
62
  *
63
63
  * override dispose() {
64
- * this.viewer.removeEventListener("mousedown", this.onMouseDown);
64
+ * this.viewer.removeEventListener("geometryend", this.onGeometryEnd);
65
65
  * }
66
66
  *
67
- * onMouseDown = (event: PointerEvent) => {
68
- * // place custom logic here
67
+ * onGeometryEnd = (event: MouseEvent) => {
68
+ * this.viewer.executeCommand("zoomToExtents");
69
69
  * };
70
70
  * }
71
71
  *
@@ -10,6 +10,7 @@ import {
10
10
  LineLoop,
11
11
  Group,
12
12
  Vector3,
13
+ Vector2,
13
14
  Quaternion,
14
15
  Matrix4,
15
16
  Box3,
@@ -26,6 +27,8 @@ import {
26
27
  import { GL_CONSTANTS } from "./GltfStructure.js";
27
28
  import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
28
29
 
30
+ const STRUCTURE_ID_SEPARATOR = ":";
31
+
29
32
  //#AI-GENERATED using Gemini 2.5 Pro, Claude-4-sonnet
30
33
  //#Reviewed and adapted by dborysov@opendesign.com
31
34
 
@@ -42,6 +45,7 @@ export class DynamicGltfLoader {
42
45
  geometryerror: [],
43
46
  update: [],
44
47
  geometrymemory: [],
48
+ optimizationprogress: [],
45
49
  };
46
50
 
47
51
  this.loadDistance = 100;
@@ -98,7 +102,6 @@ export class DynamicGltfLoader {
98
102
  this.newOptimizedObjects = new Set();
99
103
  this.oldOptimizeObjects = new Set();
100
104
 
101
- this.maxConcurrentChunks = 8;
102
105
  this.activeChunkLoads = 0;
103
106
  this.chunkQueue = [];
104
107
 
@@ -113,6 +116,8 @@ export class DynamicGltfLoader {
113
116
  // Merged geometry tracking
114
117
  this.mergedObjectMap = new Map(); // objectId -> {mergedObject, startIndex, endIndex, vertexCount}
115
118
  this.mergedGeometryVisibility = new Map(); // mergedObject -> visibility array
119
+
120
+ this._webglInfoCache = null; // { renderer, vendor }
116
121
  }
117
122
 
118
123
  setVisibleEdges(visible) {
@@ -244,6 +249,136 @@ export class DynamicGltfLoader {
244
249
  console.log(`Final memory usage: ${Math.round(currentMemoryUsage / (1024 * 1024))}MB`);
245
250
  }
246
251
 
252
+ getStats() {
253
+ let totalObjects = 0;
254
+ let renderedObjects = 0;
255
+ let totalTriangles = 0;
256
+ let renderedTriangles = 0;
257
+ let totalLines = 0;
258
+ let renderedLines = 0;
259
+ let totalEdges = 0;
260
+ let renderedEdges = 0;
261
+
262
+ this.scene.traverse((object) => {
263
+ totalObjects++;
264
+
265
+ const geometry = object.geometry;
266
+ if (!geometry) return;
267
+
268
+ let triCount = 0;
269
+ if (geometry.index) {
270
+ triCount = Math.floor(geometry.index.count / 3);
271
+ } else if (geometry.attributes && geometry.attributes.position) {
272
+ triCount = Math.floor(geometry.attributes.position.count / 3);
273
+ }
274
+ totalTriangles += triCount;
275
+
276
+ let lineCount = 0;
277
+ if (geometry.index) {
278
+ lineCount = Math.floor(geometry.index.count / 2);
279
+ } else if (geometry.attributes && geometry.attributes.position) {
280
+ lineCount = Math.floor(geometry.attributes.position.count / 2);
281
+ }
282
+
283
+ if (object.type === "Line" || object.type === "LineSegments" || object.type === "LineLoop") {
284
+ if (object.userData.isEdge) {
285
+ totalEdges += lineCount;
286
+ } else {
287
+ totalLines += lineCount;
288
+ }
289
+ }
290
+
291
+ if (object.visible !== false) {
292
+ if (object.isMesh || object.isLine || object.isPoints) {
293
+ renderedObjects++;
294
+ if (object.isMesh) {
295
+ renderedTriangles += triCount;
296
+ } else if (object.type === "Line" || object.type === "LineSegments" || object.type === "LineLoop") {
297
+ if (object.userData.isEdge) {
298
+ renderedEdges += lineCount;
299
+ } else {
300
+ renderedLines += lineCount;
301
+ }
302
+ }
303
+ }
304
+ }
305
+ });
306
+
307
+ const geometryCount = this.geometryCache ? this.geometryCache.size : 0;
308
+ const geometryMemoryBytes = Array.from(this.geometryCache?.values?.() || []).reduce((a, b) => a + b, 0);
309
+
310
+ const uniqueMaterialIds = new Set();
311
+ const uniqueTextureIds = new Set();
312
+ if (Array.isArray(this.structures)) {
313
+ for (const structure of this.structures) {
314
+ console.log(structure.materialCache.values());
315
+ try {
316
+ for (const entry of structure.materialCache.values()) {
317
+ if (entry?.mesh?.uuid) uniqueMaterialIds.add(entry.mesh.uuid);
318
+ if (entry?.points?.uuid) uniqueMaterialIds.add(entry.points.uuid);
319
+ if (entry?.lines?.uuid) uniqueMaterialIds.add(entry.lines.uuid);
320
+ }
321
+ } catch (exp) {
322
+ console.error("Error adding material to uniqueMaterialIds", exp);
323
+ }
324
+ }
325
+ }
326
+ const materialCount = uniqueMaterialIds.size;
327
+ const textureCount = uniqueTextureIds.size;
328
+ const estimatedGpuMemoryBytes = geometryMemoryBytes;
329
+
330
+ if (!this._webglInfoCache) {
331
+ try {
332
+ const gl = this.renderer.getContext();
333
+ const dbgInfo = gl.getExtension("WEBGL_debug_renderer_info");
334
+ if (dbgInfo) {
335
+ const rendererStr = gl.getParameter(dbgInfo.UNMASKED_RENDERER_WEBGL);
336
+ const vendorStr = gl.getParameter(dbgInfo.UNMASKED_VENDOR_WEBGL);
337
+ this._webglInfoCache = { renderer: rendererStr, vendor: vendorStr };
338
+ } else {
339
+ this._webglInfoCache = { renderer: null, vendor: null };
340
+ }
341
+ } catch (e) {
342
+ console.error("Error getting webgl info", e);
343
+ this._webglInfoCache = { renderer: null, vendor: null };
344
+ }
345
+ }
346
+
347
+ const size = new Vector2();
348
+ if (this.renderer && this.renderer.getSize) {
349
+ this.renderer.getSize(size);
350
+ }
351
+
352
+ return {
353
+ scene: {
354
+ beforeOptimization: {
355
+ objects: totalObjects - renderedObjects,
356
+ triangles: totalTriangles - renderedTriangles,
357
+ lines: totalLines - renderedLines,
358
+ edges: totalEdges - renderedEdges,
359
+ },
360
+
361
+ afterOptimization: {
362
+ objects: renderedObjects,
363
+ triangles: renderedTriangles,
364
+ lines: renderedLines,
365
+ edges: renderedEdges,
366
+ },
367
+ },
368
+ memory: {
369
+ geometries: { count: geometryCount, bytes: geometryMemoryBytes },
370
+ textures: { count: textureCount },
371
+ materials: { count: materialCount },
372
+ totalEstimatedGpuBytes: estimatedGpuMemoryBytes,
373
+ },
374
+ system: {
375
+ webglRenderer: this._webglInfoCache?.renderer || "",
376
+ webglVendor: this._webglInfoCache?.vendor || "",
377
+ viewport: { width: size.x || 0, height: size.y || 0 },
378
+ },
379
+ };
380
+ }
381
+
247
382
  async loadNode(nodeId, onLoadFinishCb) {
248
383
  const node = this.nodes.get(nodeId);
249
384
  if (!node || node.loaded || node.loading) return;
@@ -431,7 +566,7 @@ export class DynamicGltfLoader {
431
566
  if (node.handle) {
432
567
  mesh.userData.handle = node.handle;
433
568
  } else {
434
- mesh.userData.handle = `${node.structure.id}_${mesh.userData.handle}`;
569
+ mesh.userData.handle = `${node.structure.id}${STRUCTURE_ID_SEPARATOR}${mesh.userData.handle}`;
435
570
  }
436
571
  if (mesh.material.name === "edges") {
437
572
  mesh.userData.isEdge = true;
@@ -607,7 +742,7 @@ export class DynamicGltfLoader {
607
742
 
608
743
  let handle = null;
609
744
  if (nodeDef.extras?.handle) {
610
- handle = `${structure.id}_${nodeDef.extras.handle}`;
745
+ handle = `${structure.id}${STRUCTURE_ID_SEPARATOR}${nodeDef.extras.handle}`;
611
746
  }
612
747
 
613
748
  if (nodeDef.camera !== undefined) {
@@ -628,7 +763,7 @@ export class DynamicGltfLoader {
628
763
  if (nodeDef.extras) {
629
764
  nodeGroup.userData = { ...nodeDef.extras };
630
765
  if (nodeGroup.userData.handle) {
631
- nodeGroup.userData.handle = `${structure.id}_${nodeGroup.userData.handle}`;
766
+ nodeGroup.userData.handle = `${structure.id}${STRUCTURE_ID_SEPARATOR}${nodeGroup.userData.handle}`;
632
767
  }
633
768
  }
634
769
 
@@ -680,7 +815,7 @@ export class DynamicGltfLoader {
680
815
  }
681
816
 
682
817
  if (meshDef.extras && meshDef.extras.handle) {
683
- handle = `${structure.id}_${meshDef.extras.handle}`;
818
+ handle = `${structure.id}${STRUCTURE_ID_SEPARATOR}${meshDef.extras.handle}`;
684
819
  }
685
820
 
686
821
  this.nodes.set(uniqueNodeId, {
@@ -785,13 +920,13 @@ export class DynamicGltfLoader {
785
920
  }
786
921
 
787
922
  async loadNodes() {
788
- console.time("process nodes");
923
+ console.time("Process nodes");
789
924
  await this.processNodes();
790
- console.timeEnd("process nodes");
925
+ console.timeEnd("Process nodes");
791
926
 
792
- console.time("optimize scene");
927
+ console.time("Optimize scene");
793
928
  await this.optimizeScene();
794
- console.timeEnd("optimize scene");
929
+ console.timeEnd("Optimize scene");
795
930
  }
796
931
 
797
932
  cleanupPartialLoad() {
@@ -955,7 +1090,7 @@ export class DynamicGltfLoader {
955
1090
  }
956
1091
 
957
1092
  createVisibilityMaterial(material) {
958
- // Применяем шейдер напрямую к оригинальному материалу
1093
+ // Apply shader directly to the original material
959
1094
  material.onBeforeCompile = (shader) => {
960
1095
  shader.vertexShader = shader.vertexShader.replace(
961
1096
  "#include <common>",
@@ -991,7 +1126,7 @@ export class DynamicGltfLoader {
991
1126
  );
992
1127
  };
993
1128
 
994
- // Принудительная перекомпиляция материала
1129
+ // Force recompilation of material
995
1130
  material.needsUpdate = true;
996
1131
 
997
1132
  return material;
@@ -1162,7 +1297,7 @@ export class DynamicGltfLoader {
1162
1297
  }
1163
1298
  this.handleToObjects.get(fullHandle).add(object);
1164
1299
 
1165
- object.userData.structureId = object.userData.handle.split("_")[0];
1300
+ object.userData.structureId = object.userData.handle.split(STRUCTURE_ID_SEPARATOR)[0];
1166
1301
  }
1167
1302
 
1168
1303
  getObjectsByHandle(handle) {
@@ -1237,11 +1372,32 @@ export class DynamicGltfLoader {
1237
1372
  this.originalObjects.add(object);
1238
1373
  }
1239
1374
 
1240
- optimizeScene() {
1375
+ yieldToUI() {
1376
+ return new Promise((resolve) => {
1377
+ requestAnimationFrame(() => {
1378
+ setTimeout(resolve, 0);
1379
+ });
1380
+ });
1381
+ }
1382
+
1383
+ async optimizeScene() {
1384
+ console.log("Starting scene optimization...");
1385
+ this.dispatchEvent("optimizationprogress", {
1386
+ phase: "start",
1387
+ progress: 0,
1388
+ message: "Starting optimization...",
1389
+ });
1390
+
1241
1391
  this.originalObjects.clear();
1242
1392
  this.originalObjectsToSelection.clear();
1243
1393
  const structureGroups = new Map();
1244
1394
 
1395
+ this.dispatchEvent("optimizationprogress", {
1396
+ phase: "collecting",
1397
+ progress: 5,
1398
+ message: "Collecting scene objects...",
1399
+ });
1400
+
1245
1401
  this.scene.traverse((object) => {
1246
1402
  if (object.userData.structureId) {
1247
1403
  const structureId = object.userData.structureId;
@@ -1274,32 +1430,78 @@ export class DynamicGltfLoader {
1274
1430
  }
1275
1431
  });
1276
1432
 
1433
+ let processedGroups = 0;
1434
+ const totalGroups = structureGroups.size;
1435
+
1436
+ this.dispatchEvent("optimizationprogress", {
1437
+ phase: "merging",
1438
+ progress: 10,
1439
+ message: `Merging ${totalGroups} structure groups...`,
1440
+ current: 0,
1441
+ total: totalGroups,
1442
+ });
1443
+
1277
1444
  for (const group of structureGroups.values()) {
1278
1445
  group.mapMeshes.clear();
1279
1446
  group.mapLines.clear();
1280
1447
  group.mapLineSegments.clear();
1281
1448
  group.mapPoints.clear();
1282
1449
 
1283
- this.mergeMeshGroups(group.meshes, group.rootGroup);
1284
- this.mergeLineGroups(group.lines, group.rootGroup);
1285
- this.mergeLineSegmentGroups(group.lineSegments, group.rootGroup);
1286
- this.mergePointsGroups(group.points, group.rootGroup);
1450
+ await this.mergeMeshGroups(group.meshes, group.rootGroup);
1451
+ await this.yieldToUI();
1452
+
1453
+ await this.mergeLineGroups(group.lines, group.rootGroup);
1454
+ await this.yieldToUI();
1455
+
1456
+ await this.mergeLineSegmentGroups(group.lineSegments, group.rootGroup);
1457
+ await this.yieldToUI();
1458
+
1459
+ await this.mergePointsGroups(group.points, group.rootGroup);
1460
+
1461
+ processedGroups++;
1462
+ const progress = 10 + Math.round((processedGroups / totalGroups) * 80);
1463
+
1464
+ this.dispatchEvent("optimizationprogress", {
1465
+ phase: "merging",
1466
+ progress,
1467
+ message: `Processing structure ${processedGroups}/${totalGroups}...`,
1468
+ current: processedGroups,
1469
+ total: totalGroups,
1470
+ });
1471
+
1472
+ console.log(`Optimization progress: ${processedGroups}/${totalGroups} structure groups processed (${progress}%)`);
1473
+
1474
+ await this.yieldToUI();
1287
1475
  }
1288
1476
 
1477
+ this.dispatchEvent("optimizationprogress", {
1478
+ phase: "finalizing",
1479
+ progress: 95,
1480
+ message: "Finalizing optimization...",
1481
+ });
1482
+
1289
1483
  this.originalObjects.forEach((obj) => {
1290
1484
  obj.visible = false;
1291
1485
  if (!(obj instanceof Points) && !obj.userData.isEdge) {
1292
1486
  this.originalObjectsToSelection.add(obj);
1293
1487
  }
1294
1488
  });
1489
+
1295
1490
  this.initializeObjectVisibility();
1296
1491
 
1297
1492
  console.log(`Optimization complete. Total objects: ${this.maxObjectId}`);
1298
1493
 
1494
+ this.dispatchEvent("optimizationprogress", {
1495
+ phase: "complete",
1496
+ progress: 100,
1497
+ message: `Optimization complete! ${this.maxObjectId} objects processed.`,
1498
+ });
1499
+
1299
1500
  this.dispatchEvent("update");
1300
1501
  }
1301
1502
 
1302
- mergeMeshGroups(materialGroups, rootGroup) {
1503
+ async mergeMeshGroups(materialGroups, rootGroup) {
1504
+ let processedGroups = 0;
1303
1505
  for (const group of materialGroups) {
1304
1506
  if (!group.material) {
1305
1507
  console.warn("Skipping mesh group with null material");
@@ -1399,6 +1601,11 @@ export class DynamicGltfLoader {
1399
1601
  this.handleToOptimizedObjects.set(handle, mergedObjects);
1400
1602
  }
1401
1603
  });
1604
+
1605
+ processedGroups++;
1606
+ if (processedGroups % 5 === 0) {
1607
+ await this.yieldToUI();
1608
+ }
1402
1609
  } catch (error) {
1403
1610
  console.error("Failed to merge meshes for material:", error);
1404
1611
  group.objects.forEach((mesh) => {
@@ -1408,7 +1615,8 @@ export class DynamicGltfLoader {
1408
1615
  }
1409
1616
  }
1410
1617
 
1411
- mergeLineGroups(materialGroups, rootGroup) {
1618
+ async mergeLineGroups(materialGroups, rootGroup) {
1619
+ let processedGroups = 0;
1412
1620
  for (const group of materialGroups) {
1413
1621
  if (group.objects.length === 0) continue;
1414
1622
 
@@ -1433,7 +1641,9 @@ export class DynamicGltfLoader {
1433
1641
  const indices = [];
1434
1642
  let vertexOffset = 0;
1435
1643
 
1644
+ let isEdge = false;
1436
1645
  group.objects.forEach((line) => {
1646
+ isEdge = line.userData.isEdge;
1437
1647
  const geometry = line.geometry;
1438
1648
  const positionAttr = geometry.attributes.position;
1439
1649
  const vertexCount = positionAttr.count;
@@ -1442,7 +1652,6 @@ export class DynamicGltfLoader {
1442
1652
  if (!this.objectIdToIndex.has(handle)) {
1443
1653
  this.objectIdToIndex.set(handle, this.maxObjectId++);
1444
1654
  }
1445
- // const objectId = this.objectIdToIndex.get(handle);
1446
1655
 
1447
1656
  objectMapping.set(line, {
1448
1657
  startVertexIndex: currentVertexOffset,
@@ -1497,6 +1706,8 @@ export class DynamicGltfLoader {
1497
1706
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1498
1707
 
1499
1708
  const mergedLine = new LineSegments(geometry, visibilityMaterial);
1709
+ mergedLine.userData.isEdge = isEdge;
1710
+
1500
1711
  const mergedObjects = [mergedLine];
1501
1712
  if (this.useVAO) {
1502
1713
  this.createVAO(mergedLine);
@@ -1522,10 +1733,16 @@ export class DynamicGltfLoader {
1522
1733
  this.handleToOptimizedObjects.set(handle, mergedObjects);
1523
1734
  }
1524
1735
  });
1736
+
1737
+ processedGroups++;
1738
+ if (processedGroups % 5 === 0) {
1739
+ await this.yieldToUI();
1740
+ }
1525
1741
  }
1526
1742
  }
1527
1743
 
1528
- mergeLineSegmentGroups(materialGroups, rootGroup) {
1744
+ async mergeLineSegmentGroups(materialGroups, rootGroup) {
1745
+ let processedGroups = 0;
1529
1746
  for (const group of materialGroups) {
1530
1747
  if (!group.material) {
1531
1748
  console.warn("Skipping line segment group with null material");
@@ -1538,8 +1755,10 @@ export class DynamicGltfLoader {
1538
1755
  const handles = new Set();
1539
1756
  const objectMapping = new Map();
1540
1757
  let currentVertexOffset = 0;
1758
+ let isEdge = false;
1541
1759
 
1542
1760
  for (const line of group.objects) {
1761
+ isEdge = line.userData.isEdge;
1543
1762
  const geometry = line.geometry.clone();
1544
1763
  line.updateWorldMatrix(true, false);
1545
1764
  geometry.applyMatrix4(line.matrixWorld);
@@ -1585,6 +1804,7 @@ export class DynamicGltfLoader {
1585
1804
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1586
1805
 
1587
1806
  const mergedLine = new LineSegments(mergedGeometry, visibilityMaterial);
1807
+ mergedLine.userData.isEdge = isEdge;
1588
1808
 
1589
1809
  if (this.useVAO) {
1590
1810
  this.createVAO(mergedLine);
@@ -1618,6 +1838,11 @@ export class DynamicGltfLoader {
1618
1838
  this.handleToOptimizedObjects.set(handle, mergedObjects);
1619
1839
  }
1620
1840
  });
1841
+
1842
+ processedGroups++;
1843
+ if (processedGroups % 5 === 0) {
1844
+ await this.yieldToUI();
1845
+ }
1621
1846
  } catch (error) {
1622
1847
  console.warn("Failed to merge line segments for material:", error);
1623
1848
  group.objects.forEach((line) => {
@@ -1627,7 +1852,8 @@ export class DynamicGltfLoader {
1627
1852
  }
1628
1853
  }
1629
1854
 
1630
- mergePointsGroups(materialGroups, rootGroup) {
1855
+ async mergePointsGroups(materialGroups, rootGroup) {
1856
+ let processedGroups = 0;
1631
1857
  for (const group of materialGroups) {
1632
1858
  if (!group.material) {
1633
1859
  console.warn("Skipping points group with null material");
@@ -1678,6 +1904,11 @@ export class DynamicGltfLoader {
1678
1904
  this.handleToOptimizedObjects.set(handle, mergedObjects);
1679
1905
  }
1680
1906
  });
1907
+
1908
+ processedGroups++;
1909
+ if (processedGroups % 5 === 0) {
1910
+ await this.yieldToUI();
1911
+ }
1681
1912
  } catch (error) {
1682
1913
  console.warn("Failed to merge points for material:", error);
1683
1914
  group.objects.forEach((points) => {
@@ -30,7 +30,6 @@ import { DynamicGltfLoader } from "./DynamicGltfLoader.js";
30
30
 
31
31
  export class DynamicModelImpl extends ModelImpl {
32
32
  public gltfLoader: DynamicGltfLoader;
33
- public modelId: number;
34
33
 
35
34
  override getExtents(target: Box3): Box3 {
36
35
  return target.union(this.gltfLoader.getTotalGeometryExtent());
@@ -53,35 +52,46 @@ export class DynamicModelImpl extends ModelImpl {
53
52
  }
54
53
 
55
54
  override getObjectsByHandles(handles: string | string[]): Object3D[] {
56
- const handlesSet = new Set(handles);
55
+ const ownHandles = this.getOwnHandles(handles);
56
+ if (ownHandles.length === 0) return [];
57
+
58
+ const handlesSet = new Set(ownHandles);
59
+
57
60
  const objects = [];
58
61
  handlesSet.forEach((handle) => {
59
- const handle2 = `${this.modelId}_${handle}`; // <- props fix: Dynamic Loader uses handle with structure ID prefix
60
- const handles = this.gltfLoader.handleToObjects.get(handle2) || [];
61
- objects.push(...Array.from(handles));
62
+ objects.push(...this.gltfLoader.getObjectsByHandle(handle));
62
63
  });
64
+
63
65
  return objects;
64
66
  }
65
67
 
66
68
  override getHandlesByObjects(objects: Object3D | Object3D[]): string[] {
67
- const handles = super.getHandlesByObjects(objects);
68
- return handles.map((x) => x.split("_").pop()); // <- props fix: remove structure ID prefix by the Dynamic Loader
69
+ const ownObjects = this.getOwnObjects(objects);
70
+ if (ownObjects.length === 0) return [];
71
+
72
+ const handleSet = new Set<string>();
73
+ ownObjects.forEach((object) => {
74
+ const handle = object.userData.handle;
75
+ if (handle) handleSet.add(handle);
76
+ });
77
+
78
+ return Array.from(handleSet);
69
79
  }
70
80
 
71
81
  override hideObjects(objects: Object3D | Object3D[]): this {
72
- const handles = super.getHandlesByObjects(objects);
82
+ const handles = this.getHandlesByObjects(objects);
73
83
  this.gltfLoader.hideObjects(handles);
74
84
  return this;
75
85
  }
76
86
 
77
87
  override isolateObjects(objects: Object3D | Object3D[]): this {
78
- const handles = super.getHandlesByObjects(objects);
88
+ const handles = this.getHandlesByObjects(objects);
79
89
  this.gltfLoader.isolateObjects(new Set(handles));
80
90
  return this;
81
91
  }
82
92
 
83
93
  override showObjects(objects: Object3D | Object3D[]): this {
84
- const handles = super.getHandlesByObjects(objects);
94
+ const handles = this.getHandlesByObjects(objects);
85
95
  this.gltfLoader.showObjects(handles);
86
96
  return this;
87
97
  }