@thatopen/services 0.3.11 → 0.3.12
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/dist/cli/commands/revit.d.ts +9 -0
- package/dist/cli/lib/revit-addin.d.ts +20 -0
- package/dist/cli.js +101 -0
- package/dist/cloud/LasToPotree.d.ts +59 -0
- package/dist/cloud/SplatToFq.d.ts +38 -0
- package/dist/cloud/index.d.ts +2 -0
- package/docs/builtin/paths.json +86 -86
- package/docs/cloud/GaussianSplatConverter.md +43 -0
- package/docs/cloud/PointCloudConverter.md +53 -0
- package/docs/revit-collab-quickstart.md +151 -0
- package/package.json +95 -95
- package/src/built-in/ClashesManager/example.ts +283 -283
- package/src/built-in/UIManager/src/clashes-panel/example.ts +160 -160
- package/src/built-in/UIManager/src/models-list/example.ts +122 -118
- package/src/cli/templates/app/package.json +33 -33
- package/src/cli/templates/app/src/setups/camera-tools.ts +0 -63
- package/src/cli/templates/app/src/setups/camera-views.ts +0 -201
- package/src/cli/templates/app/src/setups/card-header.ts +0 -35
- package/src/cli/templates/app/src/setups/clipper-tool.ts +0 -773
- package/src/cli/templates/app/src/setups/clipper.ts +0 -75
- package/src/cli/templates/app/src/setups/cloud-runner.ts +0 -13
- package/src/cli/templates/app/src/setups/exploded-view.ts +0 -246
- package/src/cli/templates/app/src/setups/helper-panel.ts +0 -88
- package/src/cli/templates/app/src/setups/hider.ts +0 -98
- package/src/cli/templates/app/src/setups/index.ts +0 -23
- package/src/cli/templates/app/src/setups/inspection.ts +0 -136
- package/src/cli/templates/app/src/setups/measurement-panel.ts +0 -188
- package/src/cli/templates/app/src/setups/measurement-tool.ts +0 -588
- package/src/cli/templates/app/src/setups/measurements.ts +0 -133
- package/src/cli/templates/app/src/setups/navigation-gizmo.ts +0 -316
- package/src/cli/templates/app/src/setups/plans-panel.ts +0 -619
- package/src/cli/templates/app/src/setups/right-sidebar.ts +0 -178
- package/src/cli/templates/app/src/setups/styles-panel.ts +0 -110
- package/src/cli/templates/app/src/setups/styles.ts +0 -375
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +0 -156
- package/src/cli/templates/app/src/setups/tool-mode.ts +0 -23
- package/src/cli/templates/app/src/setups/toolbar.ts +0 -229
- package/src/cli/templates/app/src/setups/ui-manager.ts +0 -27
|
@@ -1,588 +0,0 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import * as OBC from "@thatopen/components";
|
|
3
|
-
import * as OBF from "@thatopen/components-front";
|
|
4
|
-
import * as FRAGS from "@thatopen/fragments";
|
|
5
|
-
import { toolModeManager, type ManagedTool } from "./tool-mode-manager";
|
|
6
|
-
import type { InstanceRow } from "./inspection";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Measurement TOOL (logic only; the UI lives in measurement-panel.ts).
|
|
10
|
-
*
|
|
11
|
-
* Drives the three OBF measurers — Length (point-to-point), Area (polygon) and
|
|
12
|
-
* Angle (3-point) — as exclusive MODES:
|
|
13
|
-
* - pick a mode → that measurer is enabled and a viewport double-click places
|
|
14
|
-
* points (Length: 2 pts; Area: N pts, Enter to close; Angle: 3 pts),
|
|
15
|
-
* - Escape cancels the in-progress measurement, Delete removes the one under the
|
|
16
|
-
* cursor (Shift+Delete clears all).
|
|
17
|
-
*
|
|
18
|
-
* SNAPPING (the thing that makes dimensions reliable vs eyeballed) is the
|
|
19
|
-
* library's vertex/edge/face snapper, on for all three classes by default; the
|
|
20
|
-
* panel can toggle each class. The snapper resolves the nearest snap target under
|
|
21
|
-
* the cursor (vertices win ties), which is exactly what we want for precise picks.
|
|
22
|
-
*
|
|
23
|
-
* Exposes aggregated rows (type + value + units) over all three lists, plus
|
|
24
|
-
* onChanged/onModeChanged so the panel re-renders.
|
|
25
|
-
*/
|
|
26
|
-
export type MeasureMode = "none" | "length" | "area" | "angle" | "volume";
|
|
27
|
-
export type SnapKind = "point" | "line" | "face";
|
|
28
|
-
|
|
29
|
-
export type LengthUnit = "mm" | "cm" | "m" | "km";
|
|
30
|
-
export type AreaUnit = "mm2" | "cm2" | "m2" | "km2";
|
|
31
|
-
export type AngleUnit = "deg" | "rad";
|
|
32
|
-
|
|
33
|
-
/** Snapshot for W2's merged Settings panel (measurement section). */
|
|
34
|
-
export interface MeasurementSettings {
|
|
35
|
-
color: string; // "#rrggbb" — applied to all measurement types
|
|
36
|
-
units: { length: LengthUnit; area: AreaUnit; angle: AngleUnit };
|
|
37
|
-
/** Valid unit options per type (for the dropdowns). */
|
|
38
|
-
unitOptions: { length: string[]; area: string[]; angle: string[] };
|
|
39
|
-
rounding: number; // decimal places
|
|
40
|
-
snaps: Record<SnapKind, boolean>;
|
|
41
|
-
visible: boolean;
|
|
42
|
-
/** Line width (px) for Length/Area fat lines. (Angle is 1px until its arc is converted.) */
|
|
43
|
-
thickness: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface MeasurementRow {
|
|
47
|
-
key: string;
|
|
48
|
-
type: string;
|
|
49
|
-
text: string; // e.g. "5.25 m"
|
|
50
|
-
remove(): void;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface MeasurementTool {
|
|
54
|
-
readonly onChanged: OBC.Event<void>;
|
|
55
|
-
readonly onModeChanged: OBC.Event<void>;
|
|
56
|
-
/**
|
|
57
|
-
* Activate a measurement type. `subMode` forces the measurer's creation mode —
|
|
58
|
-
* "edge" for Length (snap to edges), "face" for Area (measure a face) — so the
|
|
59
|
-
* toolbar's measure-edge / measure-face actions reuse Length/Area with the right
|
|
60
|
-
* mode. Defaults each measurer back to "free" when omitted.
|
|
61
|
-
*/
|
|
62
|
-
setMode(mode: MeasureMode, subMode?: string): void;
|
|
63
|
-
/** Current measurer sub-mode (e.g. "edge"/"face"/"free"), for action isActive(). */
|
|
64
|
-
getSubMode(): string;
|
|
65
|
-
getMode(): MeasureMode;
|
|
66
|
-
rows(): MeasurementRow[];
|
|
67
|
-
/** Per-measurement rows for the Objects outliner (W2): hide / delete. */
|
|
68
|
-
instances(): InstanceRow[];
|
|
69
|
-
clearAll(): void;
|
|
70
|
-
setVisible(on: boolean): void;
|
|
71
|
-
isVisible(): boolean;
|
|
72
|
-
getSnaps(): Record<SnapKind, boolean>;
|
|
73
|
-
setSnap(kind: SnapKind, on: boolean): void;
|
|
74
|
-
// ── Settings (W2 Settings panel) ──
|
|
75
|
-
getMeasurementSettings(): MeasurementSettings;
|
|
76
|
-
setColor(hex: string): void;
|
|
77
|
-
setUnits(type: "length" | "area" | "angle", value: string): void;
|
|
78
|
-
setRounding(decimals: number): void;
|
|
79
|
-
/** Line width (px) for Length/Area fat lines. */
|
|
80
|
-
setThickness(px: number): void;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const COLOR = 0x6528d7;
|
|
84
|
-
|
|
85
|
-
type Measurer =
|
|
86
|
-
| OBF.LengthMeasurement
|
|
87
|
-
| OBF.AreaMeasurement
|
|
88
|
-
| OBF.AngleMeasurement
|
|
89
|
-
| OBF.VolumeMeasurement;
|
|
90
|
-
|
|
91
|
-
const measurementToolImpl = (
|
|
92
|
-
components: OBC.Components,
|
|
93
|
-
): MeasurementTool => {
|
|
94
|
-
const length = components.get(OBF.LengthMeasurement);
|
|
95
|
-
const area = components.get(OBF.AreaMeasurement);
|
|
96
|
-
const angle = components.get(OBF.AngleMeasurement);
|
|
97
|
-
const volume = components.get(OBF.VolumeMeasurement);
|
|
98
|
-
const all: Measurer[] = [length, area, angle, volume];
|
|
99
|
-
|
|
100
|
-
const worlds = components.get(OBC.Worlds);
|
|
101
|
-
const getWorld = () => [...worlds.list.values()][0] as OBC.World | undefined;
|
|
102
|
-
|
|
103
|
-
const onChanged = new OBC.Event<void>();
|
|
104
|
-
const onModeChanged = new OBC.Event<void>();
|
|
105
|
-
|
|
106
|
-
// Snapping: all three classes on by default (vertices, edges, faces).
|
|
107
|
-
const snaps: Record<SnapKind, boolean> = { point: true, line: true, face: true };
|
|
108
|
-
const snapClass: Record<SnapKind, FRAGS.SnappingClass> = {
|
|
109
|
-
point: FRAGS.SnappingClass.POINT,
|
|
110
|
-
line: FRAGS.SnappingClass.LINE,
|
|
111
|
-
face: FRAGS.SnappingClass.FACE,
|
|
112
|
-
};
|
|
113
|
-
const applySnaps = () => {
|
|
114
|
-
const list = (Object.keys(snaps) as SnapKind[])
|
|
115
|
-
.filter((k) => snaps[k])
|
|
116
|
-
.map((k) => snapClass[k]);
|
|
117
|
-
for (const m of all) m.snappings = list;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Stable per-measurement ids (entry object → id) so W2's outliner row keys
|
|
121
|
-
// survive deletion of OTHER rows (index-based ids would shift).
|
|
122
|
-
const entryIds = new WeakMap<object, string>();
|
|
123
|
-
let entrySeq = 0;
|
|
124
|
-
const idFor = (entry: object): string => {
|
|
125
|
-
let id = entryIds.get(entry);
|
|
126
|
-
if (!id) {
|
|
127
|
-
id = `m${++entrySeq}`;
|
|
128
|
-
entryIds.set(entry, id);
|
|
129
|
-
}
|
|
130
|
-
return id;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
let mode: MeasureMode = "none";
|
|
134
|
-
let subMode = "free"; // measurer creation mode: "free" | "edge" (Length) | "face"/"square" (Area)
|
|
135
|
-
let visible = true;
|
|
136
|
-
let canvas: HTMLElement | undefined;
|
|
137
|
-
let wired = false;
|
|
138
|
-
|
|
139
|
-
const manager = toolModeManager(components);
|
|
140
|
-
// When another tool takes over, drop measure mode locally (manager handles
|
|
141
|
-
// hover/select suppression).
|
|
142
|
-
const managed: ManagedTool = {
|
|
143
|
-
id: "measure",
|
|
144
|
-
label: () => (mode === "none" ? "Measuring" : `Measuring ${mode}`),
|
|
145
|
-
icon: "mdi:ruler",
|
|
146
|
-
onDeactivate: () => {
|
|
147
|
-
for (const m of all) {
|
|
148
|
-
m.cancelCreation();
|
|
149
|
-
// Only flip when it actually changes. Some measurers' enabled-setter has
|
|
150
|
-
// side effects — VolumeMeasurement toggles the GLOBAL Hoverer on/off — and
|
|
151
|
-
// firing that spuriously (disabling an already-disabled measurer) corrupts
|
|
152
|
-
// the hover state the toolModeManager captures, so hover stays off after
|
|
153
|
-
// returning to Select. Skipping no-op flips keeps that side effect from
|
|
154
|
-
// running unless the measurer is genuinely being turned off.
|
|
155
|
-
if (m.enabled) m.enabled = false;
|
|
156
|
-
}
|
|
157
|
-
mode = "none";
|
|
158
|
-
if (canvas) canvas.style.cursor = "";
|
|
159
|
-
onModeChanged.trigger();
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const active = (): Measurer | null =>
|
|
164
|
-
mode === "length"
|
|
165
|
-
? length
|
|
166
|
-
: mode === "area"
|
|
167
|
-
? area
|
|
168
|
-
: mode === "angle"
|
|
169
|
-
? angle
|
|
170
|
-
: mode === "volume"
|
|
171
|
-
? volume
|
|
172
|
-
: null;
|
|
173
|
-
|
|
174
|
-
const onDblClick = () => active()?.create();
|
|
175
|
-
|
|
176
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
177
|
-
if (event.code === "Enter" || event.code === "NumpadEnter") {
|
|
178
|
-
// Only the Area polygon needs an explicit close; Length (2 pts) and Angle
|
|
179
|
-
// (3 pts) finalize themselves.
|
|
180
|
-
if (mode === "area") area.endCreation();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (event.code === "Escape") {
|
|
184
|
-
active()?.cancelCreation();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (event.code === "Delete" || event.code === "Backspace") {
|
|
188
|
-
if (event.shiftKey) {
|
|
189
|
-
for (const m of all) m.list.clear();
|
|
190
|
-
} else {
|
|
191
|
-
void active()?.delete();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// Bind world + canvas + list events once the viewport exists.
|
|
197
|
-
const ensure = (): boolean => {
|
|
198
|
-
const world = getWorld();
|
|
199
|
-
if (!world) return false;
|
|
200
|
-
for (const m of all) if (m.world !== world) m.world = world;
|
|
201
|
-
if (wired) return true;
|
|
202
|
-
|
|
203
|
-
for (const m of all) m.color = new THREE.Color(COLOR);
|
|
204
|
-
angle.units = "deg";
|
|
205
|
-
applySnaps();
|
|
206
|
-
|
|
207
|
-
for (const m of all) {
|
|
208
|
-
m.list.onItemAdded.add(() => onChanged.trigger());
|
|
209
|
-
// onItemDeleted (NOT onBeforeDelete) — the panel's re-render re-reads the
|
|
210
|
-
// live list, and onBeforeDelete fires while the entry is still present, so
|
|
211
|
-
// the deleted row would linger. onItemDeleted fires after removal.
|
|
212
|
-
m.list.onItemDeleted.add(() => onChanged.trigger());
|
|
213
|
-
m.list.onCleared.add(() => onChanged.trigger());
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const renderer = world.renderer as OBF.PostproductionRenderer | undefined;
|
|
217
|
-
canvas = renderer?.three.domElement;
|
|
218
|
-
if (canvas) canvas.addEventListener("dblclick", onDblClick);
|
|
219
|
-
window.addEventListener("keydown", onKeyDown);
|
|
220
|
-
|
|
221
|
-
// Deferred-overlay registration. In the deferred pipeline the single-pass
|
|
222
|
-
// capture HIDES every non-emitter, and the measurement line (LineBasicMaterial)
|
|
223
|
-
// + area fill are non-emitters that are never redrawn — so the dimension line
|
|
224
|
-
// is invisible/faint in the deferred app (it only shows in a forward/Simple-
|
|
225
|
-
// Renderer example). Register the measurers' shared materials into the
|
|
226
|
-
// postproduction overlay set (the same mechanism clip sections / grid / hover
|
|
227
|
-
// use), depthTest/depthWrite off so they draw on top. The endpoint markers are
|
|
228
|
-
// CSS2D (rendered by three2D) and don't need this.
|
|
229
|
-
// Deferred-overlay registration + fat-line resolution must be ROBUST to
|
|
230
|
-
// timing. ensure() can run BEFORE postproduction.basePass exists; a one-shot
|
|
231
|
-
// register then silently no-ops and never retries (wired=true), leaving the
|
|
232
|
-
// measurement line OUT of isolatedMaterials → the capture hides it → invisible
|
|
233
|
-
// (while clip edges, registered later by ClipEdges at build time, show fine).
|
|
234
|
-
// So: re-run the static material registration + resolution sync before EVERY
|
|
235
|
-
// render (both idempotent/cheap) so they take effect the moment postproduction
|
|
236
|
-
// is ready. Per-instance materials (area fills, volumes) register on creation.
|
|
237
|
-
const fills = (
|
|
238
|
-
area as unknown as {
|
|
239
|
-
fills?: FRAGS.DataSet<{ material?: THREE.Material }>;
|
|
240
|
-
}
|
|
241
|
-
).fills;
|
|
242
|
-
fills?.onItemAdded.add((fill) => {
|
|
243
|
-
registerMat(fill.material);
|
|
244
|
-
getWorld()?.renderer?.update?.();
|
|
245
|
-
});
|
|
246
|
-
const volumes = (
|
|
247
|
-
volume as unknown as {
|
|
248
|
-
volumes?: FRAGS.DataSet<{ material?: THREE.Material }>;
|
|
249
|
-
}
|
|
250
|
-
).volumes;
|
|
251
|
-
volumes?.onItemAdded.add((v) => {
|
|
252
|
-
registerMat(v.material);
|
|
253
|
-
getWorld()?.renderer?.update?.();
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
registerStaticMats();
|
|
257
|
-
syncLineResolution();
|
|
258
|
-
renderer?.onBeforeUpdate?.add(() => {
|
|
259
|
-
registerStaticMats();
|
|
260
|
-
syncLineResolution();
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
wired = true;
|
|
264
|
-
return true;
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
// Live lookup of the deferred overlay material set (postproduction may not be
|
|
268
|
-
// ready at ensure() time, so never cache this).
|
|
269
|
-
const isolatedMaterials = (): THREE.Material[] | undefined => {
|
|
270
|
-
const r = getWorld()?.renderer as OBF.PostproductionRenderer | undefined;
|
|
271
|
-
try {
|
|
272
|
-
return r?.postproduction?.basePass?.isolatedMaterials;
|
|
273
|
-
} catch {
|
|
274
|
-
return undefined;
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
// Register a material into the deferred overlay (idempotent). Fat lines are also
|
|
279
|
-
// marked transparent so they sort with the hover/overlay group. No-op until the
|
|
280
|
-
// overlay set exists.
|
|
281
|
-
const registerMat = (mat?: THREE.Material) => {
|
|
282
|
-
if (!mat) return;
|
|
283
|
-
const isolated = isolatedMaterials();
|
|
284
|
-
if (!isolated) return;
|
|
285
|
-
mat.depthTest = false;
|
|
286
|
-
mat.depthWrite = false;
|
|
287
|
-
if ((mat as { isLineMaterial?: boolean }).isLineMaterial) {
|
|
288
|
-
mat.transparent = true;
|
|
289
|
-
}
|
|
290
|
-
if (!isolated.includes(mat)) isolated.push(mat);
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
// (Re)register the shared per-measurer materials: line + fill for each, plus
|
|
294
|
-
// Angle's dedicated 1px ray/arc material and Volume's shared mesh material.
|
|
295
|
-
const registerStaticMats = () => {
|
|
296
|
-
for (const m of all) {
|
|
297
|
-
registerMat((m as { linesMaterial?: THREE.Material }).linesMaterial);
|
|
298
|
-
registerMat((m as { fillsMaterial?: THREE.Material }).fillsMaterial);
|
|
299
|
-
}
|
|
300
|
-
registerMat(
|
|
301
|
-
(volume as unknown as { volumesMaterial?: THREE.Material })
|
|
302
|
-
.volumesMaterial,
|
|
303
|
-
);
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const _resScratch = new THREE.Vector2();
|
|
307
|
-
const syncLineResolution = () => {
|
|
308
|
-
const three = (getWorld()?.renderer as OBF.PostproductionRenderer | undefined)
|
|
309
|
-
?.three;
|
|
310
|
-
if (!three) return;
|
|
311
|
-
three.getDrawingBufferSize(_resScratch);
|
|
312
|
-
// Skip until the viewport is actually sized — setting resolution to (0,0) makes
|
|
313
|
-
// the fat line's screen-space quad divide by zero → NaN → invisible.
|
|
314
|
-
if (_resScratch.x < 1 || _resScratch.y < 1) return;
|
|
315
|
-
for (const m of all) {
|
|
316
|
-
const mat = (m as { linesMaterial?: unknown }).linesMaterial as
|
|
317
|
-
| { isLineMaterial?: boolean; resolution?: THREE.Vector2 }
|
|
318
|
-
| undefined;
|
|
319
|
-
if (mat?.isLineMaterial && mat.resolution) {
|
|
320
|
-
mat.resolution.set(_resScratch.x, _resScratch.y);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
const collect = (
|
|
326
|
-
out: MeasurementRow[],
|
|
327
|
-
type: string,
|
|
328
|
-
measurer: Measurer,
|
|
329
|
-
) => {
|
|
330
|
-
let i = 0;
|
|
331
|
-
for (const entry of measurer.list) {
|
|
332
|
-
const value = (entry as { value: number }).value;
|
|
333
|
-
const units = measurer.units ?? "";
|
|
334
|
-
out.push({
|
|
335
|
-
key: `${type}-${i++}`,
|
|
336
|
-
type,
|
|
337
|
-
text: `${value}${units ? ` ${units}` : ""}`,
|
|
338
|
-
// DataSet.delete(value) removes that single measurement.
|
|
339
|
-
remove: () => {
|
|
340
|
-
(measurer.list as { delete(v: unknown): boolean }).delete(entry);
|
|
341
|
-
},
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
onChanged,
|
|
348
|
-
onModeChanged,
|
|
349
|
-
setMode(next, sub) {
|
|
350
|
-
ensure();
|
|
351
|
-
for (const m of all) {
|
|
352
|
-
m.cancelCreation();
|
|
353
|
-
// Only flip when it actually changes. Some measurers' enabled-setter has
|
|
354
|
-
// side effects — VolumeMeasurement toggles the GLOBAL Hoverer on/off — and
|
|
355
|
-
// firing that spuriously (disabling an already-disabled measurer) corrupts
|
|
356
|
-
// the hover state the toolModeManager captures, so hover stays off after
|
|
357
|
-
// returning to Select. Skipping no-op flips keeps that side effect from
|
|
358
|
-
// running unless the measurer is genuinely being turned off.
|
|
359
|
-
if (m.enabled) m.enabled = false;
|
|
360
|
-
}
|
|
361
|
-
mode = next;
|
|
362
|
-
subMode = sub ?? "free";
|
|
363
|
-
const a = active();
|
|
364
|
-
if (a) {
|
|
365
|
-
a.enabled = true;
|
|
366
|
-
// Length/Area carry a creation sub-mode ("edge" / "face" / "square");
|
|
367
|
-
// force it so measure-edge / measure-face reuse those measurers correctly.
|
|
368
|
-
if (next === "length" || next === "area") {
|
|
369
|
-
(a as unknown as { mode: string }).mode = subMode;
|
|
370
|
-
}
|
|
371
|
-
// Constrain the active measurer's snap set to match the sub-mode: edge mode
|
|
372
|
-
// snaps to EDGES only (not vertices/faces), face mode to FACES only. Other
|
|
373
|
-
// modes use the user's full snap set (applySnaps below restores it).
|
|
374
|
-
if (subMode === "edge") {
|
|
375
|
-
a.snappings = [FRAGS.SnappingClass.LINE];
|
|
376
|
-
} else if (subMode === "face") {
|
|
377
|
-
a.snappings = [FRAGS.SnappingClass.FACE];
|
|
378
|
-
} else {
|
|
379
|
-
applySnaps();
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (canvas) canvas.style.cursor = a ? "crosshair" : "";
|
|
383
|
-
// Route through the central manager: a real mode makes this the sole active
|
|
384
|
-
// tool (suppressing hover/select + exiting any other tool); "none" exits.
|
|
385
|
-
if (next === "none") manager.clearActive(managed);
|
|
386
|
-
else manager.setActive(managed);
|
|
387
|
-
// Mode may have changed while staying the active tool (length → area);
|
|
388
|
-
// refresh so the HUD re-reads the dynamic label.
|
|
389
|
-
manager.refresh();
|
|
390
|
-
onModeChanged.trigger();
|
|
391
|
-
},
|
|
392
|
-
getMode: () => mode,
|
|
393
|
-
getSubMode: () => subMode,
|
|
394
|
-
rows() {
|
|
395
|
-
const out: MeasurementRow[] = [];
|
|
396
|
-
collect(out, "Length", length);
|
|
397
|
-
collect(out, "Area", area);
|
|
398
|
-
collect(out, "Angle", angle);
|
|
399
|
-
collect(out, "Volume", volume);
|
|
400
|
-
return out;
|
|
401
|
-
},
|
|
402
|
-
instances() {
|
|
403
|
-
ensure();
|
|
404
|
-
const rows: InstanceRow[] = [];
|
|
405
|
-
// Per-measurement visibility lives on the visual element, mapped from the
|
|
406
|
-
// list entry differently per type: Length/Angle's DimensionLine.line ===
|
|
407
|
-
// entry (in `lines`); Area's fill.area === entry (in `fills`); Angle stores
|
|
408
|
-
// an entry→visual map (`_visuals`) with group/label/endpoints.
|
|
409
|
-
type Adapter = {
|
|
410
|
-
get: (entry: unknown) => boolean;
|
|
411
|
-
set: (entry: unknown, on: boolean) => void;
|
|
412
|
-
};
|
|
413
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
414
|
-
const lineAdapter = (m: any): Adapter => ({
|
|
415
|
-
get: (e) =>
|
|
416
|
-
[...m.lines].find((d: any) => d.line === e)?.visible ?? true,
|
|
417
|
-
set: (e, on) => {
|
|
418
|
-
const d = [...m.lines].find((x: any) => x.line === e);
|
|
419
|
-
if (d) d.visible = on;
|
|
420
|
-
},
|
|
421
|
-
});
|
|
422
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
423
|
-
const fillAdapter = (m: any): Adapter => ({
|
|
424
|
-
get: (e) =>
|
|
425
|
-
[...m.fills].find((f: any) => f.area === e)?.visible ?? true,
|
|
426
|
-
set: (e, on) => {
|
|
427
|
-
const f = [...m.fills].find((x: any) => x.area === e);
|
|
428
|
-
if (f) f.visible = on;
|
|
429
|
-
// Area outlines + corner endpoints are separate DimensionLines mapped
|
|
430
|
-
// to the area; cascade so hiding the area hides them too (else the fill
|
|
431
|
-
// disappears but the wireframe + endpoints stay on screen). DimensionLine
|
|
432
|
-
// .visible also un-hides the per-segment length label — but area lines
|
|
433
|
-
// are created with labels off, so force them back off after showing.
|
|
434
|
-
for (const line of m.getAreaLines?.(e) ?? []) {
|
|
435
|
-
line.visible = on;
|
|
436
|
-
if (on) line.label.visible = false;
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
441
|
-
const angleAdapter = (m: any): Adapter => ({
|
|
442
|
-
get: (e) => m._visuals?.get(e)?.group?.visible ?? true,
|
|
443
|
-
set: (e, on) => {
|
|
444
|
-
const v = m._visuals?.get(e);
|
|
445
|
-
if (!v) return;
|
|
446
|
-
v.group.visible = on;
|
|
447
|
-
v.label.visible = on;
|
|
448
|
-
for (const ep of v.endpoints) ep.visible = on;
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
// Volume: MeasureVolume.volume === entry (in `volumes`).
|
|
452
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
453
|
-
const volumeAdapter = (m: any): Adapter => ({
|
|
454
|
-
get: (e) =>
|
|
455
|
-
[...m.volumes].find((v: any) => v.volume === e)?.visible ?? true,
|
|
456
|
-
set: (e, on) => {
|
|
457
|
-
const v = [...m.volumes].find((x: any) => x.volume === e);
|
|
458
|
-
if (v) v.visible = on;
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
const add = (m: Measurer, type: string, ad: Adapter) => {
|
|
462
|
-
let i = 0;
|
|
463
|
-
for (const entry of m.list) {
|
|
464
|
-
const idx = ++i;
|
|
465
|
-
const value = (entry as { value: number }).value;
|
|
466
|
-
const units = m.units ?? "";
|
|
467
|
-
rows.push({
|
|
468
|
-
id: idFor(entry as object),
|
|
469
|
-
kind: "measurement",
|
|
470
|
-
type,
|
|
471
|
-
label: `${type} ${idx} — ${value}${units ? ` ${units}` : ""}`,
|
|
472
|
-
visible: ad.get(entry),
|
|
473
|
-
setVisible: (on) => {
|
|
474
|
-
ad.set(entry, on);
|
|
475
|
-
getWorld()?.renderer?.update();
|
|
476
|
-
onChanged.trigger();
|
|
477
|
-
},
|
|
478
|
-
remove: () => {
|
|
479
|
-
(m.list as { delete(v: unknown): boolean }).delete(entry);
|
|
480
|
-
onChanged.trigger();
|
|
481
|
-
},
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
};
|
|
485
|
-
add(length, "Length", lineAdapter(length));
|
|
486
|
-
add(area, "Area", fillAdapter(area));
|
|
487
|
-
add(angle, "Angle", angleAdapter(angle));
|
|
488
|
-
add(volume, "Volume", volumeAdapter(volume));
|
|
489
|
-
return rows;
|
|
490
|
-
},
|
|
491
|
-
clearAll() {
|
|
492
|
-
for (const m of all) m.list.clear();
|
|
493
|
-
onChanged.trigger();
|
|
494
|
-
},
|
|
495
|
-
setVisible(on) {
|
|
496
|
-
visible = on;
|
|
497
|
-
for (const m of all) m.visible = on;
|
|
498
|
-
onChanged.trigger();
|
|
499
|
-
},
|
|
500
|
-
isVisible: () => visible,
|
|
501
|
-
getSnaps: () => ({ ...snaps }),
|
|
502
|
-
setSnap(kind, on) {
|
|
503
|
-
snaps[kind] = on;
|
|
504
|
-
ensure();
|
|
505
|
-
applySnaps();
|
|
506
|
-
onChanged.trigger();
|
|
507
|
-
},
|
|
508
|
-
getMeasurementSettings() {
|
|
509
|
-
ensure();
|
|
510
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
511
|
-
const u = (m: Measurer) => (m as any).units as string;
|
|
512
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
513
|
-
const opts = (m: Measurer) => [...((m as any).unitsList as string[])];
|
|
514
|
-
return {
|
|
515
|
-
color: `#${(length.color as THREE.Color).getHexString()}`,
|
|
516
|
-
units: {
|
|
517
|
-
length: u(length) as LengthUnit,
|
|
518
|
-
area: u(area) as AreaUnit,
|
|
519
|
-
angle: u(angle) as AngleUnit,
|
|
520
|
-
},
|
|
521
|
-
unitOptions: {
|
|
522
|
-
length: opts(length),
|
|
523
|
-
area: opts(area),
|
|
524
|
-
angle: opts(angle),
|
|
525
|
-
},
|
|
526
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
527
|
-
rounding: (length as any).rounding as number,
|
|
528
|
-
snaps: { ...snaps },
|
|
529
|
-
visible,
|
|
530
|
-
thickness:
|
|
531
|
-
(length.linesMaterial as unknown as { linewidth?: number })
|
|
532
|
-
.linewidth ?? 1,
|
|
533
|
-
};
|
|
534
|
-
},
|
|
535
|
-
setColor(hex) {
|
|
536
|
-
const c = new THREE.Color(hex);
|
|
537
|
-
for (const m of all) m.color = c.clone();
|
|
538
|
-
getWorld()?.renderer?.update();
|
|
539
|
-
onChanged.trigger();
|
|
540
|
-
},
|
|
541
|
-
setUnits(type, value) {
|
|
542
|
-
ensure();
|
|
543
|
-
const target = type === "length" ? length : type === "area" ? area : angle;
|
|
544
|
-
// Per-measurer `units` is a narrow string-literal union; the panel feeds a
|
|
545
|
-
// value from unitOptions for that type, so cast through unknown.
|
|
546
|
-
(target as unknown as { units: string }).units = value;
|
|
547
|
-
getWorld()?.renderer?.update();
|
|
548
|
-
onChanged.trigger();
|
|
549
|
-
},
|
|
550
|
-
setRounding(decimals) {
|
|
551
|
-
for (const m of all) {
|
|
552
|
-
(m as unknown as { rounding: number }).rounding = decimals;
|
|
553
|
-
}
|
|
554
|
-
getWorld()?.renderer?.update();
|
|
555
|
-
onChanged.trigger();
|
|
556
|
-
},
|
|
557
|
-
setThickness(px) {
|
|
558
|
-
// Applies to the fat Length/Area line materials (linewidth uniform). Angle's
|
|
559
|
-
// 1px material ignores it until its arc is converted to Line2.
|
|
560
|
-
for (const m of all) {
|
|
561
|
-
const mat = (m as { linesMaterial?: unknown }).linesMaterial as
|
|
562
|
-
| { isLineMaterial?: boolean; linewidth?: number }
|
|
563
|
-
| undefined;
|
|
564
|
-
if (mat?.isLineMaterial) mat.linewidth = px;
|
|
565
|
-
}
|
|
566
|
-
getWorld()?.renderer?.update();
|
|
567
|
-
onChanged.trigger();
|
|
568
|
-
},
|
|
569
|
-
};
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
// Real OBC.Component wrapper → per-`components` singleton resolved by UUID. The
|
|
573
|
-
// toolbar built-in and the side panels (measurement-settings, objects) share the
|
|
574
|
-
// SAME MeasurementTool because `components` is a single cross-bundle instance.
|
|
575
|
-
export class MeasurementToolComponent extends OBC.Component {
|
|
576
|
-
static readonly uuid = "9b2c7e10-3a4f-4c21-8b6d-1f0a2c3d4e52" as const;
|
|
577
|
-
enabled = true;
|
|
578
|
-
readonly tool: MeasurementTool;
|
|
579
|
-
constructor(components: OBC.Components) {
|
|
580
|
-
super(components);
|
|
581
|
-
components.add(MeasurementToolComponent.uuid, this);
|
|
582
|
-
this.tool = measurementToolImpl(components);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/** Per-Components singleton accessor for the measurement tool. */
|
|
587
|
-
export const measurementTool = (components: OBC.Components): MeasurementTool =>
|
|
588
|
-
components.get(MeasurementToolComponent).tool;
|