@inweb/viewer-three 25.11.2 → 25.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.
@@ -0,0 +1,464 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2002-2024, 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-2024 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 { Camera, Matrix4, Object3D, Scene, Raycaster, Vector2, Vector3, Vector4 } from "three";
25
+
26
+ import type { Viewer } from "../Viewer";
27
+ import { OrbitDragger } from "./OrbitDragger";
28
+
29
+ const PRECISION = 0.01;
30
+
31
+ export class MeasureLineDragger extends OrbitDragger {
32
+ private overlay: MeasureOverlay;
33
+ private line: MeasureLine;
34
+ private snapper: MeasureSnapper;
35
+
36
+ constructor(viewer: Viewer) {
37
+ super(viewer);
38
+
39
+ this.overlay = new MeasureOverlay(viewer.camera, viewer.canvas);
40
+ this.overlay.attach();
41
+
42
+ this.line = new MeasureLine(this.overlay);
43
+ this.overlay.addLine(this.line);
44
+
45
+ this.snapper = new MeasureSnapper(viewer.camera, viewer.canvas);
46
+ this.snapper.update(viewer.scene);
47
+
48
+ this.viewer.canvas.addEventListener("pointerdown", this.onPointerDown);
49
+ this.viewer.canvas.addEventListener("pointermove", this.onPointerMove);
50
+ this.viewer.canvas.addEventListener("pointerup", this.onPointerUp);
51
+ this.viewer.canvas.addEventListener("pointercancel", this.onPointerCancel);
52
+ this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave);
53
+
54
+ this.viewer.addEventListener("render", this.renderOverlay);
55
+ this.viewer.addEventListener("hide", this.updateSnapper);
56
+ this.viewer.addEventListener("isolate", this.updateSnapper);
57
+ this.viewer.addEventListener("showall", this.updateSnapper);
58
+ }
59
+
60
+ override dispose() {
61
+ this.viewer.canvas.removeEventListener("pointerdown", this.onPointerDown);
62
+ this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove);
63
+ this.viewer.canvas.removeEventListener("pointerup", this.onPointerUp);
64
+ this.viewer.canvas.removeEventListener("pointercancel", this.onPointerCancel);
65
+ this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave);
66
+
67
+ this.viewer.removeEventListener("render", this.renderOverlay);
68
+ this.viewer.removeEventListener("hide", this.updateSnapper);
69
+ this.viewer.removeEventListener("isolate", this.updateSnapper);
70
+ this.viewer.removeEventListener("showall", this.updateSnapper);
71
+
72
+ this.overlay.detach();
73
+ this.overlay.dispose();
74
+
75
+ super.dispose();
76
+ }
77
+
78
+ onPointerDown = (event: PointerEvent) => {
79
+ if (event.button !== 0) return;
80
+
81
+ this.line.startPoint = this.snapper.getSnapPoint(event);
82
+ this.line.render();
83
+
84
+ this.viewer.canvas.setPointerCapture(event.pointerId);
85
+ this.orbit.enabled = !this.line.startPoint;
86
+ };
87
+
88
+ onPointerMove = (event: PointerEvent) => {
89
+ if (this.orbit.enabled && this.orbit.state !== -1) return;
90
+
91
+ this.line.endPoint = this.snapper.getSnapPoint(event);
92
+ this.line.render();
93
+
94
+ if (this.line.startPoint) this.changed = true; // <- to prevent context menu
95
+ };
96
+
97
+ onPointerUp = (event: PointerEvent) => {
98
+ if (this.line.startPoint && this.line.endPoint && this.line.getDistance() >= PRECISION) {
99
+ this.line = new MeasureLine(this.overlay);
100
+ this.overlay.addLine(this.line);
101
+ } else {
102
+ this.line.startPoint = undefined;
103
+ this.line.endPoint = undefined;
104
+ this.line.render();
105
+ }
106
+
107
+ this.viewer.canvas.releasePointerCapture(event.pointerId);
108
+ this.orbit.enabled = true;
109
+ };
110
+
111
+ onPointerCancel = (event: PointerEvent) => {
112
+ this.viewer.canvas.dispatchEvent(new PointerEvent("pointerup", event));
113
+ };
114
+
115
+ onPointerLeave = () => {
116
+ this.line.endPoint = undefined;
117
+ this.line.render();
118
+ };
119
+
120
+ renderOverlay = () => {
121
+ this.overlay.render();
122
+ };
123
+
124
+ updateSnapper = () => {
125
+ this.snapper.update(this.viewer.scene);
126
+ };
127
+ }
128
+
129
+ class MeasureSnapper {
130
+ private camera: Camera;
131
+ private canvas: HTMLCanvasElement;
132
+ private objects: Object3D[] = [];
133
+ private raycaster: Raycaster;
134
+
135
+ constructor(camera: Camera, canvas: HTMLCanvasElement) {
136
+ this.camera = camera;
137
+ this.canvas = canvas;
138
+ this.raycaster = new Raycaster();
139
+ }
140
+
141
+ getSnapPoint(event: PointerEvent): Vector3 | undefined {
142
+ const mouse = new Vector2(event.clientX, event.clientY);
143
+
144
+ const rect = this.canvas.getBoundingClientRect();
145
+ const x = ((mouse.x - rect.left) / rect.width) * 2 - 1;
146
+ const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1;
147
+
148
+ const coords = new Vector2(x, y);
149
+ this.raycaster.setFromCamera(coords, this.camera);
150
+
151
+ this.raycaster.params = {
152
+ Mesh: {},
153
+ Line: { threshold: 0.25 },
154
+ Line2: { threshold: 0.25 },
155
+ LOD: {},
156
+ Points: { threshold: 0.1 },
157
+ Sprite: {},
158
+ };
159
+
160
+ const intersects = this.raycaster.intersectObjects(this.objects, false);
161
+ if (intersects.length === 0) return undefined;
162
+
163
+ return intersects[0].point;
164
+ }
165
+
166
+ update(scene: Scene) {
167
+ this.objects = [];
168
+ scene.traverseVisible((child) => this.objects.push(child));
169
+ }
170
+ }
171
+
172
+ class MeasureOverlay {
173
+ public camera: Camera;
174
+ public canvas: HTMLCanvasElement;
175
+ public container: HTMLElement;
176
+ public lines: MeasureLine[] = [];
177
+ public projector: MeasureProjector;
178
+
179
+ constructor(camera: Camera, canvas: HTMLCanvasElement) {
180
+ this.camera = camera;
181
+ this.canvas = canvas;
182
+ this.projector = new MeasureProjector(camera, canvas);
183
+ }
184
+
185
+ attach() {
186
+ this.container = document.createElement("div");
187
+ this.container.id = "measure-container";
188
+ this.container.style.background = "rgba(0,0,0,0)";
189
+ this.container.style.position = "absolute";
190
+ this.container.style.top = "0px";
191
+ this.container.style.left = "0px";
192
+ this.container.style.width = "100%";
193
+ this.container.style.height = "100%";
194
+ this.container.style.outline = "none";
195
+ this.container.style.pointerEvents = "none";
196
+ this.container.style.overflow = "hidden";
197
+
198
+ this.canvas.parentElement.appendChild(this.container);
199
+ }
200
+
201
+ dispose() {
202
+ this.clear();
203
+ }
204
+
205
+ detach() {
206
+ this.container.remove();
207
+ this.container = undefined;
208
+ }
209
+
210
+ clear() {
211
+ this.lines.forEach((line) => line.dispose());
212
+ this.lines = [];
213
+ }
214
+
215
+ render() {
216
+ this.projector.updateProjectionMatrix();
217
+ this.lines.forEach((line) => line.render());
218
+ }
219
+
220
+ update() {
221
+ this.lines.forEach((line) => line.update());
222
+ }
223
+
224
+ addLine(line: MeasureLine) {
225
+ this.lines.push(line);
226
+ }
227
+
228
+ removeLine(line: MeasureLine) {
229
+ this.lines = this.lines.filter((x) => x !== line);
230
+ }
231
+ }
232
+
233
+ const _middlePoint = new Vector3();
234
+
235
+ class MeasureLine {
236
+ private overlay: MeasureOverlay;
237
+
238
+ private elementStartPoint: HTMLElement;
239
+ private elementEndPoint: HTMLElement;
240
+ private elementLine: HTMLElement;
241
+ private elementLabel: HTMLElement;
242
+
243
+ public startPoint: Vector3;
244
+ public endPoint: Vector3;
245
+
246
+ public id = Date.now();
247
+ public unit = "";
248
+ public scale = 1.0;
249
+ public size = 10.0;
250
+ public lineWidth = 2;
251
+
252
+ public style = {
253
+ border: "2px solid #FFFFFF",
254
+ background: "#009bff",
255
+ boxShadow: "0 0 10px rgba(0,0,0,0.5)",
256
+ color: "white",
257
+ font: "1rem system-ui",
258
+ };
259
+
260
+ constructor(overlay: MeasureOverlay) {
261
+ this.overlay = overlay;
262
+
263
+ this.elementStartPoint = overlay.container.appendChild(document.createElement("div"));
264
+ this.elementEndPoint = overlay.container.appendChild(document.createElement("div"));
265
+ this.elementLine = overlay.container.appendChild(document.createElement("div"));
266
+ this.elementLabel = overlay.container.appendChild(document.createElement("div"));
267
+
268
+ this.update();
269
+ }
270
+
271
+ dispose() {
272
+ this.elementStartPoint.remove();
273
+ this.elementEndPoint.remove();
274
+ this.elementLine.remove();
275
+ this.elementLabel.remove();
276
+ }
277
+
278
+ render() {
279
+ const projector = this.overlay.projector;
280
+
281
+ if (this.startPoint) {
282
+ const { point, visible } = projector.projectPoint(this.startPoint);
283
+
284
+ this.elementStartPoint.style.display = visible ? "block" : "none";
285
+ this.elementStartPoint.style.left = `${point.x}px`;
286
+ this.elementStartPoint.style.top = `${point.y}px`;
287
+ } else {
288
+ this.elementStartPoint.style.display = "none";
289
+ }
290
+
291
+ if (this.endPoint) {
292
+ const { point, visible } = projector.projectPoint(this.endPoint);
293
+
294
+ this.elementEndPoint.style.display = visible ? "block" : "none";
295
+ this.elementEndPoint.style.left = `${point.x}px`;
296
+ this.elementEndPoint.style.top = `${point.y}px`;
297
+ } else {
298
+ this.elementEndPoint.style.display = "none";
299
+ }
300
+
301
+ if (this.startPoint && this.endPoint) {
302
+ const { point1, point2, visible } = projector.projectLine(this.startPoint, this.endPoint);
303
+
304
+ point2.sub(point1);
305
+ const angle = point2.angle();
306
+ const width = point2.length();
307
+
308
+ this.elementLine.style.display = visible ? "block" : "none";
309
+ this.elementLine.style.left = `${point1.x}px`;
310
+ this.elementLine.style.top = `${point1.y}px`;
311
+ this.elementLine.style.width = `${width}px`;
312
+ this.elementLine.style.transform = `translate(0px, ${-this.lineWidth / 2}px) rotate(${angle}rad)`;
313
+ } else {
314
+ this.elementLine.style.display = "none";
315
+ }
316
+
317
+ if (this.startPoint && this.endPoint) {
318
+ _middlePoint.lerpVectors(this.startPoint, this.endPoint, 0.5);
319
+ const { point, visible } = projector.projectPoint(_middlePoint);
320
+
321
+ const distance = this.getDistance();
322
+
323
+ this.elementLabel.style.display = visible && distance >= PRECISION ? "block" : "none";
324
+ this.elementLabel.style.left = `${point.x}px`;
325
+ this.elementLabel.style.top = `${point.y}px`;
326
+ this.elementLabel.innerHTML = `${distance.toFixed(2)} ${this.unit}`;
327
+ } else {
328
+ this.elementLabel.style.display = "none";
329
+ }
330
+ }
331
+
332
+ update() {
333
+ this.elementStartPoint.id = `markup-dot-start-${this.id}`;
334
+ this.elementStartPoint.style.position = "absolute";
335
+ this.elementStartPoint.style.zIndex = "2";
336
+ this.elementStartPoint.style.width = `${this.size}px`;
337
+ this.elementStartPoint.style.height = `${this.size}px`;
338
+ this.elementStartPoint.style.border = this.style.border;
339
+ this.elementStartPoint.style.borderRadius = `${this.size}px`;
340
+ this.elementStartPoint.style.background = this.style.background;
341
+ this.elementStartPoint.style.boxShadow = this.style.boxShadow;
342
+ this.elementStartPoint.style.transform = "translate(-50%, -50%)";
343
+
344
+ this.elementEndPoint.id = `markup-dot-end-${this.id}`;
345
+ this.elementEndPoint.style.position = "absolute";
346
+ this.elementEndPoint.style.zIndex = "2";
347
+ this.elementEndPoint.style.width = `${this.size}px`;
348
+ this.elementEndPoint.style.height = `${this.size}px`;
349
+ this.elementEndPoint.style.border = this.style.border;
350
+ this.elementEndPoint.style.borderRadius = `${this.size}px`;
351
+ this.elementEndPoint.style.background = this.style.background;
352
+ this.elementEndPoint.style.boxShadow = this.style.boxShadow;
353
+ this.elementEndPoint.style.transform = "translate(-50%, -50%)";
354
+
355
+ this.elementLine.id = `markup-line-${this.id}`;
356
+ this.elementLine.style.position = "absolute";
357
+ this.elementLine.style.zIndex = "1";
358
+ this.elementLine.style.height = `${this.lineWidth}px`;
359
+ this.elementLine.style.background = this.style.background;
360
+ this.elementLine.style.boxShadow = this.style.boxShadow;
361
+ this.elementLine.style.transformOrigin = `0px ${this.lineWidth / 2}px`;
362
+
363
+ this.elementLabel.id = `markup-label-${this.id}`;
364
+ this.elementLabel.style.position = "absolute";
365
+ this.elementLabel.style.zIndex = "3";
366
+ this.elementLabel.style.padding = "2px";
367
+ this.elementLabel.style.paddingInline = "5px";
368
+ this.elementLabel.style.borderRadius = "5px";
369
+ this.elementLabel.style.background = this.style.background;
370
+ this.elementLabel.style.boxShadow = this.style.boxShadow;
371
+ this.elementLabel.style.color = this.style.color;
372
+ this.elementLabel.style.font = this.style.font;
373
+ this.elementLabel.style.transform = "translate(-50%, -50%)";
374
+ }
375
+
376
+ getDistance(): number {
377
+ return this.startPoint.distanceTo(this.endPoint) / this.scale;
378
+ }
379
+ }
380
+
381
+ let _widthHalf: number;
382
+ let _heightHalf: number;
383
+ const _viewMatrix = new Matrix4();
384
+ const _viewProjectionMatrix = new Matrix4();
385
+ const _vector = new Vector3();
386
+ const _vector1 = new Vector4();
387
+ const _vector2 = new Vector4();
388
+ const point = new Vector2();
389
+ const point1 = new Vector2();
390
+ const point2 = new Vector2();
391
+
392
+ class MeasureProjector {
393
+ private camera: Camera;
394
+ private canvas: HTMLElement;
395
+
396
+ constructor(camera: Camera, canvas: HTMLCanvasElement) {
397
+ this.camera = camera;
398
+ this.canvas = canvas;
399
+ }
400
+
401
+ updateProjectionMatrix() {
402
+ const rect = this.canvas.getBoundingClientRect();
403
+ _widthHalf = rect.width / 2;
404
+ _heightHalf = rect.height / 2;
405
+
406
+ _viewMatrix.copy(this.camera.matrixWorldInverse);
407
+ _viewProjectionMatrix.multiplyMatrices(this.camera.projectionMatrix, _viewMatrix);
408
+ }
409
+
410
+ projectPoint(p: Vector3) {
411
+ _vector.copy(p).applyMatrix4(_viewProjectionMatrix);
412
+ const visible = _vector.z >= -1 && _vector.z <= 1;
413
+
414
+ point.x = (_vector.x + 1) * _widthHalf;
415
+ point.y = (-_vector.y + 1) * _heightHalf;
416
+
417
+ return { point, visible };
418
+ }
419
+
420
+ projectLine(p1: Vector3, p2: Vector3) {
421
+ let visible: boolean;
422
+
423
+ _vector1.copy(p1 as any).applyMatrix4(_viewProjectionMatrix);
424
+ _vector2.copy(p2 as any).applyMatrix4(_viewProjectionMatrix);
425
+
426
+ // see three/examples/jsm/renderers/Projector.js/clipLine for more details
427
+
428
+ const bc1near = _vector1.z + _vector1.w;
429
+ const bc2near = _vector2.z + _vector2.w;
430
+ const bc1far = -_vector1.z + _vector1.w;
431
+ const bc2far = -_vector2.z + _vector2.w;
432
+
433
+ if (bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0) visible = true;
434
+ else if ((bc1near < 0 && bc2near < 0) || (bc1far < 0 && bc2far < 0)) visible = false;
435
+ else {
436
+ let alpha1 = 0;
437
+ let alpha2 = 1;
438
+
439
+ if (bc1near < 0) alpha1 = Math.max(alpha1, bc1near / (bc1near - bc2near));
440
+ else if (bc2near < 0) alpha2 = Math.min(alpha2, bc1near / (bc1near - bc2near));
441
+
442
+ if (bc1far < 0) alpha1 = Math.max(alpha1, bc1far / (bc1far - bc2far));
443
+ else if (bc2far < 0) alpha2 = Math.min(alpha2, bc1far / (bc1far - bc2far));
444
+
445
+ visible = alpha2 >= alpha1;
446
+
447
+ if (visible) {
448
+ _vector1.lerp(_vector2, alpha1);
449
+ _vector2.lerp(_vector1, 1 - alpha2);
450
+ }
451
+ }
452
+
453
+ _vector1.multiplyScalar(1 / _vector1.w);
454
+ _vector2.multiplyScalar(1 / _vector2.w);
455
+
456
+ point1.x = (_vector1.x + 1) * _widthHalf;
457
+ point1.y = (-_vector1.y + 1) * _heightHalf;
458
+
459
+ point2.x = (_vector2.x + 1) * _widthHalf;
460
+ point2.y = (-_vector2.y + 1) * _heightHalf;
461
+
462
+ return { point1, point2, visible };
463
+ }
464
+ }