@inweb/viewer-three 26.11.0 → 26.12.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 (44) hide show
  1. package/dist/extensions/components/AxesHelperComponent.js +1 -1
  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 +1 -1
  5. package/dist/extensions/components/AxesHelperComponent.module.js.map +1 -1
  6. package/dist/extensions/components/InfoPanelComponent.js +170 -0
  7. package/dist/extensions/components/InfoPanelComponent.js.map +1 -0
  8. package/dist/extensions/components/InfoPanelComponent.min.js +24 -0
  9. package/dist/extensions/components/InfoPanelComponent.module.js +164 -0
  10. package/dist/extensions/components/InfoPanelComponent.module.js.map +1 -0
  11. package/dist/extensions/components/StatsPanelComponent.js +9 -3
  12. package/dist/extensions/components/StatsPanelComponent.js.map +1 -1
  13. package/dist/extensions/components/StatsPanelComponent.min.js +1 -1
  14. package/dist/extensions/components/StatsPanelComponent.module.js +9 -3
  15. package/dist/extensions/components/StatsPanelComponent.module.js.map +1 -1
  16. package/dist/extensions/loaders/PotreeLoader.js +55 -4
  17. package/dist/extensions/loaders/PotreeLoader.js.map +1 -1
  18. package/dist/extensions/loaders/PotreeLoader.min.js +1 -1
  19. package/dist/extensions/loaders/PotreeLoader.module.js +52 -0
  20. package/dist/extensions/loaders/PotreeLoader.module.js.map +1 -1
  21. package/dist/viewer-three.js +435 -14
  22. package/dist/viewer-three.js.map +1 -1
  23. package/dist/viewer-three.min.js +8 -3
  24. package/dist/viewer-three.module.js +379 -10
  25. package/dist/viewer-three.module.js.map +1 -1
  26. package/extensions/components/AxesHelperComponent.ts +1 -1
  27. package/extensions/components/InfoPanelComponent.ts +197 -0
  28. package/extensions/components/StatsPanelComponent.ts +10 -3
  29. package/extensions/loaders/Potree/PotreeModelImpl.ts +72 -0
  30. package/lib/Viewer/Viewer.d.ts +2 -1
  31. package/lib/Viewer/components/InfoComponent.d.ts +22 -0
  32. package/lib/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.d.ts +2 -0
  33. package/lib/Viewer/models/IModelImpl.d.ts +2 -1
  34. package/lib/Viewer/models/ModelImpl.d.ts +2 -0
  35. package/package.json +5 -5
  36. package/src/Viewer/Viewer.ts +41 -5
  37. package/src/Viewer/components/InfoComponent.ts +187 -0
  38. package/src/Viewer/components/index.ts +2 -0
  39. package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +16 -8
  40. package/src/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.ts +25 -0
  41. package/src/Viewer/loaders/DynamicGltfLoader/GltfStructure.js +67 -1
  42. package/src/Viewer/loaders/RangesLoader.ts +11 -1
  43. package/src/Viewer/models/IModelImpl.ts +3 -1
  44. package/src/Viewer/models/ModelImpl.ts +158 -0
@@ -0,0 +1,187 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2002-2025, 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-2025 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
+
24
+ import { REVISION, Vector2 } from "three";
25
+ import { IComponent } from "@inweb/viewer-core";
26
+ import type { Viewer } from "../Viewer";
27
+
28
+ export class InfoComponent implements IComponent {
29
+ protected viewer: Viewer;
30
+ private startTime: number;
31
+ private beginTime: number;
32
+ private prevTime: number;
33
+ private frames: number;
34
+
35
+ constructor(viewer: Viewer) {
36
+ this.viewer = viewer;
37
+ this.startTime = 0;
38
+ this.beginTime = performance.now();
39
+ this.prevTime = performance.now();
40
+ this.frames = 0;
41
+ this.viewer.addEventListener("initialize", this.initialize);
42
+ this.viewer.addEventListener("clear", this.clear);
43
+ this.viewer.addEventListener("optionschange", this.optionsChange);
44
+ this.viewer.addEventListener("geometrystart", this.geometryStart);
45
+ this.viewer.addEventListener("databasechunk", this.databaseChunk);
46
+ this.viewer.addEventListener("geometryend", this.geometryEnd);
47
+ this.viewer.addEventListener("resize", this.resize);
48
+ this.viewer.addEventListener("render", this.render);
49
+ this.viewer.addEventListener("animate", this.animate);
50
+ }
51
+
52
+ dispose() {
53
+ this.viewer.removeEventListener("initialize", this.initialize);
54
+ this.viewer.removeEventListener("clear", this.clear);
55
+ this.viewer.removeEventListener("optionschange", this.optionsChange);
56
+ this.viewer.removeEventListener("geometrystart", this.geometryStart);
57
+ this.viewer.removeEventListener("databasechunk", this.databaseChunk);
58
+ this.viewer.removeEventListener("geometryend", this.geometryEnd);
59
+ this.viewer.removeEventListener("resize", this.resize);
60
+ this.viewer.removeEventListener("render", this.render);
61
+ this.viewer.addEventListener("animate", this.animate);
62
+ }
63
+
64
+ initialize = () => {
65
+ try {
66
+ const gl = this.viewer.renderer.getContext();
67
+ const dbgInfo = gl.getExtension("WEBGL_debug_renderer_info");
68
+ if (dbgInfo) {
69
+ this.viewer.info.system.webglRenderer = gl.getParameter(dbgInfo.UNMASKED_RENDERER_WEBGL);
70
+ this.viewer.info.system.webglVendor = gl.getParameter(dbgInfo.UNMASKED_VENDOR_WEBGL);
71
+ }
72
+ } catch (error) {
73
+ console.error("Error reading WebGL info.", error);
74
+ }
75
+
76
+ console.log("THREE.WebGLRenderer:", REVISION);
77
+ console.log("WebGL Renderer:", this.viewer.info.system.webglRenderer);
78
+ console.log("WebGL Vendor:", this.viewer.info.system.webglVendor);
79
+
80
+ this.resize();
81
+
82
+ this.optionsChange({ data: this.viewer.options });
83
+ };
84
+
85
+ clear = () => {
86
+ this.viewer.info.performance.timeToFirstRender = 0;
87
+ this.viewer.info.performance.loadTime = 0;
88
+
89
+ this.viewer.info.scene.objects = 0;
90
+ this.viewer.info.scene.triangles = 0;
91
+ this.viewer.info.scene.points = 0;
92
+ this.viewer.info.scene.lines = 0;
93
+ this.viewer.info.scene.edges = 0;
94
+
95
+ this.viewer.info.optimizedScene.objects = 0;
96
+ this.viewer.info.optimizedScene.triangles = 0;
97
+ this.viewer.info.optimizedScene.points = 0;
98
+ this.viewer.info.optimizedScene.lines = 0;
99
+ this.viewer.info.optimizedScene.edges = 0;
100
+
101
+ this.viewer.info.memory.geometries = 0;
102
+ this.viewer.info.memory.geometryBytes = 0;
103
+ this.viewer.info.memory.textures = 0;
104
+ this.viewer.info.memory.textureBytes = 0;
105
+ this.viewer.info.memory.materials = 0;
106
+ this.viewer.info.memory.totalEstimatedGpuBytes = 0;
107
+ this.viewer.info.memory.usedJSHeapSize = 0;
108
+ };
109
+
110
+ optionsChange = ({ data: options }) => {
111
+ if (options.antialiasing === false) this.viewer.info.render.antialiasing = "";
112
+ else if (options.antialiasing === true) this.viewer.info.render.antialiasing = "mxaa";
113
+ else this.viewer.info.render.antialiasing = options.antialiasing;
114
+ };
115
+
116
+ geometryStart = () => {
117
+ this.startTime = performance.now();
118
+ };
119
+
120
+ databaseChunk = () => {
121
+ this.viewer.info.performance.timeToFirstRender += performance.now() - this.startTime;
122
+
123
+ console.log("Time to first render:", this.viewer.info.performance.timeToFirstRender, "ms");
124
+ };
125
+
126
+ geometryEnd = () => {
127
+ const model = this.viewer.models[this.viewer.models.length - 1];
128
+ const info = model.getInfo();
129
+
130
+ this.viewer.info.scene.objects += info.scene.objects;
131
+ this.viewer.info.scene.triangles += info.scene.triangles;
132
+ this.viewer.info.scene.points += info.scene.points;
133
+ this.viewer.info.scene.lines += info.scene.lines;
134
+ this.viewer.info.scene.edges += info.scene.edges;
135
+
136
+ this.viewer.info.optimizedScene.objects += info.optimizedScene.objects;
137
+ this.viewer.info.optimizedScene.triangles += info.optimizedScene.triangles;
138
+ this.viewer.info.optimizedScene.points += info.optimizedScene.points;
139
+ this.viewer.info.optimizedScene.lines += info.optimizedScene.lines;
140
+ this.viewer.info.optimizedScene.edges += info.optimizedScene.edges;
141
+
142
+ this.viewer.info.memory.geometries += info.memory.geometries;
143
+ this.viewer.info.memory.geometryBytes += info.memory.geometryBytes;
144
+ this.viewer.info.memory.textures += info.memory.textures;
145
+ this.viewer.info.memory.textureBytes += info.memory.textureBytes;
146
+ this.viewer.info.memory.materials += info.memory.materials;
147
+ this.viewer.info.memory.totalEstimatedGpuBytes += info.memory.totalEstimatedGpuBytes;
148
+
149
+ const memory = performance["memory"];
150
+ if (memory) this.viewer.info.memory.usedJSHeapSize = memory.usedJSHeapSize;
151
+
152
+ this.viewer.info.performance.loadTime += performance.now() - this.startTime;
153
+
154
+ console.log("Number of objects:", info.scene.objects);
155
+ console.log("Number of objects after optimization:", info.optimizedScene.objects);
156
+ console.log("Total geometry size:", info.memory.totalEstimatedGpuBytes / (1024 * 1024), "MB");
157
+
158
+ console.log("File load time:", this.viewer.info.performance.loadTime, "ms");
159
+ };
160
+
161
+ resize = () => {
162
+ const rendererSize = this.viewer.renderer.getSize(new Vector2());
163
+ this.viewer.info.render.viewport.width = rendererSize.x;
164
+ this.viewer.info.render.viewport.height = rendererSize.y;
165
+ };
166
+
167
+ render = () => {
168
+ this.viewer.info.render.drawCalls = this.viewer.renderer.info.render.calls;
169
+ this.viewer.info.render.triangles = this.viewer.renderer.info.render.triangles;
170
+ this.viewer.info.render.points = this.viewer.renderer.info.render.points;
171
+ this.viewer.info.render.lines = this.viewer.renderer.info.render.lines;
172
+ };
173
+
174
+ animate = () => {
175
+ const time = performance.now();
176
+
177
+ this.viewer.info.performance.frameTime = Math.round(time - this.beginTime);
178
+ this.beginTime = time;
179
+
180
+ this.frames++;
181
+ if (time - this.prevTime >= 1000) {
182
+ this.viewer.info.performance.fps = Math.round((this.frames * 1000) / (time - this.prevTime));
183
+ this.prevTime = time;
184
+ this.frames = 0;
185
+ }
186
+ };
187
+ }
@@ -27,6 +27,7 @@ import { BackgroundComponent } from "./BackgroundComponent";
27
27
  import { CameraComponent } from "./CameraComponent";
28
28
  import { ExtentsComponent } from "./ExtentsComponent";
29
29
  import { LightComponent } from "./LightComponent";
30
+ import { InfoComponent } from "./InfoComponent";
30
31
  import { RenderLoopComponent } from "./RenderLoopComponent";
31
32
  import { ResizeCanvasComponent } from "./ResizeCanvasComponent";
32
33
  import { HighlighterComponent } from "./HighlighterComponent";
@@ -80,6 +81,7 @@ components.registerComponent("ExtentsComponent", (viewer) => new ExtentsComponen
80
81
  components.registerComponent("CameraComponent", (viewer) => new CameraComponent(viewer));
81
82
  components.registerComponent("BackgroundComponent", (viewer) => new BackgroundComponent(viewer));
82
83
  components.registerComponent("LightComponent", (viewer) => new LightComponent(viewer));
84
+ components.registerComponent("InfoComponent", (viewer) => new InfoComponent(viewer));
83
85
  components.registerComponent("ResizeCanvasComponent", (viewer) => new ResizeCanvasComponent(viewer));
84
86
  components.registerComponent("RenderLoopComponent", (viewer) => new RenderLoopComponent(viewer));
85
87
  components.registerComponent("HighlighterComponent", (viewer) => new HighlighterComponent(viewer));
@@ -117,7 +117,7 @@ export class DynamicGltfLoader {
117
117
  this.mergedObjectMap = new Map(); // objectId -> {mergedObject, startIndex, endIndex, vertexCount}
118
118
  this.mergedGeometryVisibility = new Map(); // mergedObject -> visibility array
119
119
 
120
- this._webglInfoCache = null; // { renderer, vendor }
120
+ this._webglInfoCache = null;
121
121
  }
122
122
 
123
123
  setVisibleEdges(visible) {
@@ -604,10 +604,17 @@ export class DynamicGltfLoader {
604
604
  onLoadFinishCb();
605
605
  }
606
606
  } catch (error) {
607
- if (error.name !== "AbortError") {
608
- console.error(`Error loading node ${nodeId}:`, error);
609
- }
610
607
  node.loading = false;
608
+
609
+ if (error.name === "AbortError") {
610
+ return;
611
+ }
612
+
613
+ if (node.structure && node.structure.loadingAborted) {
614
+ return;
615
+ }
616
+
617
+ console.error(`Error loading node ${nodeId}:`, error);
611
618
  }
612
619
  }
613
620
 
@@ -1099,7 +1106,6 @@ export class DynamicGltfLoader {
1099
1106
  }
1100
1107
 
1101
1108
  createVisibilityMaterial(material) {
1102
- // Apply shader directly to the original material
1103
1109
  material.onBeforeCompile = (shader) => {
1104
1110
  shader.vertexShader = shader.vertexShader.replace(
1105
1111
  "#include <common>",
@@ -1134,8 +1140,6 @@ export class DynamicGltfLoader {
1134
1140
  `
1135
1141
  );
1136
1142
  };
1137
-
1138
- // Force recompilation of material
1139
1143
  material.needsUpdate = true;
1140
1144
 
1141
1145
  return material;
@@ -1170,7 +1174,6 @@ export class DynamicGltfLoader {
1170
1174
  });
1171
1175
  this.nodes.clear();
1172
1176
 
1173
- // Clear all loaded meshes
1174
1177
  this.loadedMeshes.forEach((mesh) => {
1175
1178
  if (mesh.geometry) mesh.geometry.dispose();
1176
1179
  if (mesh.material) {
@@ -1578,6 +1581,7 @@ export class DynamicGltfLoader {
1578
1581
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1579
1582
 
1580
1583
  const mergedMesh = new Mesh(mergedGeometry, visibilityMaterial);
1584
+ mergedMesh.userData.isOptimized = true;
1581
1585
  rootGroup.add(mergedMesh);
1582
1586
 
1583
1587
  this.mergedMesh.add(mergedMesh);
@@ -1710,6 +1714,7 @@ export class DynamicGltfLoader {
1710
1714
 
1711
1715
  const mergedLine = new LineSegments(geometry, visibilityMaterial);
1712
1716
  mergedLine.userData.isEdge = isEdge;
1717
+ mergedLine.userData.isOptimized = true;
1713
1718
 
1714
1719
  const mergedObjects = [mergedLine];
1715
1720
  if (this.useVAO) {
@@ -1806,6 +1811,7 @@ export class DynamicGltfLoader {
1806
1811
 
1807
1812
  const mergedLine = new LineSegments(mergedGeometry, visibilityMaterial);
1808
1813
  mergedLine.userData.isEdge = isEdge;
1814
+ mergedLine.userData.isOptimized = true;
1809
1815
 
1810
1816
  if (this.useVAO) {
1811
1817
  this.createVAO(mergedLine);
@@ -1878,6 +1884,7 @@ export class DynamicGltfLoader {
1878
1884
  if (geometries.length > 0) {
1879
1885
  const mergedGeometry = mergeGeometries(geometries, false);
1880
1886
  const mergedPoints = new Points(mergedGeometry, group.material);
1887
+ mergedPoints.userData.isOptimized = true;
1881
1888
 
1882
1889
  if (this.useVAO) {
1883
1890
  this.createVAO(mergedPoints);
@@ -1970,6 +1977,7 @@ export class DynamicGltfLoader {
1970
1977
 
1971
1978
  const mergedLine = new LineSegments(finalGeometry, material);
1972
1979
  mergedLine.userData.structureId = structureId;
1980
+ mergedLine.userData.isOptimized = true;
1973
1981
  rootGroup.add(mergedLine);
1974
1982
  this.mergedLineSegments.add(mergedLine);
1975
1983
 
@@ -22,6 +22,7 @@
22
22
  ///////////////////////////////////////////////////////////////////////////////
23
23
 
24
24
  import { Box3, Object3D } from "three";
25
+ import { IInfo, Info } from "@inweb/viewer-core";
25
26
 
26
27
  import { ModelImpl } from "../../models/ModelImpl";
27
28
  import { DynamicGltfLoader } from "./DynamicGltfLoader.js";
@@ -31,6 +32,30 @@ import { DynamicGltfLoader } from "./DynamicGltfLoader.js";
31
32
  export class DynamicModelImpl extends ModelImpl {
32
33
  public gltfLoader: DynamicGltfLoader;
33
34
 
35
+ override getInfo(): IInfo {
36
+ const stats = this.gltfLoader.getStats();
37
+
38
+ const info = new Info();
39
+
40
+ info.scene.objects = stats.scene.beforeOptimization.objects;
41
+ info.scene.triangles = stats.scene.beforeOptimization.triangles;
42
+ info.scene.lines = stats.scene.beforeOptimization.lines;
43
+ info.scene.edges = stats.scene.beforeOptimization.edges;
44
+
45
+ info.optimizedScene.objects = stats.scene.afterOptimization.objects;
46
+ info.optimizedScene.triangles = stats.scene.afterOptimization.triangles;
47
+ info.optimizedScene.lines = stats.scene.afterOptimization.lines;
48
+ info.optimizedScene.edges = stats.scene.afterOptimization.edges;
49
+
50
+ info.memory.geometries = stats.memory.geometries.count;
51
+ info.memory.geometryBytes = stats.memory.geometries.bytes;
52
+ info.memory.textures = stats.memory.textures.count;
53
+ info.memory.materials = stats.memory.materials.count;
54
+ info.memory.totalEstimatedGpuBytes = stats.memory.totalEstimatedGpuBytes;
55
+
56
+ return info;
57
+ }
58
+
34
59
  override getExtents(target: Box3): Box3 {
35
60
  return target.union(this.gltfLoader.getTotalGeometryExtent());
36
61
  }
@@ -60,6 +60,8 @@ export class GltfStructure {
60
60
  this.materialCache = new Map();
61
61
  this.uri = "";
62
62
  this._nextObjectId = 0;
63
+ this.loadingAborted = false;
64
+ this.criticalError = null;
63
65
  }
64
66
 
65
67
  async initialize(loader) {
@@ -85,6 +87,8 @@ export class GltfStructure {
85
87
 
86
88
  this.activeChunkLoads = 0;
87
89
  this.chunkQueue = [];
90
+ this.loadingAborted = false;
91
+ this.criticalError = null;
88
92
  }
89
93
 
90
94
  getJson() {
@@ -93,6 +97,10 @@ export class GltfStructure {
93
97
 
94
98
  scheduleRequest(request) {
95
99
  return new Promise((resolve, reject) => {
100
+ if (this.loadingAborted) {
101
+ reject(this.criticalError || new Error("Structure loading has been aborted due to critical error"));
102
+ return;
103
+ }
96
104
  this.pendingRequests.push({
97
105
  ...request,
98
106
  _resolve: resolve,
@@ -101,6 +109,52 @@ export class GltfStructure {
101
109
  });
102
110
  }
103
111
 
112
+ isCriticalHttpError(error) {
113
+ if (!error) return false;
114
+
115
+ const status = error.status || error.statusCode || error.code;
116
+
117
+ if (typeof status === "number") {
118
+ return status >= 400 && status < 600;
119
+ }
120
+
121
+ if (error.message) {
122
+ const match = error.message.match(/HTTP\s+(\d{3})/i);
123
+ if (match) {
124
+ const code = parseInt(match[1], 10);
125
+ return code >= 400 && code < 600;
126
+ }
127
+ }
128
+
129
+ return false;
130
+ }
131
+
132
+ abortLoading(error) {
133
+ if (this.loadingAborted) {
134
+ return;
135
+ }
136
+
137
+ this.loadingAborted = true;
138
+ this.criticalError = error;
139
+
140
+ const requests = [...this.pendingRequests];
141
+ this.pendingRequests = [];
142
+
143
+ for (const req of requests) {
144
+ if (req._reject) {
145
+ req._reject(error);
146
+ }
147
+ }
148
+
149
+ console.error(
150
+ `❌ Critical error for structure "${this.id}". All further loading aborted.`,
151
+ `\n Error: ${error.message || error}`,
152
+ `\n Rejected ${requests.length} pending chunk requests.`
153
+ );
154
+
155
+ throw error;
156
+ }
157
+
104
158
  async flushBufferRequests() {
105
159
  if (!this.pendingRequests || this.pendingRequests.length === 0) return;
106
160
  const requests = [...this.pendingRequests];
@@ -165,6 +219,13 @@ export class GltfStructure {
165
219
  }
166
220
 
167
221
  const promises = finalRanges.map(async (range, index) => {
222
+ if (this.loadingAborted) {
223
+ for (const req of range.requests) {
224
+ req._reject(this.criticalError || new Error("Structure loading aborted"));
225
+ }
226
+ return;
227
+ }
228
+
168
229
  await this.loader.waitForChunkSlot();
169
230
 
170
231
  try {
@@ -183,7 +244,12 @@ export class GltfStructure {
183
244
  for (const req of range.requests) {
184
245
  req._reject(error);
185
246
  }
186
- console.warn(`Failed to load chunk ${index + 1}/${finalRanges.length} (${range.start}-${range.end}):`, error);
247
+
248
+ if (this.isCriticalHttpError(error)) {
249
+ this.abortLoading(error);
250
+ } else {
251
+ console.warn(`Failed to load chunk ${index + 1}/${finalRanges.length} (${range.start}-${range.end}):`, error);
252
+ }
187
253
  } finally {
188
254
  this.loader.releaseChunkSlot();
189
255
  }
@@ -21,6 +21,16 @@
21
21
  // acknowledge and accept the above terms.
22
22
  ///////////////////////////////////////////////////////////////////////////////
23
23
 
24
+ class FetchError extends Error {
25
+ public status: number;
26
+
27
+ constructor(status: number, message: string) {
28
+ super(message);
29
+ this.name = "FetchError";
30
+ this.status = status;
31
+ }
32
+ }
33
+
24
34
  export interface Range {
25
35
  offset: number;
26
36
  length: number;
@@ -61,7 +71,7 @@ export class RangesLoader {
61
71
 
62
72
  const response = await fetch(url, init);
63
73
  if (!response.ok) {
64
- throw new Error(`Failed to fetch "${url}", status ${response.status}`);
74
+ throw new FetchError(response.status, `Failed to fetch "${url}", status ${response.status}`);
65
75
  }
66
76
 
67
77
  if (response.status !== 206) {
@@ -22,7 +22,7 @@
22
22
  ///////////////////////////////////////////////////////////////////////////////
23
23
 
24
24
  import { Box3, Object3D } from "three";
25
- import { IModel } from "@inweb/viewer-core";
25
+ import { IInfo, IModel } from "@inweb/viewer-core";
26
26
 
27
27
  /**
28
28
  * Basic model implementation.
@@ -38,6 +38,8 @@ export interface IModelImpl extends IModel {
38
38
 
39
39
  getPrecision(): number;
40
40
 
41
+ getInfo(): IInfo;
42
+
41
43
  getExtents(target: Box3): Box3;
42
44
 
43
45
  getObjects(): Object3D[];
@@ -22,6 +22,8 @@
22
22
  ///////////////////////////////////////////////////////////////////////////////
23
23
 
24
24
  import { Box3, Object3D, Vector3 } from "three";
25
+ import { IInfo, Info } from "@inweb/viewer-core";
26
+
25
27
  import { IModelImpl } from "./IModelImpl";
26
28
  import { convertUnits } from "../measurement/UnitConverter";
27
29
  import { getDisplayUnit } from "../measurement/UnitFormatter";
@@ -91,6 +93,162 @@ export class ModelImpl implements IModelImpl {
91
93
  return 2;
92
94
  }
93
95
 
96
+ getInfo(): IInfo {
97
+ // ===================== AI-CODE-START ======================
98
+ // Source: Claude Sonnet 4.5
99
+ // Date: 2025-10-02
100
+ // Reviewer: roman.mochalov@opendesign.com
101
+ // Issue: CLOUD-5738
102
+
103
+ const geometries = new Set();
104
+ const materials = new Set();
105
+ const textures = new Set();
106
+
107
+ let totalObjects = 0;
108
+ let totalTriangles = 0;
109
+ let totalPoints = 0;
110
+ let totalLines = 0;
111
+ let totalEdges = 0;
112
+ let geometryBytes = 0;
113
+ let textureBytes = 0;
114
+
115
+ this.scene.traverse((object: any) => {
116
+ totalObjects++;
117
+
118
+ if (object.geometry) {
119
+ const geometry = object.geometry;
120
+
121
+ if (!geometries.has(geometry)) {
122
+ geometries.add(geometry);
123
+
124
+ if (geometry.attributes) {
125
+ for (const name in geometry.attributes) {
126
+ const attribute = geometry.attributes[name];
127
+ if (attribute && attribute.array) {
128
+ geometryBytes += attribute.array.byteLength;
129
+ }
130
+ }
131
+ }
132
+
133
+ if (geometry.index && geometry.index.array) {
134
+ geometryBytes += geometry.index.array.byteLength;
135
+ }
136
+ }
137
+
138
+ if (geometry.index) {
139
+ const indexCount = geometry.index.count;
140
+
141
+ if (object.isLine || object.isLineSegments) {
142
+ totalLines += indexCount / 2;
143
+ } else if (object.isPoints) {
144
+ totalPoints += indexCount;
145
+ } else {
146
+ totalTriangles += indexCount / 3;
147
+ }
148
+ } else if (geometry.attributes && geometry.attributes.position) {
149
+ const positionCount = geometry.attributes.position.count;
150
+
151
+ if (object.isLine || object.isLineSegments) {
152
+ totalLines += positionCount / 2;
153
+ } else if (object.isPoints) {
154
+ totalPoints += positionCount;
155
+ } else {
156
+ totalTriangles += positionCount / 3;
157
+ }
158
+ }
159
+
160
+ if (object.isLineSegments && geometry.attributes.position) {
161
+ totalEdges += geometry.attributes.position.count / 2;
162
+ }
163
+ }
164
+
165
+ if (object.material) {
166
+ const materialsArray = Array.isArray(object.material) ? object.material : [object.material];
167
+
168
+ materialsArray.forEach((material: any) => {
169
+ materials.add(material);
170
+
171
+ if (material.map && !textures.has(material.map)) {
172
+ textures.add(material.map);
173
+ textureBytes += estimateTextureSize(material.map);
174
+ }
175
+
176
+ const textureProps = [
177
+ "alphaMap",
178
+ "aoMap",
179
+ "bumpMap",
180
+ "displacementMap",
181
+ "emissiveMap",
182
+ "envMap",
183
+ "lightMap",
184
+ "metalnessMap",
185
+ "normalMap",
186
+ "roughnessMap",
187
+ "specularMap",
188
+ "clearcoatMap",
189
+ "clearcoatNormalMap",
190
+ "clearcoatRoughnessMap",
191
+ "iridescenceMap",
192
+ "sheenColorMap",
193
+ "sheenRoughnessMap",
194
+ "thicknessMap",
195
+ "transmissionMap",
196
+ "anisotropyMap",
197
+ "gradientMap",
198
+ ];
199
+
200
+ textureProps.forEach((prop) => {
201
+ const texture = material[prop];
202
+ if (texture && !textures.has(texture)) {
203
+ textures.add(texture);
204
+ textureBytes += estimateTextureSize(texture);
205
+ }
206
+ });
207
+ });
208
+ }
209
+ });
210
+
211
+ function estimateTextureSize(texture: any): number {
212
+ if (!texture.image) return 0;
213
+
214
+ const width = texture.image.width || 0;
215
+ const height = texture.image.height || 0;
216
+
217
+ // Estimate bytes per pixel (RGBA = 4 bytes)
218
+ const bytesPerPixel = 4;
219
+
220
+ // Account for mipmaps (adds ~33% more memory)
221
+ const mipmapMultiplier = texture.generateMipmaps ? 1.33 : 1;
222
+
223
+ return width * height * bytesPerPixel * mipmapMultiplier;
224
+ }
225
+
226
+ // ===================== AI-CODE-END ======================
227
+
228
+ const info = new Info();
229
+
230
+ info.scene.objects = totalObjects;
231
+ info.scene.triangles = Math.floor(totalTriangles);
232
+ info.scene.points = Math.floor(totalPoints);
233
+ info.scene.lines = Math.floor(totalLines);
234
+ info.scene.edges = Math.floor(totalEdges);
235
+
236
+ info.memory.geometries = geometries.size;
237
+ info.memory.geometryBytes = geometryBytes;
238
+ info.memory.textures = textures.size;
239
+ info.memory.textureBytes = Math.floor(textureBytes);
240
+ info.memory.materials = materials.size;
241
+ info.memory.totalEstimatedGpuBytes = geometryBytes + Math.floor(textureBytes);
242
+
243
+ info.optimizedScene.objects = info.scene.objects;
244
+ info.optimizedScene.triangles = info.scene.triangles;
245
+ info.optimizedScene.points = info.scene.points;
246
+ info.optimizedScene.lines = info.scene.lines;
247
+ info.optimizedScene.edges = info.scene.edges;
248
+
249
+ return info;
250
+ }
251
+
94
252
  getExtents(target: Box3): Box3 {
95
253
  this.scene.traverseVisible((object) => !object.children.length && target.expandByObject(object));
96
254
  return target;