@inweb/viewer-three 26.10.6 → 26.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +7 -4
  2. package/dist/{plugins → extensions}/components/AxesHelperComponent.js +23 -1
  3. package/dist/extensions/components/AxesHelperComponent.js.map +1 -0
  4. package/dist/extensions/components/AxesHelperComponent.min.js +24 -0
  5. package/dist/{plugins → extensions}/components/AxesHelperComponent.module.js +24 -2
  6. package/dist/extensions/components/AxesHelperComponent.module.js.map +1 -0
  7. package/dist/{plugins → extensions}/components/ExtentsHelperComponent.js +18 -0
  8. package/dist/extensions/components/ExtentsHelperComponent.js.map +1 -0
  9. package/dist/{plugins/components/AxesHelperComponent.min.js → extensions/components/ExtentsHelperComponent.min.js} +1 -1
  10. package/dist/{plugins → extensions}/components/ExtentsHelperComponent.module.js +19 -1
  11. package/dist/extensions/components/ExtentsHelperComponent.module.js.map +1 -0
  12. package/dist/extensions/components/GridHelperComponent.js.map +1 -0
  13. package/dist/extensions/components/GridHelperComponent.module.js.map +1 -0
  14. package/dist/extensions/components/InfoPanelComponent.js +170 -0
  15. package/dist/extensions/components/InfoPanelComponent.js.map +1 -0
  16. package/dist/extensions/components/InfoPanelComponent.min.js +24 -0
  17. package/dist/extensions/components/InfoPanelComponent.module.js +164 -0
  18. package/dist/extensions/components/InfoPanelComponent.module.js.map +1 -0
  19. package/dist/extensions/components/LightHelperComponent.js.map +1 -0
  20. package/dist/extensions/components/LightHelperComponent.module.js.map +1 -0
  21. package/dist/extensions/components/RoomEnvironmentComponent.js.map +1 -0
  22. package/dist/extensions/components/RoomEnvironmentComponent.module.js.map +1 -0
  23. package/dist/{plugins → extensions}/components/StatsPanelComponent.js +9 -3
  24. package/dist/extensions/components/StatsPanelComponent.js.map +1 -0
  25. package/dist/extensions/components/StatsPanelComponent.min.js +24 -0
  26. package/dist/{plugins → extensions}/components/StatsPanelComponent.module.js +9 -3
  27. package/dist/extensions/components/StatsPanelComponent.module.js.map +1 -0
  28. package/dist/{plugins → extensions}/loaders/GLTFCloudLoader.js +2 -3
  29. package/dist/extensions/loaders/GLTFCloudLoader.js.map +1 -0
  30. package/dist/{plugins → extensions}/loaders/GLTFCloudLoader.min.js +1 -1
  31. package/dist/{plugins → extensions}/loaders/GLTFCloudLoader.module.js +2 -3
  32. package/dist/extensions/loaders/GLTFCloudLoader.module.js.map +1 -0
  33. package/dist/extensions/loaders/GLTFFileLoader.js +2499 -0
  34. package/dist/extensions/loaders/GLTFFileLoader.js.map +1 -0
  35. package/dist/extensions/loaders/GLTFFileLoader.min.js +24 -0
  36. package/dist/extensions/loaders/GLTFFileLoader.module.js +74 -0
  37. package/dist/extensions/loaders/GLTFFileLoader.module.js.map +1 -0
  38. package/dist/{plugins → extensions}/loaders/IFCXLoader.js +5 -7
  39. package/dist/extensions/loaders/IFCXLoader.js.map +1 -0
  40. package/dist/{plugins → extensions}/loaders/IFCXLoader.min.js +1 -1
  41. package/dist/{plugins → extensions}/loaders/IFCXLoader.module.js +5 -7
  42. package/dist/extensions/loaders/IFCXLoader.module.js.map +1 -0
  43. package/dist/{plugins → extensions}/loaders/PotreeLoader.js +56 -6
  44. package/dist/extensions/loaders/PotreeLoader.js.map +1 -0
  45. package/dist/extensions/loaders/PotreeLoader.min.js +24 -0
  46. package/dist/{plugins → extensions}/loaders/PotreeLoader.module.js +53 -2
  47. package/dist/extensions/loaders/PotreeLoader.module.js.map +1 -0
  48. package/dist/viewer-three.js +1416 -2930
  49. package/dist/viewer-three.js.map +1 -1
  50. package/dist/viewer-three.min.js +8 -3
  51. package/dist/viewer-three.module.js +1205 -363
  52. package/dist/viewer-three.module.js.map +1 -1
  53. package/{plugins → extensions}/components/AxesHelperComponent.ts +31 -2
  54. package/{plugins → extensions}/components/ExtentsHelperComponent.ts +25 -0
  55. package/extensions/components/InfoPanelComponent.ts +197 -0
  56. package/{plugins → extensions}/components/StatsPanelComponent.ts +10 -3
  57. package/{plugins → extensions}/loaders/GLTFCloudLoader.ts +2 -3
  58. package/{src/Viewer → extensions}/loaders/GLTFFileLoader.ts +21 -12
  59. package/{plugins → extensions}/loaders/IFCX/IFCXCloudLoader.ts +5 -5
  60. package/{plugins → extensions}/loaders/IFCX/IFCXFileLoader.ts +3 -4
  61. package/{plugins → extensions}/loaders/Potree/PotreeFileLoader.ts +3 -4
  62. package/extensions/loaders/Potree/PotreeModelImpl.ts +108 -0
  63. package/lib/Viewer/Viewer.d.ts +28 -20
  64. package/lib/Viewer/commands/GetSelected2.d.ts +2 -0
  65. package/lib/Viewer/commands/SelectModel.d.ts +1 -1
  66. package/lib/Viewer/commands/SetSelected2.d.ts +2 -0
  67. package/lib/Viewer/components/InfoComponent.d.ts +22 -0
  68. package/lib/Viewer/components/SelectionComponent.d.ts +1 -3
  69. package/lib/Viewer/components/index.d.ts +6 -6
  70. package/lib/Viewer/draggers/MeasureLineDragger.d.ts +7 -1
  71. package/lib/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.d.ts +2 -1
  72. package/lib/Viewer/loaders/GLTFBinaryExtension.d.ts +5 -0
  73. package/lib/Viewer/loaders/GLTFCloudDynamicLoader.d.ts +2 -2
  74. package/lib/Viewer/loaders/{GLTFFileLoader.d.ts → GLTFFileDynamicLoader.d.ts} +7 -1
  75. package/lib/Viewer/loaders/GLTFLoadingManager.d.ts +4 -3
  76. package/lib/Viewer/loaders/RangesLoader.d.ts +15 -0
  77. package/lib/Viewer/loaders/index.d.ts +22 -14
  78. package/lib/Viewer/measurement/Snapper.d.ts +15 -0
  79. package/lib/Viewer/measurement/UnitConverter.d.ts +63 -0
  80. package/lib/Viewer/measurement/UnitFormatter.d.ts +4 -0
  81. package/lib/Viewer/models/IModelImpl.d.ts +11 -8
  82. package/lib/Viewer/models/ModelImpl.d.ts +9 -5
  83. package/package.json +11 -11
  84. package/src/Viewer/Viewer.ts +127 -88
  85. package/src/Viewer/commands/ClearSelected.ts +3 -1
  86. package/src/Viewer/commands/GetModels.ts +1 -1
  87. package/src/Viewer/commands/GetSelected.ts +2 -2
  88. package/{plugins/loaders/Potree/PotreeModelImpl.ts → src/Viewer/commands/GetSelected2.ts} +7 -9
  89. package/src/Viewer/commands/HideSelected.ts +3 -1
  90. package/src/Viewer/commands/SelectModel.ts +5 -5
  91. package/src/Viewer/commands/SetSelected.ts +9 -10
  92. package/src/Viewer/commands/SetSelected2.ts +42 -0
  93. package/src/Viewer/commands/ZoomToObjects.ts +5 -6
  94. package/src/Viewer/commands/ZoomToSelected.ts +3 -1
  95. package/src/Viewer/commands/index.ts +4 -0
  96. package/src/Viewer/components/CameraComponent.ts +6 -1
  97. package/src/Viewer/components/ExtentsComponent.ts +4 -1
  98. package/src/Viewer/components/InfoComponent.ts +187 -0
  99. package/src/Viewer/components/SelectionComponent.ts +7 -30
  100. package/src/Viewer/components/index.ts +8 -6
  101. package/src/Viewer/draggers/MeasureLineDragger.ts +84 -226
  102. package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +276 -39
  103. package/src/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.ts +45 -10
  104. package/src/Viewer/loaders/DynamicGltfLoader/GltfStructure.js +71 -2
  105. package/src/Viewer/loaders/GLTFBinaryExtension.ts +91 -0
  106. package/src/Viewer/loaders/GLTFCloudDynamicLoader.ts +13 -19
  107. package/src/Viewer/loaders/GLTFFileDynamicLoader.ts +145 -0
  108. package/src/Viewer/loaders/GLTFLoadingManager.ts +5 -4
  109. package/src/Viewer/loaders/RangesLoader.ts +105 -0
  110. package/src/Viewer/loaders/index.ts +24 -16
  111. package/src/Viewer/measurement/Snapper.ts +208 -0
  112. package/src/Viewer/measurement/UnitConverter.ts +47 -0
  113. package/src/Viewer/measurement/UnitFormatter.ts +95 -0
  114. package/src/Viewer/models/IModelImpl.ts +17 -8
  115. package/src/Viewer/models/ModelImpl.ts +205 -16
  116. package/src/index-umd.ts +1 -1
  117. package/dist/plugins/components/AxesHelperComponent.js.map +0 -1
  118. package/dist/plugins/components/AxesHelperComponent.module.js.map +0 -1
  119. package/dist/plugins/components/ExtentsHelperComponent.js.map +0 -1
  120. package/dist/plugins/components/ExtentsHelperComponent.min.js +0 -24
  121. package/dist/plugins/components/ExtentsHelperComponent.module.js.map +0 -1
  122. package/dist/plugins/components/GridHelperComponent.js.map +0 -1
  123. package/dist/plugins/components/GridHelperComponent.module.js.map +0 -1
  124. package/dist/plugins/components/LightHelperComponent.js.map +0 -1
  125. package/dist/plugins/components/LightHelperComponent.module.js.map +0 -1
  126. package/dist/plugins/components/RoomEnvironmentComponent.js.map +0 -1
  127. package/dist/plugins/components/RoomEnvironmentComponent.module.js.map +0 -1
  128. package/dist/plugins/components/StatsPanelComponent.js.map +0 -1
  129. package/dist/plugins/components/StatsPanelComponent.min.js +0 -24
  130. package/dist/plugins/components/StatsPanelComponent.module.js.map +0 -1
  131. package/dist/plugins/loaders/GLTFCloudLoader.js.map +0 -1
  132. package/dist/plugins/loaders/GLTFCloudLoader.module.js.map +0 -1
  133. package/dist/plugins/loaders/IFCXLoader.js.map +0 -1
  134. package/dist/plugins/loaders/IFCXLoader.module.js.map +0 -1
  135. package/dist/plugins/loaders/PotreeLoader.js.map +0 -1
  136. package/dist/plugins/loaders/PotreeLoader.min.js +0 -24
  137. package/dist/plugins/loaders/PotreeLoader.module.js.map +0 -1
  138. /package/dist/{plugins → extensions}/components/GridHelperComponent.js +0 -0
  139. /package/dist/{plugins → extensions}/components/GridHelperComponent.min.js +0 -0
  140. /package/dist/{plugins → extensions}/components/GridHelperComponent.module.js +0 -0
  141. /package/dist/{plugins → extensions}/components/LightHelperComponent.js +0 -0
  142. /package/dist/{plugins → extensions}/components/LightHelperComponent.min.js +0 -0
  143. /package/dist/{plugins → extensions}/components/LightHelperComponent.module.js +0 -0
  144. /package/dist/{plugins → extensions}/components/RoomEnvironmentComponent.js +0 -0
  145. /package/dist/{plugins → extensions}/components/RoomEnvironmentComponent.min.js +0 -0
  146. /package/dist/{plugins → extensions}/components/RoomEnvironmentComponent.module.js +0 -0
  147. /package/{plugins → extensions}/components/GridHelperComponent.ts +0 -0
  148. /package/{plugins → extensions}/components/LightHelperComponent.ts +0 -0
  149. /package/{plugins → extensions}/components/RoomEnvironmentComponent.ts +0 -0
  150. /package/{plugins → extensions}/loaders/IFCX/IFCXLoader.ts +0 -0
  151. /package/{plugins → extensions}/loaders/IFCX/index.ts +0 -0
  152. /package/{plugins → extensions}/loaders/IFCX/render.js +0 -0
  153. /package/{plugins → extensions}/loaders/Potree/index.ts +0 -0
@@ -0,0 +1,105 @@
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
+ 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
+
34
+ export interface Range {
35
+ offset: number;
36
+ length: number;
37
+ }
38
+
39
+ export class RangesLoader {
40
+ private requestHeader: HeadersInit;
41
+ private withCredentials: boolean;
42
+ private abortSignal: AbortSignal | undefined;
43
+
44
+ constructor() {
45
+ this.requestHeader = {};
46
+ this.withCredentials = false;
47
+ this.abortSignal = undefined;
48
+ }
49
+
50
+ setRequestHeader(requestHeader: HeadersInit) {
51
+ this.requestHeader = requestHeader;
52
+ }
53
+
54
+ setWithCredentials(withCredentials: boolean) {
55
+ this.withCredentials = withCredentials;
56
+ }
57
+
58
+ setAbortSignal(abortSignal: AbortSignal) {
59
+ this.abortSignal = abortSignal;
60
+ }
61
+
62
+ async load(url: string, ranges: Range[]): Promise<ArrayBuffer> {
63
+ const init: RequestInit = {
64
+ headers: {
65
+ ...this.requestHeader,
66
+ Range: "bytes=" + ranges.map((x) => `${x.offset}-${x.offset + x.length - 1}`).join(","),
67
+ },
68
+ credentials: this.withCredentials ? "include" : "same-origin",
69
+ signal: this.abortSignal,
70
+ };
71
+
72
+ const response = await fetch(url, init);
73
+ if (!response.ok) {
74
+ throw new FetchError(response.status, `Failed to fetch "${url}", status ${response.status}`);
75
+ }
76
+
77
+ if (response.status !== 206) {
78
+ const arrayBuffer = await response.arrayBuffer();
79
+ return this.extractRanges(arrayBuffer, ranges);
80
+ }
81
+
82
+ return response.arrayBuffer();
83
+ }
84
+
85
+ // ===================== AI-CODE-START ======================
86
+ // Source: Claude Sonnet 4.5
87
+ // Date: 2025-28-10
88
+ // Reviewer: roman.mochalov@opendesign.com
89
+ // Issue: CLOUD-5933
90
+
91
+ extractRanges(arrayBuffer: ArrayBuffer, ranges: Range[]): ArrayBuffer {
92
+ const totalLength = ranges.reduce((sum, range) => sum + range.length, 0);
93
+ const result = new Uint8Array(totalLength);
94
+
95
+ let offset = 0;
96
+ for (const range of ranges) {
97
+ const chunk = new Uint8Array(arrayBuffer, range.offset, range.length);
98
+ result.set(chunk, offset);
99
+ offset += range.length;
100
+ }
101
+
102
+ return result.buffer;
103
+ }
104
+ // ===================== AI-CODE-END ======================
105
+ }
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { ILoadersRegistry, loadersRegistry } from "@inweb/viewer-core";
25
25
 
26
- import { GLTFFileLoader } from "./GLTFFileLoader";
26
+ import { GLTFFileDynamicLoader } from "./GLTFFileDynamicLoader";
27
27
  import { GLTFCloudDynamicLoader } from "./GLTFCloudDynamicLoader";
28
28
 
29
29
  /**
@@ -33,29 +33,30 @@ import { GLTFCloudDynamicLoader } from "./GLTFCloudDynamicLoader";
33
33
  *
34
34
  * 1. Define a loader class implements {@link ILoader}.
35
35
  * 2. Define a constructor with a `viewer` parameter.
36
- * 3. Override {@link ILoader.isSupport} and сheck if the loader can load the specified file.
36
+ * 3. Override {@link ILoader.isSupport} and check if the loader can load the specified file.
37
37
  * 4. Override {@link ILoader.load} and define the logic for loading the scene from the file.
38
38
  *
39
39
  * The loader should do:
40
40
  *
41
41
  * - Load raw data from file and convert it to the `Three.js` scene.
42
42
  * - Add scene to the viewer `scene`.
43
- * - Create `ModelImpl` for the scene and to the viewer `models` list.
43
+ * - Create `ModelImpl` instance with unique model ID add it to the viewer `models` list.
44
44
  * - Synchronize viewer options and overlay.
45
45
  * - Update the viewer.
46
46
  *
47
47
  * The loader must emit events:
48
48
  *
49
- * - `geometryprogress` - during loading. If progress is not supported, emit it once with a value of 100%
50
- * after the load is complete.
49
+ * - `geometryprogress` - during loading (or once at 100% when complete).
51
50
  * - `databasechunk` - when scene is loaded and ready to render.
52
51
  * 5. Override {@link ILoader.dispose} and release loader resources, if required.
53
- * 6. Register loader provider in the loaders registry by calling the {@link loaders.registerLoader}.
52
+ * 6. Use `this.abortController` (defined in `Loader` class) to abort loading raw data.
53
+ * 7. Register loader provider in the loaders registry by calling the {@link loaders.registerLoader}.
54
54
  *
55
55
  * @example Implementing a custom loader.
56
56
  *
57
57
  * ```javascript
58
- * import { loaders, Loader, ModelImpl, Viewer } from "@inweb/viewer-three";
58
+ * import { Scene } from "three";
59
+ * import { Loader, loaders, ModelImpl, Viewer } from "@inweb/viewer-three";
59
60
  *
60
61
  * class MyLoader extends Loader {
61
62
  * public viewer: Viewer;
@@ -66,18 +67,19 @@ import { GLTFCloudDynamicLoader } from "./GLTFCloudDynamicLoader";
66
67
  * }
67
68
  *
68
69
  * override isSupport(file, format): Boolean {
69
- * // place custom logic here
70
- * return ...;
70
+ * // check if this loader supports the file source and format
71
+ * return type file === "string" && format === "myformat";
71
72
  * }
72
73
  *
73
- * override load(file, format, params): Promise<this> {
74
+ * override load(file, format, params = {}): Promise<this> {
75
+ * // load raw data from file (custom loading logic)
76
+ * const data = await fetch(file).then((result) => result.arrayBuffer());
74
77
  *
75
- * // place custom loading logic here
76
- * const scene = ...;
78
+ * // convert raw data to the Three.js scene (custom parsing logic)
79
+ * const scene = this.parse(data);
77
80
  *
78
81
  * const modelImpl = new ModelImpl(scene);
79
- * modelImpl.loader = this;
80
- * modelImpl.viewer = this.viewer;
82
+ * modelImpl.id = params.modelId;
81
83
  *
82
84
  * this.viewer.scene.add(scene);
83
85
  * this.viewer.models.push(modelImpl);
@@ -86,11 +88,17 @@ import { GLTFCloudDynamicLoader } from "./GLTFCloudDynamicLoader";
86
88
  * this.viewer.syncOverlay();
87
89
  * this.viewer.update();
88
90
  *
91
+ * this.viewer.emitEvent({ type: "geometryprogress", data: 1, file });
89
92
  * this.viewer.emitEvent({ type: "databasechunk", data: scene, file });
90
93
  *
91
94
  * return Promise.resove(this);
92
95
  * };
93
- * }
96
+ *
97
+ * private parse(data: ArrayBuffer): Scene {
98
+ * // custom parsing logic
99
+ * return new Scene();
100
+ * }
101
+ * }
94
102
  *
95
103
  * loaders.registerLoader("MyLoader", (viewer) => new MyLoader(viewer));
96
104
  * ```
@@ -99,5 +107,5 @@ export const loaders: ILoadersRegistry = loadersRegistry("threejs");
99
107
 
100
108
  // build-in loaders
101
109
 
102
- loaders.registerLoader("gltf-file", (viewer: any) => new GLTFFileLoader(viewer));
110
+ loaders.registerLoader("gltf-file", (viewer: any) => new GLTFFileDynamicLoader(viewer));
103
111
  loaders.registerLoader("gltf-cloud", (viewer: any) => new GLTFCloudDynamicLoader(viewer));
@@ -0,0 +1,208 @@
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 {
25
+ Camera,
26
+ EdgesGeometry,
27
+ Intersection,
28
+ Line3,
29
+ MathUtils,
30
+ Object3D,
31
+ Plane,
32
+ Raycaster,
33
+ Vector2,
34
+ Vector3,
35
+ WebGLRenderer,
36
+ } from "three";
37
+
38
+ const DESKTOP_SNAP_DISTANCE = 10;
39
+ const MOBILE_SNAP_DISTANCE = 50;
40
+
41
+ const _vertex = new Vector3();
42
+ const _start = new Vector3();
43
+ const _end = new Vector3();
44
+ const _line = new Line3();
45
+ const _center = new Vector3();
46
+ const _projection = new Vector3();
47
+
48
+ export class Snapper {
49
+ public camera: Camera;
50
+ public renderer: WebGLRenderer;
51
+ public canvas: HTMLCanvasElement;
52
+ private raycaster: Raycaster;
53
+ private detectRadiusInPixels: number;
54
+ private edgesCache: WeakMap<any, EdgesGeometry>;
55
+
56
+ constructor(camera: Camera, renderer: WebGLRenderer, canvas: HTMLCanvasElement) {
57
+ this.camera = camera;
58
+ this.renderer = renderer;
59
+ this.canvas = canvas;
60
+
61
+ this.raycaster = new Raycaster();
62
+ this.detectRadiusInPixels = this.isMobile() ? MOBILE_SNAP_DISTANCE : DESKTOP_SNAP_DISTANCE;
63
+ this.edgesCache = new WeakMap();
64
+ }
65
+
66
+ isMobile(): boolean {
67
+ if (typeof navigator === "undefined") return false;
68
+
69
+ // ===================== AI-CODE-START ======================
70
+ // Source: Claude Sonnet 4
71
+ // Date: 2025-09-09
72
+ // Reviewer: roman.mochalov@opendesign.com
73
+ // Issue: CLOUD-5799
74
+
75
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
76
+
77
+ // ===================== AI-CODE-END ======================
78
+ }
79
+
80
+ getMousePosition(event: MouseEvent, target: Vector2): Vector2 {
81
+ return target.set(event.clientX, event.clientY);
82
+ }
83
+
84
+ getPointerIntersects(mouse: Vector2, objects: Object3D[]): Array<Intersection<Object3D>> {
85
+ const rect = this.canvas.getBoundingClientRect();
86
+ const x = ((mouse.x - rect.left) / rect.width) * 2 - 1;
87
+ const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1;
88
+
89
+ const coords = new Vector2(x, y);
90
+ this.raycaster.setFromCamera(coords, this.camera);
91
+
92
+ this.raycaster.params = {
93
+ Mesh: {},
94
+ Line: { threshold: 0.05 },
95
+ Line2: { threshold: 0.05 },
96
+ LOD: {},
97
+ Points: { threshold: 0.01 },
98
+ Sprite: {},
99
+ };
100
+
101
+ let intersects = this.raycaster.intersectObjects(objects, false);
102
+
103
+ (this.renderer.clippingPlanes || []).forEach((plane: Plane) => {
104
+ intersects = intersects.filter((intersect) => plane.distanceToPoint(intersect.point) >= 0);
105
+ });
106
+
107
+ return intersects;
108
+ }
109
+
110
+ getDetectRadius(point: Vector3): number {
111
+ const camera: any = this.camera;
112
+
113
+ // ===================== AI-CODE-START ======================
114
+ // Source: Gemini 2.5 Pro
115
+ // Date: 2025-08-27
116
+ // Reviewer: roman.mochalov@opendesign.com
117
+ // Issue: CLOUD-5799
118
+ // Notes: Originally AI-generated, modified manually
119
+
120
+ if (camera.isOrthographicCamera) {
121
+ const fieldHeight = (camera.top - camera.bottom) / camera.zoom;
122
+
123
+ const canvasHeight = this.canvas.height;
124
+ const worldUnitsPerPixel = fieldHeight / canvasHeight;
125
+
126
+ return this.detectRadiusInPixels * worldUnitsPerPixel;
127
+ }
128
+
129
+ if (camera.isPerspectiveCamera) {
130
+ const distance = camera.position.distanceTo(point);
131
+ const fieldHeight = 2 * Math.tan(MathUtils.degToRad(camera.fov * 0.5)) * distance;
132
+
133
+ const canvasHeight = this.canvas.height;
134
+ const worldUnitsPerPixel = fieldHeight / canvasHeight;
135
+
136
+ return this.detectRadiusInPixels * worldUnitsPerPixel;
137
+ }
138
+
139
+ // ===================== AI-CODE-END ======================
140
+
141
+ return 0.1;
142
+ }
143
+
144
+ getSnapPoint(mouse: Vector2, objects: Object3D[]): Vector3 {
145
+ const intersections = this.getPointerIntersects(mouse, objects);
146
+ if (intersections.length === 0) return undefined;
147
+
148
+ // ===================== AI-CODE-START ======================
149
+ // Source: Gemini 2.5 Pro
150
+ // Date: 2025-08-20
151
+ // Reviewer: roman.mochalov@opendesign.com
152
+ // Issue: CLOUD-5799
153
+ // Notes: Originally AI-generated, modified manually
154
+
155
+ const object: any = intersections[0].object;
156
+ const intersectionPoint = intersections[0].point;
157
+ const localPoint = object.worldToLocal(intersectionPoint.clone());
158
+
159
+ let snapPoint: Vector3;
160
+ let snapDistance = this.getDetectRadius(intersectionPoint);
161
+
162
+ const geometry = object.geometry;
163
+
164
+ const positions = geometry.attributes.position.array;
165
+ for (let i = 0; i < positions.length; i += 3) {
166
+ _vertex.set(positions[i], positions[i + 1], positions[i + 2]);
167
+ const distance = _vertex.distanceTo(localPoint);
168
+ if (distance < snapDistance) {
169
+ snapDistance = distance;
170
+ snapPoint = _vertex.clone();
171
+ }
172
+ }
173
+ if (snapPoint) return object.localToWorld(snapPoint);
174
+
175
+ let edges = this.edgesCache.get(geometry);
176
+ if (!edges) {
177
+ edges = new EdgesGeometry(geometry);
178
+ this.edgesCache.set(geometry, edges);
179
+ }
180
+
181
+ const edgePositions = edges.attributes.position.array;
182
+ for (let i = 0; i < edgePositions.length; i += 6) {
183
+ _start.set(edgePositions[i], edgePositions[i + 1], edgePositions[i + 2]);
184
+ _end.set(edgePositions[i + 3], edgePositions[i + 4], edgePositions[i + 5]);
185
+ _line.set(_start, _end);
186
+
187
+ _line.getCenter(_center);
188
+ const centerDistance = _center.distanceTo(localPoint);
189
+ if (centerDistance < snapDistance) {
190
+ snapDistance = centerDistance;
191
+ snapPoint = _center.clone();
192
+ continue;
193
+ }
194
+
195
+ _line.closestPointToPoint(localPoint, true, _projection);
196
+ const lineDistance = _projection.distanceTo(localPoint);
197
+ if (lineDistance < snapDistance) {
198
+ snapDistance = lineDistance;
199
+ snapPoint = _projection.clone();
200
+ }
201
+ }
202
+ if (snapPoint) return object.localToWorld(snapPoint);
203
+
204
+ // ===================== AI-CODE-END ======================
205
+
206
+ return intersectionPoint.clone();
207
+ }
208
+ }
@@ -0,0 +1,47 @@
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
+ // Model units for the user to choose from.
25
+
26
+ export const ModelUnits = {
27
+ Meters: { name: "Meters", type: "m", scale: 1.0 },
28
+ Centimeters: { name: "Centimeters", type: "cm", scale: 0.01 },
29
+ Millimeters: { name: "Millimeters", type: "mm", scale: 0.001 },
30
+ Feet: { name: "Feet", type: "ft", scale: 0.3048 },
31
+ Inches: { name: "Inches", type: "in", scale: 0.0254 },
32
+ Yards: { name: "Yards", type: "yd", scale: 0.9144 },
33
+ Kilometers: { name: "Kilometers", type: "km", scale: 1000.0 },
34
+ Miles: { name: "Miles", type: "mi", scale: 1609.344 },
35
+ Micrometers: { name: "Micrometers", type: "µm", scale: 0.000001 },
36
+ Mils: { name: "Mils", type: "mil", scale: 0.0000254 },
37
+ MicroInches: { name: "Micro-inches", type: "µin", scale: 0.0000000254 },
38
+ Default: { name: "File units", type: "unit", scale: 1.0 },
39
+ };
40
+
41
+ // Convert distance from unit to unit.
42
+
43
+ export function convertUnits(fromUnits: string, toUnits: string, distance: number) {
44
+ const fromFactor = 1 / (ModelUnits[fromUnits] || ModelUnits.Default).scale;
45
+ const toFactor = (ModelUnits[toUnits] || ModelUnits.Default).scale || 1;
46
+ return distance * fromFactor * toFactor;
47
+ }
@@ -0,0 +1,95 @@
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 { ModelUnits } from "./UnitConverter";
25
+
26
+ // Returns a standard string representation of the display unit.
27
+
28
+ export function getDisplayUnit(units: string) {
29
+ return (ModelUnits[units] || ModelUnits.Default).type;
30
+ }
31
+
32
+ // ===================== AI-CODE-START ======================
33
+ // Source: Claude Sonnet 4.5
34
+ // Date: 2025-10-08
35
+ // Reviewer: roman.mochalov@opendesign.com
36
+ // Issue: CLOUD-5963
37
+
38
+ // Calculates recommended precision for small values (|value| < 1) to avoid showing "0.00".
39
+
40
+ export function calculatePrecision(value: number) {
41
+ const distance = Math.abs(value);
42
+
43
+ if (distance >= 1000) return 0;
44
+ if (distance >= 10) return 1;
45
+ if (distance >= 0.1) return 2;
46
+ if (distance >= 0.001) return 3;
47
+
48
+ return distance > 0 ? Math.floor(-Math.log10(distance)) + 1 : 2;
49
+ }
50
+
51
+ // Calculates the minimal distance for the sepecified precision.
52
+
53
+ export function calculateDistance(precision: any): number {
54
+ let digits: number;
55
+
56
+ if (precision === "Auto") digits = 2;
57
+ else if (Number.isFinite(precision)) digits = precision;
58
+ else digits = parseFloat(precision);
59
+
60
+ if (!Number.isFinite(digits) || digits < 0) digits = 2;
61
+ else if (digits > 10) digits = 10;
62
+
63
+ return Math.pow(10, -digits);
64
+ }
65
+
66
+ // ===================== AI-CODE-END ======================
67
+
68
+ // Formats a distance with units.
69
+
70
+ function formatNumber(distance: number, digits: number, precision: any) {
71
+ let result = distance.toFixed(digits);
72
+ if (precision === "Auto") result = result.replace(/\.0+$/, "").replace(/\.$/, "");
73
+ if (+result !== distance) result = "~ " + result;
74
+ return result;
75
+ }
76
+
77
+ export function formatDistance(distance: number, units: string, precision: any = 2) {
78
+ let digits: number;
79
+
80
+ if (precision === "Auto") digits = calculatePrecision(distance);
81
+ else if (Number.isFinite(precision)) digits = precision;
82
+ else digits = parseFloat(precision);
83
+
84
+ if (!Number.isFinite(digits)) digits = 2;
85
+ else if (digits < 0) digits = 0;
86
+ else if (digits > 10) digits = 10;
87
+
88
+ if (ModelUnits[units]) {
89
+ return formatNumber(distance, digits, precision) + " " + ModelUnits[units].type;
90
+ } else if (units) {
91
+ return formatNumber(distance, digits, precision) + " " + units;
92
+ } else {
93
+ return formatNumber(distance, digits, precision);
94
+ }
95
+ }
@@ -22,18 +22,23 @@
22
22
  ///////////////////////////////////////////////////////////////////////////////
23
23
 
24
24
  import { Box3, Object3D } from "three";
25
- import { ILoader, IViewer } from "@inweb/viewer-core";
25
+ import { IInfo, IModel } from "@inweb/viewer-core";
26
26
 
27
27
  /**
28
- * Model interface.
28
+ * Basic model implementation.
29
29
  */
30
- export interface IModelImpl {
31
- handle: string;
30
+ export interface IModelImpl extends IModel {
32
31
  scene: Object3D;
33
- loader: ILoader;
34
- viewer: IViewer;
35
32
 
36
- dispose(): void;
33
+ getUnits(): string;
34
+
35
+ getUnitScale(): number;
36
+
37
+ getUnitString(): string;
38
+
39
+ getPrecision(): number;
40
+
41
+ getInfo(): IInfo;
37
42
 
38
43
  getExtents(target: Box3): Box3;
39
44
 
@@ -41,10 +46,14 @@ export interface IModelImpl {
41
46
 
42
47
  getVisibleObjects(): Object3D[];
43
48
 
44
- hasObject(objects: Object3D): boolean;
49
+ hasObject(object: Object3D): boolean;
50
+
51
+ hasHandle(handle: string): boolean;
45
52
 
46
53
  getOwnObjects(objects: Object3D | Object3D[]): Object3D[];
47
54
 
55
+ getOwnHandles(handles: string | string[]): string[];
56
+
48
57
  getObjectsByHandles(handles: string | string[]): Object3D[];
49
58
 
50
59
  getHandlesByObjects(objects: Object3D | Object3D[]): string[];