@inweb/viewer-three 26.9.7 → 26.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inweb/viewer-three",
3
- "version": "26.9.7",
3
+ "version": "26.9.8",
4
4
  "description": "JavaScript library for rendering CAD and BIM files in a browser using Three.js",
5
5
  "homepage": "https://cloud.opendesign.com/docs/index.html",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -35,10 +35,10 @@
35
35
  "docs": "typedoc"
36
36
  },
37
37
  "dependencies": {
38
- "@inweb/client": "~26.9.7",
39
- "@inweb/eventemitter2": "~26.9.7",
40
- "@inweb/markup": "~26.9.7",
41
- "@inweb/viewer-core": "~26.9.7"
38
+ "@inweb/client": "~26.9.8",
39
+ "@inweb/eventemitter2": "~26.9.8",
40
+ "@inweb/markup": "~26.9.8",
41
+ "@inweb/viewer-core": "~26.9.8"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/three": "^0.179.0",
@@ -38,7 +38,7 @@ export function zoomTo(viewer: Viewer, box: Box3): void {
38
38
  if (camera.isPerspectiveCamera) {
39
39
  const offset = new Vector3(0, 0, 1)
40
40
  .applyQuaternion(camera.quaternion)
41
- .multiplyScalar(boxSize / Math.tan(MathUtils.DEG2RAD * camera.fov * 0.5));
41
+ .multiplyScalar(boxSize / Math.tan(MathUtils.degToRad(camera.fov * 0.5)));
42
42
 
43
43
  camera.position.copy(offset).add(boxCenter);
44
44
  camera.updateMatrixWorld();
@@ -105,12 +105,12 @@ export class SelectionComponent implements IComponent {
105
105
  const coords = new Vector2(x, y);
106
106
  this.raycaster.setFromCamera(coords, this.viewer.camera);
107
107
 
108
- this.raycaster.params = this.raycaster.params = {
108
+ this.raycaster.params = {
109
109
  Mesh: {},
110
- Line: { threshold: 0.25 },
111
- Line2: { threshold: 0.25 },
110
+ Line: { threshold: 0.05 },
111
+ Line2: { threshold: 0.05 },
112
112
  LOD: {},
113
- Points: { threshold: 0.1 },
113
+ Points: { threshold: 0.01 },
114
114
  Sprite: {},
115
115
  };
116
116
 
@@ -21,12 +21,26 @@
21
21
  // acknowledge and accept the above terms.
22
22
  ///////////////////////////////////////////////////////////////////////////////
23
23
 
24
- import { Camera, Matrix4, Object3D, Scene, Raycaster, Vector2, Vector3, Vector4 } from "three";
24
+ import {
25
+ Camera,
26
+ EdgesGeometry,
27
+ Intersection,
28
+ Line3,
29
+ MathUtils,
30
+ Matrix4,
31
+ Object3D,
32
+ Raycaster,
33
+ Vector2,
34
+ Vector3,
35
+ Vector4,
36
+ } from "three";
25
37
 
26
38
  import type { Viewer } from "../Viewer";
27
39
  import { OrbitDragger } from "./OrbitDragger";
28
40
 
29
41
  const PRECISION = 0.01;
42
+ const DESKTOP_SNAP_DISTANCE = 10;
43
+ const MOBILE_SNAP_DISTANCE = 50;
30
44
 
31
45
  export class MeasureLineDragger extends OrbitDragger {
32
46
  private overlay: MeasureOverlay;
@@ -43,7 +57,7 @@ export class MeasureLineDragger extends OrbitDragger {
43
57
  this.overlay.addLine(this.line);
44
58
 
45
59
  this.snapper = new MeasureSnapper(viewer.camera, viewer.canvas);
46
- this.snapper.update(viewer.scene);
60
+ this.updateSnapper();
47
61
 
48
62
  this.viewer.canvas.addEventListener("pointerdown", this.onPointerDown);
49
63
  this.viewer.canvas.addEventListener("pointermove", this.onPointerMove);
@@ -71,6 +85,8 @@ export class MeasureLineDragger extends OrbitDragger {
71
85
  this.viewer.removeEventListener("show", this.updateSnapper);
72
86
  this.viewer.removeEventListener("showall", this.updateSnapper);
73
87
 
88
+ this.snapper.dispose();
89
+
74
90
  this.overlay.detach();
75
91
  this.overlay.dispose();
76
92
 
@@ -90,7 +106,10 @@ export class MeasureLineDragger extends OrbitDragger {
90
106
  onPointerMove = (event: PointerEvent) => {
91
107
  if (this.orbit.enabled && this.orbit.state !== -1) return;
92
108
 
93
- this.line.endPoint = this.snapper.getSnapPoint(event);
109
+ const snapPoint = this.snapper.getSnapPoint(event);
110
+ if (snapPoint && this.line.endPoint && snapPoint.equals(this.line.endPoint)) return;
111
+
112
+ this.line.endPoint = snapPoint;
94
113
  this.line.render();
95
114
 
96
115
  if (this.line.startPoint) this.changed = true; // <- to prevent context menu
@@ -124,25 +143,57 @@ export class MeasureLineDragger extends OrbitDragger {
124
143
  };
125
144
 
126
145
  updateSnapper = () => {
127
- this.snapper.update(this.viewer.scene);
146
+ this.snapper.update(this.viewer);
128
147
  };
129
148
  }
130
149
 
150
+ const _vertex = new Vector3();
151
+ const _start = new Vector3();
152
+ const _end = new Vector3();
153
+ const _line = new Line3();
154
+ const _center = new Vector3();
155
+ const _projection = new Vector3();
156
+
131
157
  class MeasureSnapper {
132
158
  private camera: Camera;
133
159
  private canvas: HTMLCanvasElement;
134
- private objects: Object3D[] = [];
160
+ private objects: Object3D[];
135
161
  private raycaster: Raycaster;
162
+ private detectRadiusInPixels: number;
163
+ private edgesCache: WeakMap<any, EdgesGeometry>;
136
164
 
137
165
  constructor(camera: Camera, canvas: HTMLCanvasElement) {
138
166
  this.camera = camera;
139
167
  this.canvas = canvas;
168
+ this.objects = [];
140
169
  this.raycaster = new Raycaster();
170
+ this.detectRadiusInPixels = this.isMobile() ? MOBILE_SNAP_DISTANCE : DESKTOP_SNAP_DISTANCE;
171
+ this.edgesCache = new WeakMap();
172
+ }
173
+
174
+ dispose() {
175
+ this.objects = [];
176
+ }
177
+
178
+ isMobile(): boolean {
179
+ if (typeof navigator === "undefined") return false;
180
+
181
+ // ===================== AI-CODE-START ======================
182
+ // Source: Claude Sonnet 4
183
+ // Date: 2025-09-09
184
+ // Reviewer: roman.mochalov@opendesign.com
185
+ // Issue: CLOUD-5799
186
+
187
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
188
+
189
+ // ===================== AI-CODE-END ======================
141
190
  }
142
191
 
143
- getSnapPoint(event: PointerEvent): Vector3 | undefined {
144
- const mouse = new Vector2(event.clientX, event.clientY);
192
+ getMousePosition(event: MouseEvent, target: Vector2): Vector2 {
193
+ return target.set(event.clientX, event.clientY);
194
+ }
145
195
 
196
+ getPointerIntersects(mouse: Vector2, objects: Object3D[]): Array<Intersection<Object3D>> {
146
197
  const rect = this.canvas.getBoundingClientRect();
147
198
  const x = ((mouse.x - rect.left) / rect.width) * 2 - 1;
148
199
  const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1;
@@ -152,22 +203,122 @@ class MeasureSnapper {
152
203
 
153
204
  this.raycaster.params = {
154
205
  Mesh: {},
155
- Line: { threshold: 0.25 },
156
- Line2: { threshold: 0.25 },
206
+ Line: { threshold: 0.05 },
207
+ Line2: { threshold: 0.05 },
157
208
  LOD: {},
158
- Points: { threshold: 0.1 },
209
+ Points: { threshold: 0.01 },
159
210
  Sprite: {},
160
211
  };
161
212
 
162
- const intersects = this.raycaster.intersectObjects(this.objects, false);
163
- if (intersects.length === 0) return undefined;
213
+ return this.raycaster.intersectObjects(objects, false);
214
+ }
215
+
216
+ getDetectRadius(point: Vector3): number {
217
+ const camera: any = this.camera;
218
+
219
+ // ===================== AI-CODE-START ======================
220
+ // Source: Gemini 2.5 Pro
221
+ // Date: 2025-08-27
222
+ // Reviewer: roman.mochalov@opendesign.com
223
+ // Issue: CLOUD-5799
224
+ // Notes: Originally AI-generated, modified manually
225
+
226
+ if (camera.isOrthographicCamera) {
227
+ const worldHeight = camera.top - camera.bottom;
228
+
229
+ const canvasHeight = this.canvas.height;
230
+ const worldUnitsPerPixel = worldHeight / canvasHeight;
231
+
232
+ return this.detectRadiusInPixels * worldUnitsPerPixel;
233
+ }
234
+
235
+ if (camera.isPerspectiveCamera) {
236
+ const distance = camera.position.distanceTo(point);
237
+ const worldHeight = 2 * Math.tan(MathUtils.degToRad(camera.fov * 0.5)) * distance;
238
+
239
+ const canvasHeight = this.canvas.height;
240
+ const worldUnitsPerPixel = worldHeight / canvasHeight;
241
+
242
+ return this.detectRadiusInPixels * worldUnitsPerPixel;
243
+ }
244
+
245
+ // ===================== AI-CODE-END ======================
164
246
 
165
- return intersects[0].point;
247
+ return 0.1;
166
248
  }
167
249
 
168
- update(scene: Scene) {
169
- this.objects = [];
170
- scene.traverseVisible((child) => this.objects.push(child));
250
+ getSnapPoint(event: PointerEvent): Vector3 {
251
+ const mouse = this.getMousePosition(event, new Vector2());
252
+
253
+ const intersections = this.getPointerIntersects(mouse, this.objects);
254
+ if (intersections.length === 0) return undefined;
255
+
256
+ // ===================== AI-CODE-START ======================
257
+ // Source: Gemini 2.5 Pro
258
+ // Date: 2025-08-20
259
+ // Reviewer: roman.mochalov@opendesign.com
260
+ // Issue: CLOUD-5799
261
+ // Notes: Originally AI-generated, modified manually
262
+
263
+ const object: any = intersections[0].object;
264
+ const intersectionPoint = intersections[0].point;
265
+ const localPoint = object.worldToLocal(intersectionPoint.clone());
266
+
267
+ let snapPoint: Vector3;
268
+ let snapDistance = this.getDetectRadius(intersectionPoint);
269
+
270
+ const geometry = object.geometry;
271
+
272
+ const positions = geometry.attributes.position.array;
273
+ for (let i = 0; i < positions.length; i += 3) {
274
+ _vertex.set(positions[i], positions[i + 1], positions[i + 2]);
275
+ const distance = _vertex.distanceTo(localPoint);
276
+ if (distance < snapDistance) {
277
+ snapDistance = distance;
278
+ snapPoint = _vertex.clone();
279
+ }
280
+ }
281
+ if (snapPoint) return object.localToWorld(snapPoint);
282
+
283
+ let edges = this.edgesCache.get(geometry);
284
+ if (!edges) {
285
+ edges = new EdgesGeometry(geometry);
286
+ this.edgesCache.set(geometry, edges);
287
+ }
288
+
289
+ const edgePositions = edges.attributes.position.array;
290
+ for (let i = 0; i < edgePositions.length; i += 6) {
291
+ _start.set(edgePositions[i], edgePositions[i + 1], edgePositions[i + 2]);
292
+ _end.set(edgePositions[i + 3], edgePositions[i + 4], edgePositions[i + 5]);
293
+ _line.set(_start, _end);
294
+
295
+ _line.getCenter(_center);
296
+ const centerDistance = _center.distanceTo(localPoint);
297
+ if (centerDistance < snapDistance) {
298
+ snapDistance = centerDistance;
299
+ snapPoint = _center.clone();
300
+ continue;
301
+ }
302
+
303
+ _line.closestPointToPoint(localPoint, true, _projection);
304
+ const lineDistance = _projection.distanceTo(localPoint);
305
+ if (lineDistance < snapDistance) {
306
+ snapDistance = lineDistance;
307
+ snapPoint = _projection.clone();
308
+ }
309
+ }
310
+ if (snapPoint) return object.localToWorld(snapPoint);
311
+
312
+ // ===================== AI-CODE-END ======================
313
+
314
+ return intersectionPoint.clone();
315
+ }
316
+
317
+ update(viewer: Viewer) {
318
+ this.objects.length = 0;
319
+ viewer.models.forEach((model) => {
320
+ model.getVisibleObjects().forEach((object) => this.objects.push(object));
321
+ });
171
322
  }
172
323
  }
173
324
 
@@ -197,6 +348,8 @@ class MeasureOverlay {
197
348
  this.container.style.pointerEvents = "none";
198
349
  this.container.style.overflow = "hidden";
199
350
 
351
+ if (!this.canvas.parentElement) return;
352
+
200
353
  this.canvas.parentElement.appendChild(this.container);
201
354
  }
202
355
 
@@ -245,7 +398,7 @@ class MeasureLine {
245
398
  public startPoint: Vector3;
246
399
  public endPoint: Vector3;
247
400
 
248
- public id = Date.now();
401
+ public id = MathUtils.generateUUID();
249
402
  public unit = "";
250
403
  public scale = 1.0;
251
404
  public size = 10.0;
@@ -275,6 +428,11 @@ class MeasureLine {
275
428
  this.elementEndPoint.remove();
276
429
  this.elementLine.remove();
277
430
  this.elementLabel.remove();
431
+
432
+ this.elementStartPoint = undefined;
433
+ this.elementEndPoint = undefined;
434
+ this.elementLine = undefined;
435
+ this.elementLabel = undefined;
278
436
  }
279
437
 
280
438
  render() {