@tonybfox/threejs-tools 1.0.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 (80) hide show
  1. package/README.md +321 -0
  2. package/dist/asset-loader/index.cjs +376 -0
  3. package/dist/asset-loader/index.cjs.map +1 -0
  4. package/dist/asset-loader/index.d.mts +101 -0
  5. package/dist/asset-loader/index.d.ts +101 -0
  6. package/dist/asset-loader/index.mjs +7 -0
  7. package/dist/asset-loader/index.mjs.map +1 -0
  8. package/dist/camera/index.cjs +313 -0
  9. package/dist/camera/index.cjs.map +1 -0
  10. package/dist/camera/index.d.mts +82 -0
  11. package/dist/camera/index.d.ts +82 -0
  12. package/dist/camera/index.mjs +7 -0
  13. package/dist/camera/index.mjs.map +1 -0
  14. package/dist/chunk-5DP6WDB3.mjs +1161 -0
  15. package/dist/chunk-5DP6WDB3.mjs.map +1 -0
  16. package/dist/chunk-BJKSICFA.mjs +1579 -0
  17. package/dist/chunk-BJKSICFA.mjs.map +1 -0
  18. package/dist/chunk-BYRZCHE7.mjs +277 -0
  19. package/dist/chunk-BYRZCHE7.mjs.map +1 -0
  20. package/dist/chunk-EIROAPF7.mjs +387 -0
  21. package/dist/chunk-EIROAPF7.mjs.map +1 -0
  22. package/dist/chunk-EQDOX34V.mjs +164 -0
  23. package/dist/chunk-EQDOX34V.mjs.map +1 -0
  24. package/dist/chunk-IIAZ2WJJ.mjs +405 -0
  25. package/dist/chunk-IIAZ2WJJ.mjs.map +1 -0
  26. package/dist/chunk-L4VIIJZD.mjs +340 -0
  27. package/dist/chunk-L4VIIJZD.mjs.map +1 -0
  28. package/dist/chunk-P35QJCOG.mjs +339 -0
  29. package/dist/chunk-P35QJCOG.mjs.map +1 -0
  30. package/dist/chunk-R64RVBRM.mjs +394 -0
  31. package/dist/chunk-R64RVBRM.mjs.map +1 -0
  32. package/dist/compass/index.cjs +375 -0
  33. package/dist/compass/index.cjs.map +1 -0
  34. package/dist/compass/index.d.mts +58 -0
  35. package/dist/compass/index.d.ts +58 -0
  36. package/dist/compass/index.mjs +7 -0
  37. package/dist/compass/index.mjs.map +1 -0
  38. package/dist/grid/index.cjs +200 -0
  39. package/dist/grid/index.cjs.map +1 -0
  40. package/dist/grid/index.d.mts +43 -0
  41. package/dist/grid/index.d.ts +43 -0
  42. package/dist/grid/index.mjs +7 -0
  43. package/dist/grid/index.mjs.map +1 -0
  44. package/dist/index.cjs +5049 -0
  45. package/dist/index.cjs.map +1 -0
  46. package/dist/index.d.mts +13 -0
  47. package/dist/index.d.ts +13 -0
  48. package/dist/index.mjs +47 -0
  49. package/dist/index.mjs.map +1 -0
  50. package/dist/measurements/index.cjs +1198 -0
  51. package/dist/measurements/index.cjs.map +1 -0
  52. package/dist/measurements/index.d.mts +449 -0
  53. package/dist/measurements/index.d.ts +449 -0
  54. package/dist/measurements/index.mjs +9 -0
  55. package/dist/measurements/index.mjs.map +1 -0
  56. package/dist/sunlight/index.cjs +441 -0
  57. package/dist/sunlight/index.cjs.map +1 -0
  58. package/dist/sunlight/index.d.mts +92 -0
  59. package/dist/sunlight/index.d.ts +92 -0
  60. package/dist/sunlight/index.mjs +7 -0
  61. package/dist/sunlight/index.mjs.map +1 -0
  62. package/dist/terrain/index.cjs +423 -0
  63. package/dist/terrain/index.cjs.map +1 -0
  64. package/dist/terrain/index.d.mts +219 -0
  65. package/dist/terrain/index.d.ts +219 -0
  66. package/dist/terrain/index.mjs +7 -0
  67. package/dist/terrain/index.mjs.map +1 -0
  68. package/dist/transform-controls/index.cjs +1587 -0
  69. package/dist/transform-controls/index.cjs.map +1 -0
  70. package/dist/transform-controls/index.d.mts +162 -0
  71. package/dist/transform-controls/index.d.ts +162 -0
  72. package/dist/transform-controls/index.mjs +13 -0
  73. package/dist/transform-controls/index.mjs.map +1 -0
  74. package/dist/view-helper/index.cjs +430 -0
  75. package/dist/view-helper/index.cjs.map +1 -0
  76. package/dist/view-helper/index.d.mts +75 -0
  77. package/dist/view-helper/index.d.ts +75 -0
  78. package/dist/view-helper/index.mjs +7 -0
  79. package/dist/view-helper/index.mjs.map +1 -0
  80. package/package.json +124 -0
@@ -0,0 +1,1198 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // packages/measurements/src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ MeasurementTool: () => MeasurementTool,
34
+ SnapMode: () => SnapMode
35
+ });
36
+ module.exports = __toCommonJS(src_exports);
37
+
38
+ // packages/measurements/src/MeasurementTool.ts
39
+ var THREE = __toESM(require("three"));
40
+ var import_CSS2DRenderer = require("three/examples/jsm/renderers/CSS2DRenderer");
41
+
42
+ // packages/measurements/src/MeasurementTypes.ts
43
+ var SnapMode = /* @__PURE__ */ ((SnapMode2) => {
44
+ SnapMode2["VERTEX"] = "vertex";
45
+ SnapMode2["FACE"] = "face";
46
+ SnapMode2["EDGE"] = "edge";
47
+ SnapMode2["DISABLED"] = "disabled";
48
+ return SnapMode2;
49
+ })(SnapMode || {});
50
+
51
+ // packages/measurements/src/MeasurementTool.ts
52
+ var MeasurementTool = class extends THREE.EventDispatcher {
53
+ constructor(scene, camera, options = {}) {
54
+ super();
55
+ this.measurements = [];
56
+ this.raycaster = new THREE.Raycaster();
57
+ // Interactive mode properties
58
+ this.isInteractive = false;
59
+ this.domElement = null;
60
+ this.controls = null;
61
+ this.defaultTargets = [];
62
+ this.activeTargets = [];
63
+ this.currentMeasurement = null;
64
+ this.activeInteractionOptions = null;
65
+ this.pendingMeasurementOptions = null;
66
+ this.previewLine = null;
67
+ this.previewLabel = null;
68
+ this.snapMarker = null;
69
+ this.originalCursor = "";
70
+ this.cursorHidden = false;
71
+ // Configuration defaults
72
+ this.defaultOptions = {
73
+ lineColor: 16711680,
74
+ labelColor: "#ffffff",
75
+ lineWidth: 2,
76
+ fontSize: 16,
77
+ fontFamily: "Arial, sans-serif",
78
+ snapMode: "vertex" /* VERTEX */,
79
+ snapEnabled: true,
80
+ snapDistance: 0.05,
81
+ targets: [],
82
+ isDynamic: false
83
+ };
84
+ this.previewColor = 65535;
85
+ this.markerColor = 65280;
86
+ this.markerSize = 0.08;
87
+ this.markerVisible = true;
88
+ // Edit mode properties
89
+ this.isEditMode = false;
90
+ this.editingMeasurement = null;
91
+ this.editingPoint = null;
92
+ this.startEditSprite = null;
93
+ this.endEditSprite = null;
94
+ this.editSpriteMaterial = null;
95
+ this.isDragging = false;
96
+ // Private methods
97
+ this.onMouseClick = (event) => {
98
+ if (!this.isInteractive) return;
99
+ const snapResult = this.getSnapResult(event);
100
+ if (!snapResult) return;
101
+ if (!this.currentMeasurement) {
102
+ this.startMeasurement(snapResult);
103
+ } else {
104
+ this.completeMeasurement(snapResult);
105
+ }
106
+ };
107
+ this.onMouseMove = (event) => {
108
+ if (!this.isInteractive) return;
109
+ const snapResult = this.getSnapResult(event);
110
+ if (!snapResult) {
111
+ this.hideSnapMarker();
112
+ this.showCursor();
113
+ return;
114
+ }
115
+ if (!this.currentMeasurement) {
116
+ this.updateSnapMarker(snapResult.point, true);
117
+ this.hideCursor();
118
+ } else {
119
+ this.updateSnapMarker(snapResult.point, true);
120
+ this.hideCursor();
121
+ this.updatePreview(snapResult.point);
122
+ }
123
+ };
124
+ this.onKeyDown = (event) => {
125
+ if (!this.isInteractive) return;
126
+ if (event.key === "Escape") {
127
+ this.cancelCurrentMeasurement();
128
+ }
129
+ };
130
+ this.onEditMouseDown = (event) => {
131
+ if (!this.isEditMode || !this.domElement) return;
132
+ const mouse = new THREE.Vector2();
133
+ const rect = this.domElement.getBoundingClientRect();
134
+ mouse.x = (event.clientX - rect.left) / rect.width * 2 - 1;
135
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
136
+ this.raycaster.setFromCamera(mouse, this.camera);
137
+ const sprites = [this.startEditSprite, this.endEditSprite].filter(
138
+ (s) => s !== null
139
+ );
140
+ const spriteIntersects = this.raycaster.intersectObjects(sprites);
141
+ if (spriteIntersects.length > 0) {
142
+ const sprite = spriteIntersects[0].object;
143
+ this.editingPoint = sprite.userData.editPoint;
144
+ this.isDragging = true;
145
+ this.disableControls();
146
+ if (this.editingPoint === "start" && this.startEditSprite) {
147
+ this.startEditSprite.visible = false;
148
+ } else if (this.editingPoint === "end" && this.endEditSprite) {
149
+ this.endEditSprite.visible = false;
150
+ }
151
+ this.createSnapMarker();
152
+ if (this.snapMarker) {
153
+ this.snapMarker.position.copy(sprite.position);
154
+ this.snapMarker.visible = true;
155
+ }
156
+ this.hideCursor();
157
+ }
158
+ };
159
+ this.onEditMouseMove = (event) => {
160
+ if (!this.isEditMode || !this.isDragging || !this.editingMeasurement) return;
161
+ const snapResult = this.getSnapResult(event);
162
+ if (!snapResult) return;
163
+ if (this.snapMarker) {
164
+ this.snapMarker.position.copy(snapResult.point);
165
+ this.snapMarker.visible = this.markerVisible;
166
+ }
167
+ if (this.editingPoint === "start") {
168
+ this.updateMeasurementPreview(
169
+ snapResult.point,
170
+ this.editingMeasurement.end.position
171
+ );
172
+ } else if (this.editingPoint === "end") {
173
+ this.updateMeasurementPreview(
174
+ this.editingMeasurement.start.position,
175
+ snapResult.point
176
+ );
177
+ }
178
+ };
179
+ this.onEditMouseUp = (event) => {
180
+ if (!this.isEditMode || !this.isDragging || !this.editingMeasurement || !this.editingPoint)
181
+ return;
182
+ const snapResult = this.getSnapResult(event);
183
+ if (!snapResult) {
184
+ this.cancelEdit();
185
+ return;
186
+ }
187
+ const point = this.editingPoint === "start" ? this.editingMeasurement.start : this.editingMeasurement.end;
188
+ point.position.copy(snapResult.point);
189
+ const measurementOptions = this.editingMeasurement.options;
190
+ if (measurementOptions.isDynamic && snapResult.object) {
191
+ const localPos = snapResult.object.worldToLocal(snapResult.point.clone());
192
+ point.anchor = {
193
+ object: snapResult.object,
194
+ localPosition: localPos
195
+ };
196
+ } else {
197
+ point.anchor = void 0;
198
+ }
199
+ const newDistance = this.editingMeasurement.start.position.distanceTo(
200
+ this.editingMeasurement.end.position
201
+ );
202
+ this.editingMeasurement.distance = newDistance;
203
+ const positions = [
204
+ this.editingMeasurement.start.position,
205
+ this.editingMeasurement.end.position
206
+ ];
207
+ this.editingMeasurement.line.geometry.setFromPoints(positions);
208
+ this.editingMeasurement.line.geometry.attributes.position.needsUpdate = true;
209
+ const midpoint = this.editingMeasurement.start.position.clone().add(this.editingMeasurement.end.position).multiplyScalar(0.5);
210
+ this.editingMeasurement.label.position.copy(midpoint);
211
+ this.updateLabelText(this.editingMeasurement.label.element, newDistance);
212
+ if (this.startEditSprite) {
213
+ this.startEditSprite.position.copy(this.editingMeasurement.start.position);
214
+ this.startEditSprite.visible = true;
215
+ }
216
+ if (this.endEditSprite) {
217
+ this.endEditSprite.position.copy(this.editingMeasurement.end.position);
218
+ this.endEditSprite.visible = true;
219
+ }
220
+ this.removeSnapMarker();
221
+ this.showCursor();
222
+ this.enableControls();
223
+ this.dispatchEvent({
224
+ type: "measurementUpdated",
225
+ measurement: this.editingMeasurement
226
+ });
227
+ this.isDragging = false;
228
+ this.editingPoint = null;
229
+ };
230
+ this.scene = scene;
231
+ this.camera = camera;
232
+ const { domElement, controls } = options;
233
+ if (domElement) {
234
+ this.domElement = domElement;
235
+ }
236
+ if (controls) {
237
+ this.controls = controls;
238
+ }
239
+ this.previewMaterial = new THREE.LineDashedMaterial({
240
+ color: this.previewColor,
241
+ linewidth: this.defaultOptions.lineWidth,
242
+ dashSize: 0.1,
243
+ gapSize: 0.05
244
+ });
245
+ this.markerMaterial = new THREE.SpriteMaterial({
246
+ map: this.createCrosshairTexture(),
247
+ color: this.markerColor,
248
+ transparent: true,
249
+ opacity: 0.8,
250
+ sizeAttenuation: false,
251
+ // Keep constant size regardless of distance
252
+ depthTest: false
253
+ // Always render in front of other objects
254
+ });
255
+ this.editSpriteMaterial = new THREE.SpriteMaterial({
256
+ map: this.createDotTexture(),
257
+ color: 16755200,
258
+ // Orange color for edit points
259
+ transparent: true,
260
+ opacity: 0.9,
261
+ sizeAttenuation: false,
262
+ depthTest: false
263
+ });
264
+ this.raycaster.params.Line.threshold = 0.01;
265
+ this.raycaster.params.Points.threshold = 0.01;
266
+ }
267
+ /**
268
+ * Create a crosshair texture for the snap marker sprite
269
+ */
270
+ createCrosshairTexture() {
271
+ const size = 64;
272
+ const canvas = document.createElement("canvas");
273
+ canvas.width = size;
274
+ canvas.height = size;
275
+ const context = canvas.getContext("2d");
276
+ const centerX = size / 2;
277
+ const centerY = size / 2;
278
+ const lineLength = 20;
279
+ const gap = 6;
280
+ context.clearRect(0, 0, size, size);
281
+ context.strokeStyle = "#ffffff";
282
+ context.lineWidth = 3;
283
+ context.lineCap = "round";
284
+ context.beginPath();
285
+ context.moveTo(centerX - lineLength, centerY);
286
+ context.lineTo(centerX - gap, centerY);
287
+ context.moveTo(centerX + gap, centerY);
288
+ context.lineTo(centerX + lineLength, centerY);
289
+ context.moveTo(centerX, centerY - lineLength);
290
+ context.lineTo(centerX, centerY - gap);
291
+ context.moveTo(centerX, centerY + gap);
292
+ context.lineTo(centerX, centerY + lineLength);
293
+ context.stroke();
294
+ context.strokeStyle = "#000000";
295
+ context.lineWidth = 5;
296
+ context.globalCompositeOperation = "destination-over";
297
+ context.beginPath();
298
+ context.moveTo(centerX - lineLength, centerY);
299
+ context.lineTo(centerX - gap, centerY);
300
+ context.moveTo(centerX + gap, centerY);
301
+ context.lineTo(centerX + lineLength, centerY);
302
+ context.moveTo(centerX, centerY - lineLength);
303
+ context.lineTo(centerX, centerY - gap);
304
+ context.moveTo(centerX, centerY + gap);
305
+ context.lineTo(centerX, centerY + lineLength);
306
+ context.stroke();
307
+ const texture = new THREE.CanvasTexture(canvas);
308
+ texture.needsUpdate = true;
309
+ return texture;
310
+ }
311
+ /**
312
+ * Create a dot texture for edit point sprites
313
+ */
314
+ createDotTexture() {
315
+ const size = 64;
316
+ const canvas = document.createElement("canvas");
317
+ canvas.width = size;
318
+ canvas.height = size;
319
+ const context = canvas.getContext("2d");
320
+ const centerX = size / 2;
321
+ const centerY = size / 2;
322
+ const radius = 12;
323
+ context.clearRect(0, 0, size, size);
324
+ context.fillStyle = "#ffffff";
325
+ context.beginPath();
326
+ context.arc(centerX, centerY, radius, 0, Math.PI * 2);
327
+ context.fill();
328
+ context.strokeStyle = "#000000";
329
+ context.lineWidth = 3;
330
+ context.beginPath();
331
+ context.arc(centerX, centerY, radius, 0, Math.PI * 2);
332
+ context.stroke();
333
+ const texture = new THREE.CanvasTexture(canvas);
334
+ texture.needsUpdate = true;
335
+ return texture;
336
+ }
337
+ /**
338
+ * Create a measurement point that optionally tracks a scene object.
339
+ */
340
+ createMeasurementPoint(worldPosition, object, localPosition) {
341
+ if (!object) {
342
+ return { position: worldPosition.clone() };
343
+ }
344
+ const anchorLocal = localPosition?.clone() ?? object.worldToLocal(worldPosition.clone());
345
+ const resolvedWorld = object.localToWorld(anchorLocal.clone());
346
+ return {
347
+ position: resolvedWorld,
348
+ anchor: {
349
+ object,
350
+ localPosition: anchorLocal
351
+ }
352
+ };
353
+ }
354
+ /**
355
+ * Update a measurement point's world position from its anchor (if dynamic)
356
+ */
357
+ updateMeasurementPoint(point) {
358
+ if (!point.anchor) {
359
+ return false;
360
+ }
361
+ const newWorldPosition = point.anchor.localPosition.clone();
362
+ point.anchor.object.localToWorld(newWorldPosition);
363
+ if (!point.position.equals(newWorldPosition)) {
364
+ point.position.copy(newWorldPosition);
365
+ return true;
366
+ }
367
+ return false;
368
+ }
369
+ /**
370
+ * Add a measurement between two world positions with optional attachments.
371
+ *
372
+ * @param start - Starting world position
373
+ * @param end - Ending world position
374
+ * @param options - Optional configuration for the measurement
375
+ */
376
+ addMeasurement(start, end, options = {}) {
377
+ const hasAnchors = Boolean(options.startObject || options.endObject);
378
+ const resolvedOptions = this.resolveMeasurementOptions(
379
+ options,
380
+ options.isDynamic ?? hasAnchors
381
+ );
382
+ const startPoint = this.createMeasurementPoint(
383
+ start,
384
+ options.startObject,
385
+ options.startLocalPosition
386
+ );
387
+ const endPoint = this.createMeasurementPoint(
388
+ end,
389
+ options.endObject,
390
+ options.endLocalPosition
391
+ );
392
+ return this.addMeasurementFromPoints(startPoint, endPoint, resolvedOptions, {
393
+ id: options.id
394
+ });
395
+ }
396
+ resolveMeasurementOptions(overrides = {}, inferredDynamic) {
397
+ let targetCandidates;
398
+ if (overrides.targets && overrides.targets.length > 0) {
399
+ targetCandidates = overrides.targets;
400
+ } else if (this.defaultOptions.targets.length > 0) {
401
+ targetCandidates = this.defaultOptions.targets;
402
+ } else if (this.defaultTargets.length > 0) {
403
+ targetCandidates = this.defaultTargets;
404
+ }
405
+ const targets = targetCandidates && targetCandidates.length > 0 ? targetCandidates : this.getAllMeshes();
406
+ const isDynamic = overrides.isDynamic ?? inferredDynamic ?? this.defaultOptions.isDynamic;
407
+ return {
408
+ lineColor: overrides.lineColor ?? this.defaultOptions.lineColor,
409
+ labelColor: overrides.labelColor ?? this.defaultOptions.labelColor,
410
+ lineWidth: overrides.lineWidth ?? this.defaultOptions.lineWidth,
411
+ fontSize: overrides.fontSize ?? this.defaultOptions.fontSize,
412
+ fontFamily: overrides.fontFamily ?? this.defaultOptions.fontFamily,
413
+ snapMode: overrides.snapMode ?? this.defaultOptions.snapMode,
414
+ snapEnabled: overrides.snapEnabled ?? this.defaultOptions.snapEnabled,
415
+ snapDistance: overrides.snapDistance ?? this.defaultOptions.snapDistance,
416
+ targets,
417
+ isDynamic
418
+ };
419
+ }
420
+ getActiveMeasurementOptions() {
421
+ if (this.isEditMode && this.editingMeasurement) {
422
+ return this.editingMeasurement.options;
423
+ }
424
+ return this.activeInteractionOptions;
425
+ }
426
+ /**
427
+ * @deprecated Use addMeasurement(obj1, obj2, { startLocalPos, endLocalPos }) instead
428
+ * Add a dynamic measurement between two objects
429
+ */
430
+ addDynamicMeasurement(startObject, endObject, startLocalPos = new THREE.Vector3(), endLocalPos = new THREE.Vector3()) {
431
+ const startWorld = startObject.localToWorld(startLocalPos.clone());
432
+ const endWorld = endObject.localToWorld(endLocalPos.clone());
433
+ return this.addMeasurement(startWorld, endWorld, {
434
+ startObject,
435
+ endObject,
436
+ startLocalPosition: startLocalPos,
437
+ endLocalPosition: endLocalPos
438
+ });
439
+ }
440
+ /**
441
+ * @deprecated Use addMeasurement(staticPos, targetObject, { endLocalPos }) instead
442
+ * Add a measurement from a static point to a dynamic object
443
+ */
444
+ addMeasurementToObject(staticPos, targetObject, objectLocalPos = new THREE.Vector3()) {
445
+ const endWorld = targetObject.localToWorld(objectLocalPos.clone());
446
+ return this.addMeasurement(staticPos, endWorld, {
447
+ endObject: targetObject,
448
+ endLocalPosition: objectLocalPos
449
+ });
450
+ }
451
+ /**
452
+ * Core method to add a measurement from two measurement points
453
+ */
454
+ addMeasurementFromPoints(start, end, options, context) {
455
+ const id = context?.id || this.generateId();
456
+ const distance = start.position.distanceTo(end.position);
457
+ const geometry = new THREE.BufferGeometry().setFromPoints([
458
+ start.position,
459
+ end.position
460
+ ]);
461
+ const line = new THREE.Line(
462
+ geometry,
463
+ new THREE.LineBasicMaterial({
464
+ color: options.lineColor,
465
+ linewidth: options.lineWidth
466
+ })
467
+ );
468
+ const label = this.createLabel(distance, options);
469
+ const midpoint = start.position.clone().add(end.position).multiplyScalar(0.5);
470
+ label.position.copy(midpoint);
471
+ const measurement = {
472
+ id,
473
+ start,
474
+ end,
475
+ line,
476
+ label,
477
+ distance,
478
+ options: {
479
+ ...options,
480
+ targets: [...options.targets]
481
+ }
482
+ };
483
+ this.scene.add(line);
484
+ this.scene.add(label);
485
+ this.measurements.push(measurement);
486
+ this.dispatchEvent({
487
+ type: "measurementCreated",
488
+ measurement
489
+ });
490
+ return measurement;
491
+ }
492
+ /**
493
+ * Update all dynamic measurements in real-time
494
+ * Call this in your animation loop to keep dynamic measurements up-to-date
495
+ */
496
+ updateDynamicMeasurements() {
497
+ let updated = false;
498
+ for (const measurement of this.measurements) {
499
+ if (!measurement.options.isDynamic) continue;
500
+ let needsUpdate = false;
501
+ if (this.updateMeasurementPoint(measurement.start)) {
502
+ needsUpdate = true;
503
+ }
504
+ if (this.updateMeasurementPoint(measurement.end)) {
505
+ needsUpdate = true;
506
+ }
507
+ if (needsUpdate) {
508
+ const newDistance = measurement.start.position.distanceTo(
509
+ measurement.end.position
510
+ );
511
+ measurement.distance = newDistance;
512
+ const positions = [measurement.start.position, measurement.end.position];
513
+ measurement.line.geometry.setFromPoints(positions);
514
+ measurement.line.geometry.attributes.position.needsUpdate = true;
515
+ const midpoint = measurement.start.position.clone().add(measurement.end.position).multiplyScalar(0.5);
516
+ measurement.label.position.copy(midpoint);
517
+ this.updateLabelText(measurement.label.element, newDistance);
518
+ updated = true;
519
+ }
520
+ }
521
+ return updated;
522
+ }
523
+ /**
524
+ * Set whether interactive measurements should be dynamic or static
525
+ */
526
+ setDynamicMode(enabled) {
527
+ this.setDefaultMeasurementOptions({ isDynamic: enabled });
528
+ }
529
+ /**
530
+ * Get the current dynamic mode state
531
+ */
532
+ getDynamicMode() {
533
+ return this.defaultOptions.isDynamic;
534
+ }
535
+ /**
536
+ * Enter edit mode for a specific measurement
537
+ * Shows edit sprites at the measurement endpoints
538
+ * @param measurementIdOrIndex - The measurement ID or index
539
+ * @param targets - Optional target objects for snapping during edit
540
+ */
541
+ enterEditMode(measurementIdOrIndex, targets) {
542
+ this.exitEditMode();
543
+ let measurement;
544
+ if (typeof measurementIdOrIndex === "string") {
545
+ measurement = this.measurements.find((m) => m.id === measurementIdOrIndex);
546
+ } else {
547
+ measurement = this.measurements[measurementIdOrIndex];
548
+ }
549
+ if (!measurement) {
550
+ console.warn("Measurement not found:", measurementIdOrIndex);
551
+ return;
552
+ }
553
+ if (!this.domElement) {
554
+ console.warn(
555
+ "DOM element not set. Call setDomElement() or enableInteraction() first."
556
+ );
557
+ return;
558
+ }
559
+ this.isEditMode = true;
560
+ this.editingMeasurement = measurement;
561
+ const resolvedTargets = targets && targets.length > 0 ? targets : measurement.options.targets.length > 0 ? measurement.options.targets : this.getAllMeshes();
562
+ this.activeTargets = resolvedTargets;
563
+ if (targets && targets.length > 0) {
564
+ measurement.options.targets = [...targets];
565
+ }
566
+ this.createEditSprites();
567
+ this.domElement.addEventListener("mousedown", this.onEditMouseDown);
568
+ this.domElement.addEventListener("mousemove", this.onEditMouseMove);
569
+ this.domElement.addEventListener("mouseup", this.onEditMouseUp);
570
+ this.domElement.style.cursor = "pointer";
571
+ this.dispatchEvent({
572
+ type: "editModeEntered",
573
+ measurement
574
+ });
575
+ }
576
+ /**
577
+ * Exit edit mode
578
+ */
579
+ exitEditMode() {
580
+ if (!this.isEditMode) return;
581
+ const measurement = this.editingMeasurement;
582
+ this.removeEditSprites();
583
+ if (this.domElement) {
584
+ this.domElement.removeEventListener("mousedown", this.onEditMouseDown);
585
+ this.domElement.removeEventListener("mousemove", this.onEditMouseMove);
586
+ this.domElement.removeEventListener("mouseup", this.onEditMouseUp);
587
+ this.domElement.style.cursor = this.isInteractive ? "crosshair" : "default";
588
+ }
589
+ this.isEditMode = false;
590
+ this.editingMeasurement = null;
591
+ this.editingPoint = null;
592
+ this.isDragging = false;
593
+ this.activeTargets = [];
594
+ if (measurement) {
595
+ this.dispatchEvent({
596
+ type: "editModeExited",
597
+ measurement
598
+ });
599
+ }
600
+ }
601
+ /**
602
+ * Set the DOM element for interactions (both measurement and edit mode)
603
+ */
604
+ setDomElement(domElement) {
605
+ this.domElement = domElement;
606
+ }
607
+ /**
608
+ * Set the camera controls to disable during edit dragging
609
+ */
610
+ setControls(controls) {
611
+ this.controls = controls;
612
+ }
613
+ /**
614
+ * Set target objects for snapping (used in both interactive mode and edit mode)
615
+ */
616
+ setTargetObjects(targets) {
617
+ this.defaultTargets = targets.length > 0 ? targets : this.getAllMeshes();
618
+ this.defaultOptions.targets = [...this.defaultTargets];
619
+ }
620
+ /**
621
+ * Update default measurement options used when none are provided explicitly.
622
+ */
623
+ setDefaultMeasurementOptions(options) {
624
+ if (options.lineColor !== void 0) {
625
+ this.defaultOptions.lineColor = options.lineColor;
626
+ }
627
+ if (options.labelColor !== void 0) {
628
+ this.defaultOptions.labelColor = options.labelColor;
629
+ }
630
+ if (options.lineWidth !== void 0) {
631
+ this.defaultOptions.lineWidth = options.lineWidth;
632
+ }
633
+ if (options.fontSize !== void 0) {
634
+ this.defaultOptions.fontSize = options.fontSize;
635
+ }
636
+ if (options.fontFamily !== void 0) {
637
+ this.defaultOptions.fontFamily = options.fontFamily;
638
+ }
639
+ if (options.snapMode !== void 0) {
640
+ this.defaultOptions.snapMode = options.snapMode;
641
+ }
642
+ if (options.snapEnabled !== void 0) {
643
+ this.defaultOptions.snapEnabled = options.snapEnabled;
644
+ }
645
+ if (options.snapDistance !== void 0) {
646
+ this.defaultOptions.snapDistance = options.snapDistance;
647
+ }
648
+ if (options.targets !== void 0) {
649
+ this.defaultTargets = options.targets;
650
+ this.defaultOptions.targets = [...options.targets];
651
+ if (this.activeInteractionOptions) {
652
+ this.activeInteractionOptions.targets = [...options.targets];
653
+ this.activeTargets = options.targets;
654
+ }
655
+ }
656
+ if (options.isDynamic !== void 0) {
657
+ this.defaultOptions.isDynamic = options.isDynamic;
658
+ if (this.activeInteractionOptions) {
659
+ this.activeInteractionOptions.isDynamic = options.isDynamic;
660
+ }
661
+ }
662
+ if (this.activeInteractionOptions) {
663
+ if (options.lineColor !== void 0) {
664
+ this.activeInteractionOptions.lineColor = options.lineColor;
665
+ }
666
+ if (options.labelColor !== void 0) {
667
+ this.activeInteractionOptions.labelColor = options.labelColor;
668
+ }
669
+ if (options.lineWidth !== void 0) {
670
+ this.activeInteractionOptions.lineWidth = options.lineWidth;
671
+ }
672
+ if (options.fontSize !== void 0) {
673
+ this.activeInteractionOptions.fontSize = options.fontSize;
674
+ }
675
+ if (options.fontFamily !== void 0) {
676
+ this.activeInteractionOptions.fontFamily = options.fontFamily;
677
+ }
678
+ if (options.snapMode !== void 0) {
679
+ this.activeInteractionOptions.snapMode = options.snapMode;
680
+ }
681
+ if (options.snapEnabled !== void 0) {
682
+ this.activeInteractionOptions.snapEnabled = options.snapEnabled;
683
+ }
684
+ if (options.snapDistance !== void 0) {
685
+ this.activeInteractionOptions.snapDistance = options.snapDistance;
686
+ }
687
+ }
688
+ }
689
+ /**
690
+ * Disable camera controls (used during edit dragging)
691
+ */
692
+ disableControls() {
693
+ if (this.controls) {
694
+ this.controls.enabled = false;
695
+ }
696
+ }
697
+ /**
698
+ * Enable camera controls (used after edit dragging)
699
+ */
700
+ enableControls() {
701
+ if (this.controls) {
702
+ this.controls.enabled = true;
703
+ }
704
+ }
705
+ /**
706
+ * Enable interactive measurement mode
707
+ */
708
+ enableInteraction(options = {}) {
709
+ if (this.isInteractive) {
710
+ this.disableInteraction();
711
+ }
712
+ const resolvedOptions = this.resolveMeasurementOptions(options);
713
+ this.activeInteractionOptions = {
714
+ ...resolvedOptions,
715
+ targets: [...resolvedOptions.targets]
716
+ };
717
+ this.activeTargets = this.activeInteractionOptions.targets.length > 0 ? this.activeInteractionOptions.targets : this.getAllMeshes();
718
+ this.isInteractive = true;
719
+ if (this.domElement) {
720
+ this.domElement.addEventListener("click", this.onMouseClick);
721
+ this.domElement.addEventListener("mousemove", this.onMouseMove);
722
+ this.domElement.addEventListener("keydown", this.onKeyDown);
723
+ this.domElement.style.cursor = "crosshair";
724
+ }
725
+ this.createSnapMarker();
726
+ this.dispatchEvent({ type: "started" });
727
+ }
728
+ /**
729
+ * Disable interactive measurement mode
730
+ */
731
+ disableInteraction() {
732
+ if (!this.isInteractive || !this.domElement) return;
733
+ this.exitEditMode();
734
+ this.domElement.removeEventListener("click", this.onMouseClick);
735
+ this.domElement.removeEventListener("mousemove", this.onMouseMove);
736
+ this.domElement.removeEventListener("keydown", this.onKeyDown);
737
+ this.showCursor();
738
+ this.domElement.style.cursor = "default";
739
+ this.cancelCurrentMeasurement();
740
+ this.removeSnapMarker();
741
+ this.isInteractive = false;
742
+ this.activeInteractionOptions = null;
743
+ this.activeTargets = [];
744
+ this.dispatchEvent({ type: "ended" });
745
+ }
746
+ /**
747
+ * Remove the last measurement (undo)
748
+ */
749
+ undoLast() {
750
+ if (this.measurements.length === 0) return;
751
+ const lastMeasurement = this.measurements.pop();
752
+ this.removeMeasurementFromScene(lastMeasurement);
753
+ this.dispatchEvent({
754
+ type: "measurementRemoved",
755
+ measurement: lastMeasurement
756
+ });
757
+ }
758
+ /**
759
+ * Remove a specific measurement
760
+ */
761
+ removeMeasurement(measurement) {
762
+ const index = this.measurements.indexOf(measurement);
763
+ if (index === -1) return;
764
+ this.measurements.splice(index, 1);
765
+ this.removeMeasurementFromScene(measurement);
766
+ this.dispatchEvent({
767
+ type: "measurementRemoved",
768
+ measurement
769
+ });
770
+ }
771
+ /**
772
+ * Clear all measurements
773
+ */
774
+ clearAll() {
775
+ const count = this.measurements.length;
776
+ this.measurements.forEach((measurement) => {
777
+ this.removeMeasurementFromScene(measurement);
778
+ });
779
+ this.measurements = [];
780
+ this.dispatchEvent({
781
+ type: "measurementsCleared",
782
+ count
783
+ });
784
+ }
785
+ /**
786
+ * Get all measurements
787
+ */
788
+ getMeasurements() {
789
+ return [...this.measurements];
790
+ }
791
+ /**
792
+ * Convert a MeasurementPoint to serializable data
793
+ */
794
+ serializeMeasurementPoint(point) {
795
+ return {
796
+ position: point.position.toArray(),
797
+ anchorObjectId: point.anchor?.object.uuid,
798
+ anchorLocalPosition: point.anchor ? point.anchor.localPosition.toArray() : void 0
799
+ };
800
+ }
801
+ /**
802
+ * Serialize measurements to JSON-compatible format
803
+ * Note: Dynamic measurements will lose their object references and become static when deserialized
804
+ */
805
+ serialize() {
806
+ return this.measurements.map((measurement) => ({
807
+ id: measurement.id,
808
+ start: this.serializeMeasurementPoint(measurement.start),
809
+ end: this.serializeMeasurementPoint(measurement.end),
810
+ distance: measurement.distance,
811
+ options: {
812
+ snapMode: measurement.options.snapMode,
813
+ snapEnabled: measurement.options.snapEnabled,
814
+ snapDistance: measurement.options.snapDistance,
815
+ lineColor: measurement.options.lineColor,
816
+ labelColor: measurement.options.labelColor,
817
+ lineWidth: measurement.options.lineWidth,
818
+ fontSize: measurement.options.fontSize,
819
+ fontFamily: measurement.options.fontFamily,
820
+ isDynamic: measurement.options.isDynamic,
821
+ targetObjectIds: measurement.options.targets.map((obj) => obj.uuid)
822
+ }
823
+ }));
824
+ }
825
+ /**
826
+ * Deserialize measurements from JSON data
827
+ * Note: Dynamic measurements become static since object references are lost
828
+ */
829
+ deserialize(data) {
830
+ this.clearAll();
831
+ data.forEach((item) => {
832
+ const start = new THREE.Vector3().fromArray(item.start.position);
833
+ const end = new THREE.Vector3().fromArray(item.end.position);
834
+ const startObject = item.start.anchorObjectId ? this.scene.getObjectByProperty(
835
+ "uuid",
836
+ item.start.anchorObjectId
837
+ ) : null;
838
+ const endObject = item.end.anchorObjectId ? this.scene.getObjectByProperty(
839
+ "uuid",
840
+ item.end.anchorObjectId
841
+ ) : null;
842
+ const restoredTargets = item.options.targetObjectIds && item.options.targetObjectIds.length > 0 ? item.options.targetObjectIds.map((uuid) => this.scene.getObjectByProperty("uuid", uuid)).filter((obj) => obj !== void 0) : void 0;
843
+ this.addMeasurement(start, end, {
844
+ id: item.id,
845
+ targets: restoredTargets && restoredTargets.length > 0 ? restoredTargets : void 0,
846
+ snapMode: item.options.snapMode,
847
+ snapEnabled: item.options.snapEnabled,
848
+ snapDistance: item.options.snapDistance,
849
+ lineColor: item.options.lineColor,
850
+ labelColor: item.options.labelColor,
851
+ lineWidth: item.options.lineWidth,
852
+ fontSize: item.options.fontSize,
853
+ fontFamily: item.options.fontFamily,
854
+ isDynamic: item.options.isDynamic,
855
+ startObject: startObject || void 0,
856
+ startLocalPosition: item.start.anchorLocalPosition ? new THREE.Vector3().fromArray(item.start.anchorLocalPosition) : void 0,
857
+ endObject: endObject || void 0,
858
+ endLocalPosition: item.end.anchorLocalPosition ? new THREE.Vector3().fromArray(item.end.anchorLocalPosition) : void 0
859
+ });
860
+ });
861
+ }
862
+ /**
863
+ * Dispose of all resources
864
+ */
865
+ dispose() {
866
+ this.exitEditMode();
867
+ this.disableInteraction();
868
+ this.clearAll();
869
+ this.previewMaterial.dispose();
870
+ if (this.markerMaterial.map) {
871
+ this.markerMaterial.map.dispose();
872
+ }
873
+ this.markerMaterial.dispose();
874
+ if (this.editSpriteMaterial) {
875
+ if (this.editSpriteMaterial.map) {
876
+ this.editSpriteMaterial.map.dispose();
877
+ }
878
+ this.editSpriteMaterial.dispose();
879
+ }
880
+ }
881
+ hideCursor() {
882
+ if (!this.domElement || this.cursorHidden) return;
883
+ this.originalCursor = this.domElement.style.cursor;
884
+ this.domElement.style.cursor = "none";
885
+ this.cursorHidden = true;
886
+ }
887
+ showCursor() {
888
+ if (!this.domElement || !this.cursorHidden) return;
889
+ this.domElement.style.cursor = this.originalCursor || "crosshair";
890
+ this.cursorHidden = false;
891
+ }
892
+ createSnapMarker() {
893
+ if (!this.markerVisible) return;
894
+ if (this.snapMarker) {
895
+ this.scene.remove(this.snapMarker);
896
+ }
897
+ this.snapMarker = new THREE.Sprite(this.markerMaterial);
898
+ this.snapMarker.scale.setScalar(this.markerSize);
899
+ this.snapMarker.visible = false;
900
+ this.snapMarker.renderOrder = 999;
901
+ this.snapMarker.material.depthTest = false;
902
+ this.scene.add(this.snapMarker);
903
+ }
904
+ updateSnapMarker(point, visible = true) {
905
+ if (!this.snapMarker || !this.markerVisible) return;
906
+ this.snapMarker.position.copy(point);
907
+ this.snapMarker.visible = visible;
908
+ }
909
+ hideSnapMarker() {
910
+ if (this.snapMarker) {
911
+ this.snapMarker.visible = false;
912
+ }
913
+ }
914
+ removeSnapMarker() {
915
+ if (this.snapMarker) {
916
+ this.scene.remove(this.snapMarker);
917
+ this.snapMarker = null;
918
+ }
919
+ }
920
+ startMeasurement(snapResult) {
921
+ const id = this.generateId();
922
+ const point = snapResult.point;
923
+ const baseOptions = this.activeInteractionOptions ?? this.defaultOptions;
924
+ const measurementOptions = {
925
+ ...baseOptions,
926
+ targets: [...baseOptions.targets]
927
+ };
928
+ this.pendingMeasurementOptions = measurementOptions;
929
+ this.hideSnapMarker();
930
+ const geometry = new THREE.BufferGeometry().setFromPoints([point, point]);
931
+ this.previewLine = new THREE.Line(geometry, this.previewMaterial);
932
+ this.previewLine.computeLineDistances();
933
+ this.scene.add(this.previewLine);
934
+ this.previewLabel = this.createLabel(0, measurementOptions);
935
+ this.previewLabel.position.copy(point);
936
+ this.scene.add(this.previewLabel);
937
+ const startPoint = measurementOptions.isDynamic && snapResult.object ? this.createMeasurementPoint(point, snapResult.object) : this.createMeasurementPoint(point);
938
+ this.currentMeasurement = {
939
+ id,
940
+ start: startPoint
941
+ };
942
+ }
943
+ updatePreview(point) {
944
+ if (!this.currentMeasurement || !this.previewLine || !this.previewLabel)
945
+ return;
946
+ const start = this.currentMeasurement.start;
947
+ const distance = start.position.distanceTo(point);
948
+ const geometry = new THREE.BufferGeometry().setFromPoints([
949
+ start.position,
950
+ point
951
+ ]);
952
+ this.previewLine.geometry.dispose();
953
+ this.previewLine.geometry = geometry;
954
+ this.previewLine.computeLineDistances();
955
+ const midpoint = start.position.clone().add(point).multiplyScalar(0.5);
956
+ this.previewLabel.position.copy(midpoint);
957
+ this.updateLabelText(this.previewLabel.element, distance);
958
+ this.dispatchEvent({
959
+ type: "previewUpdated",
960
+ start: start.position,
961
+ current: point,
962
+ distance
963
+ });
964
+ }
965
+ completeMeasurement(snapResult) {
966
+ if (!this.currentMeasurement) return;
967
+ const start = this.currentMeasurement.start;
968
+ const point = snapResult.point;
969
+ const options = this.pendingMeasurementOptions ?? this.activeInteractionOptions ?? this.defaultOptions;
970
+ this.disableInteraction();
971
+ this.cleanupPreview();
972
+ const resolvedOptions = {
973
+ ...options,
974
+ targets: [...options.targets]
975
+ };
976
+ const endPoint = resolvedOptions.isDynamic && snapResult.object ? this.createMeasurementPoint(point, snapResult.object) : this.createMeasurementPoint(point);
977
+ this.addMeasurementFromPoints(start, endPoint, resolvedOptions);
978
+ this.currentMeasurement = null;
979
+ this.pendingMeasurementOptions = null;
980
+ this.createSnapMarker();
981
+ }
982
+ cancelCurrentMeasurement() {
983
+ this.cleanupPreview();
984
+ this.currentMeasurement = null;
985
+ this.pendingMeasurementOptions = null;
986
+ this.createSnapMarker();
987
+ }
988
+ cleanupPreview() {
989
+ if (this.previewLine) {
990
+ this.scene.remove(this.previewLine);
991
+ this.previewLine.geometry.dispose();
992
+ this.previewLine = null;
993
+ }
994
+ if (this.previewLabel) {
995
+ this.scene.remove(this.previewLabel);
996
+ if (this.previewLabel.element.parentNode) {
997
+ this.previewLabel.element.parentNode.removeChild(
998
+ this.previewLabel.element
999
+ );
1000
+ }
1001
+ this.previewLabel = null;
1002
+ }
1003
+ }
1004
+ getSnapResult(event) {
1005
+ const mouse = new THREE.Vector2();
1006
+ const rect = this.domElement.getBoundingClientRect();
1007
+ mouse.x = (event.clientX - rect.left) / rect.width * 2 - 1;
1008
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
1009
+ const options = this.getActiveMeasurementOptions() ?? this.defaultOptions;
1010
+ const targets = this.activeTargets.length > 0 ? this.activeTargets : options.targets.length > 0 ? options.targets : this.getAllMeshes();
1011
+ this.raycaster.setFromCamera(mouse, this.camera);
1012
+ const intersects = this.raycaster.intersectObjects(targets, true);
1013
+ if (intersects.length === 0) return null;
1014
+ const intersection = intersects[0];
1015
+ let snapPoint = intersection.point.clone();
1016
+ let snapped = false;
1017
+ let snapMode = "disabled" /* DISABLED */;
1018
+ if (options.snapEnabled) {
1019
+ const snapResult = this.performSnapping(intersection, options);
1020
+ snapPoint = snapResult.point;
1021
+ snapped = snapResult.snapped;
1022
+ snapMode = snapResult.snapMode;
1023
+ }
1024
+ return {
1025
+ point: snapPoint,
1026
+ originalPoint: intersection.point,
1027
+ snapped,
1028
+ snapMode,
1029
+ object: intersection.object
1030
+ };
1031
+ }
1032
+ performSnapping(intersection, options) {
1033
+ const originalPoint = intersection.point;
1034
+ let snapPoint = originalPoint.clone();
1035
+ let snapped = false;
1036
+ let snapMode = "disabled" /* DISABLED */;
1037
+ if (options.snapMode === "vertex" /* VERTEX */) {
1038
+ const vertexSnap = this.snapToVertex(intersection, options.snapDistance);
1039
+ if (vertexSnap) {
1040
+ snapPoint = vertexSnap;
1041
+ snapped = true;
1042
+ snapMode = "vertex" /* VERTEX */;
1043
+ }
1044
+ } else if (options.snapMode === "face" /* FACE */) {
1045
+ snapped = true;
1046
+ snapMode = "face" /* FACE */;
1047
+ }
1048
+ return {
1049
+ point: snapPoint,
1050
+ originalPoint,
1051
+ snapped,
1052
+ snapMode,
1053
+ object: intersection.object
1054
+ };
1055
+ }
1056
+ snapToVertex(intersection, snapDistance) {
1057
+ const geometry = intersection.object.geometry;
1058
+ if (!geometry.attributes.position) return null;
1059
+ const positions = geometry.attributes.position;
1060
+ const worldMatrix = intersection.object.matrixWorld;
1061
+ const closestVertex = new THREE.Vector3();
1062
+ let minDistance = Infinity;
1063
+ let found = false;
1064
+ for (let i = 0; i < positions.count; i++) {
1065
+ const vertex = new THREE.Vector3();
1066
+ vertex.fromBufferAttribute(positions, i);
1067
+ vertex.applyMatrix4(worldMatrix);
1068
+ const distance = vertex.distanceTo(intersection.point);
1069
+ if (distance < snapDistance && distance < minDistance) {
1070
+ minDistance = distance;
1071
+ closestVertex.copy(vertex);
1072
+ found = true;
1073
+ }
1074
+ }
1075
+ return found ? closestVertex : null;
1076
+ }
1077
+ createLabel(distance, options) {
1078
+ const labelDiv = document.createElement("div");
1079
+ labelDiv.className = "measurement-label";
1080
+ Object.assign(labelDiv.style, {
1081
+ color: options.labelColor,
1082
+ fontSize: `${options.fontSize}px`,
1083
+ fontFamily: options.fontFamily,
1084
+ fontWeight: "bold",
1085
+ background: "rgba(0, 0, 0, 0.9)",
1086
+ padding: "8px 12px",
1087
+ borderRadius: "8px",
1088
+ border: "2px solid rgba(255, 255, 255, 0.3)",
1089
+ whiteSpace: "nowrap",
1090
+ userSelect: "none",
1091
+ pointerEvents: "auto",
1092
+ // Enable pointer events for double-click
1093
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.5)",
1094
+ textShadow: "1px 1px 2px rgba(0, 0, 0, 0.8)",
1095
+ zIndex: "1000",
1096
+ cursor: "pointer"
1097
+ });
1098
+ this.updateLabelText(labelDiv, distance);
1099
+ const css2dObject = new import_CSS2DRenderer.CSS2DObject(labelDiv);
1100
+ labelDiv.addEventListener("dblclick", (event) => {
1101
+ event.stopPropagation();
1102
+ const measurement = this.measurements.find((m) => m.label === css2dObject);
1103
+ if (measurement) {
1104
+ this.enterEditMode(measurement.id);
1105
+ }
1106
+ });
1107
+ return css2dObject;
1108
+ }
1109
+ updateLabelText(element, distance) {
1110
+ const text = `${distance.toFixed(2)}m`;
1111
+ element.textContent = text;
1112
+ }
1113
+ removeMeasurementFromScene(measurement) {
1114
+ this.scene.remove(measurement.line);
1115
+ this.scene.remove(measurement.label);
1116
+ measurement.line.geometry.dispose();
1117
+ if (measurement.line.material instanceof THREE.Material) {
1118
+ measurement.line.material.dispose();
1119
+ }
1120
+ if (measurement.label.element.parentNode) {
1121
+ measurement.label.element.parentNode.removeChild(
1122
+ measurement.label.element
1123
+ );
1124
+ }
1125
+ }
1126
+ getAllMeshes() {
1127
+ const meshes = [];
1128
+ this.scene.traverse((object) => {
1129
+ if (object instanceof THREE.Mesh) {
1130
+ meshes.push(object);
1131
+ }
1132
+ });
1133
+ return meshes;
1134
+ }
1135
+ generateId() {
1136
+ return `measurement_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1137
+ }
1138
+ // Edit mode helper methods
1139
+ createEditSprites() {
1140
+ if (!this.editingMeasurement || !this.editSpriteMaterial) return;
1141
+ const measurement = this.editingMeasurement;
1142
+ this.startEditSprite = new THREE.Sprite(this.editSpriteMaterial.clone());
1143
+ this.startEditSprite.position.copy(measurement.start.position);
1144
+ this.startEditSprite.scale.set(this.markerSize, this.markerSize, 1);
1145
+ this.startEditSprite.userData.editPoint = "start";
1146
+ this.scene.add(this.startEditSprite);
1147
+ this.endEditSprite = new THREE.Sprite(this.editSpriteMaterial.clone());
1148
+ this.endEditSprite.position.copy(measurement.end.position);
1149
+ this.endEditSprite.scale.set(this.markerSize, this.markerSize, 1);
1150
+ this.endEditSprite.userData.editPoint = "end";
1151
+ this.scene.add(this.endEditSprite);
1152
+ }
1153
+ removeEditSprites() {
1154
+ if (this.startEditSprite) {
1155
+ this.scene.remove(this.startEditSprite);
1156
+ if (this.startEditSprite.material instanceof THREE.Material) {
1157
+ this.startEditSprite.material.dispose();
1158
+ }
1159
+ this.startEditSprite = null;
1160
+ }
1161
+ if (this.endEditSprite) {
1162
+ this.scene.remove(this.endEditSprite);
1163
+ if (this.endEditSprite.material instanceof THREE.Material) {
1164
+ this.endEditSprite.material.dispose();
1165
+ }
1166
+ this.endEditSprite = null;
1167
+ }
1168
+ }
1169
+ cancelEdit() {
1170
+ this.isDragging = false;
1171
+ this.editingPoint = null;
1172
+ this.enableControls();
1173
+ if (this.startEditSprite) {
1174
+ this.startEditSprite.visible = true;
1175
+ }
1176
+ if (this.endEditSprite) {
1177
+ this.endEditSprite.visible = true;
1178
+ }
1179
+ this.removeSnapMarker();
1180
+ this.showCursor();
1181
+ }
1182
+ updateMeasurementPreview(startPos, endPos) {
1183
+ if (!this.editingMeasurement) return;
1184
+ const distance = startPos.distanceTo(endPos);
1185
+ const positions = [startPos, endPos];
1186
+ this.editingMeasurement.line.geometry.setFromPoints(positions);
1187
+ this.editingMeasurement.line.geometry.attributes.position.needsUpdate = true;
1188
+ const midpoint = startPos.clone().add(endPos).multiplyScalar(0.5);
1189
+ this.editingMeasurement.label.position.copy(midpoint);
1190
+ this.updateLabelText(this.editingMeasurement.label.element, distance);
1191
+ }
1192
+ };
1193
+ // Annotate the CommonJS export names for ESM import in node:
1194
+ 0 && (module.exports = {
1195
+ MeasurementTool,
1196
+ SnapMode
1197
+ });
1198
+ //# sourceMappingURL=index.cjs.map