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