@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.
- package/README.md +321 -0
- package/dist/asset-loader/index.cjs +376 -0
- package/dist/asset-loader/index.cjs.map +1 -0
- package/dist/asset-loader/index.d.mts +101 -0
- package/dist/asset-loader/index.d.ts +101 -0
- package/dist/asset-loader/index.mjs +7 -0
- package/dist/asset-loader/index.mjs.map +1 -0
- package/dist/camera/index.cjs +313 -0
- package/dist/camera/index.cjs.map +1 -0
- package/dist/camera/index.d.mts +82 -0
- package/dist/camera/index.d.ts +82 -0
- package/dist/camera/index.mjs +7 -0
- package/dist/camera/index.mjs.map +1 -0
- package/dist/chunk-5DP6WDB3.mjs +1161 -0
- package/dist/chunk-5DP6WDB3.mjs.map +1 -0
- package/dist/chunk-BJKSICFA.mjs +1579 -0
- package/dist/chunk-BJKSICFA.mjs.map +1 -0
- package/dist/chunk-BYRZCHE7.mjs +277 -0
- package/dist/chunk-BYRZCHE7.mjs.map +1 -0
- package/dist/chunk-EIROAPF7.mjs +387 -0
- package/dist/chunk-EIROAPF7.mjs.map +1 -0
- package/dist/chunk-EQDOX34V.mjs +164 -0
- package/dist/chunk-EQDOX34V.mjs.map +1 -0
- package/dist/chunk-IIAZ2WJJ.mjs +405 -0
- package/dist/chunk-IIAZ2WJJ.mjs.map +1 -0
- package/dist/chunk-L4VIIJZD.mjs +340 -0
- package/dist/chunk-L4VIIJZD.mjs.map +1 -0
- package/dist/chunk-P35QJCOG.mjs +339 -0
- package/dist/chunk-P35QJCOG.mjs.map +1 -0
- package/dist/chunk-R64RVBRM.mjs +394 -0
- package/dist/chunk-R64RVBRM.mjs.map +1 -0
- package/dist/compass/index.cjs +375 -0
- package/dist/compass/index.cjs.map +1 -0
- package/dist/compass/index.d.mts +58 -0
- package/dist/compass/index.d.ts +58 -0
- package/dist/compass/index.mjs +7 -0
- package/dist/compass/index.mjs.map +1 -0
- package/dist/grid/index.cjs +200 -0
- package/dist/grid/index.cjs.map +1 -0
- package/dist/grid/index.d.mts +43 -0
- package/dist/grid/index.d.ts +43 -0
- package/dist/grid/index.mjs +7 -0
- package/dist/grid/index.mjs.map +1 -0
- package/dist/index.cjs +5049 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +47 -0
- package/dist/index.mjs.map +1 -0
- package/dist/measurements/index.cjs +1198 -0
- package/dist/measurements/index.cjs.map +1 -0
- package/dist/measurements/index.d.mts +449 -0
- package/dist/measurements/index.d.ts +449 -0
- package/dist/measurements/index.mjs +9 -0
- package/dist/measurements/index.mjs.map +1 -0
- package/dist/sunlight/index.cjs +441 -0
- package/dist/sunlight/index.cjs.map +1 -0
- package/dist/sunlight/index.d.mts +92 -0
- package/dist/sunlight/index.d.ts +92 -0
- package/dist/sunlight/index.mjs +7 -0
- package/dist/sunlight/index.mjs.map +1 -0
- package/dist/terrain/index.cjs +423 -0
- package/dist/terrain/index.cjs.map +1 -0
- package/dist/terrain/index.d.mts +219 -0
- package/dist/terrain/index.d.ts +219 -0
- package/dist/terrain/index.mjs +7 -0
- package/dist/terrain/index.mjs.map +1 -0
- package/dist/transform-controls/index.cjs +1587 -0
- package/dist/transform-controls/index.cjs.map +1 -0
- package/dist/transform-controls/index.d.mts +162 -0
- package/dist/transform-controls/index.d.ts +162 -0
- package/dist/transform-controls/index.mjs +13 -0
- package/dist/transform-controls/index.mjs.map +1 -0
- package/dist/view-helper/index.cjs +430 -0
- package/dist/view-helper/index.cjs.map +1 -0
- package/dist/view-helper/index.d.mts +75 -0
- package/dist/view-helper/index.d.ts +75 -0
- package/dist/view-helper/index.mjs +7 -0
- package/dist/view-helper/index.mjs.map +1 -0
- 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
|