@thatopen/services 0.3.0 → 0.3.2

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 (42) hide show
  1. package/README.md +3 -0
  2. package/dist/built-in/index.d.ts +22 -2
  3. package/dist/cli.js +3 -3
  4. package/docs/ai-quickstart.md +86 -0
  5. package/docs/cli/create.md +4 -3
  6. package/package.json +3 -3
  7. package/src/built-in/index.ts +669 -649
  8. package/src/cli/templates/app/package.json +31 -31
  9. package/src/cli/templates/app/src/app.ts +37 -37
  10. package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -50
  11. package/src/cli/templates/app/src/main.ts +223 -340
  12. package/src/cli/templates/app/src/setups/camera-tools.ts +63 -63
  13. package/src/cli/templates/app/src/setups/clipper-tool.ts +20 -1
  14. package/src/cli/templates/app/src/setups/clipper.ts +75 -75
  15. package/src/cli/templates/app/src/setups/index.ts +23 -33
  16. package/src/cli/templates/app/src/setups/inspection.ts +136 -136
  17. package/src/cli/templates/app/src/setups/measurement-tool.ts +19 -1
  18. package/src/cli/templates/app/src/setups/measurements.ts +133 -133
  19. package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -88
  20. package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -166
  21. package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -31
  22. package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -51
  23. package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -1383
  24. package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -178
  25. package/src/cli/templates/app/src/setups/styles-panel.ts +110 -110
  26. package/src/cli/templates/app/src/setups/tool-mode-manager.ts +15 -9
  27. package/src/cli/templates/app/src/setups/tool-mode.ts +23 -23
  28. package/src/cli/templates/app/src/setups/active-tool-hud.ts +0 -69
  29. package/src/cli/templates/app/src/setups/clipper-panel.ts +0 -167
  30. package/src/cli/templates/app/src/setups/commands-panel.ts +0 -239
  31. package/src/cli/templates/app/src/setups/data-table-panel.ts +0 -2224
  32. package/src/cli/templates/app/src/setups/file-format-icons.ts +0 -95
  33. package/src/cli/templates/app/src/setups/files-panel.ts +0 -1503
  34. package/src/cli/templates/app/src/setups/fps-indicator.ts +0 -63
  35. package/src/cli/templates/app/src/setups/graphics-panel.ts +0 -490
  36. package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +0 -161
  37. package/src/cli/templates/app/src/setups/model-tree.ts +0 -797
  38. package/src/cli/templates/app/src/setups/objects-panel.ts +0 -127
  39. package/src/cli/templates/app/src/setups/properties-panel.ts +0 -1048
  40. package/src/cli/templates/app/src/setups/settings-panel.ts +0 -94
  41. package/src/cli/templates/app/src/setups/visibility-toolbar.ts +0 -498
  42. package/src/cli/templates/app/src/setups/walkthrough.ts +0 -528
@@ -1,63 +1,63 @@
1
- import * as THREE from "three";
2
- import * as OBC from "@thatopen/components";
3
-
4
- /**
5
- * Camera tools for the viewer toolbar. Returns controllers matching the
6
- * toolbar's integration contract:
7
- * - `fitAll` — ACTION: fit the camera to the whole loaded model.
8
- * - `orthoToggle` — TOGGLE: switch the projection Perspective ⇄ Orthographic.
9
- */
10
-
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
- const firstWorld = (components: OBC.Components): any =>
13
- [...components.get(OBC.Worlds).list.values()][0];
14
-
15
- export interface CameraTools {
16
- fitAll: { label: string; icon: string; run(): Promise<void> };
17
- orthoToggle: {
18
- label: string;
19
- icon: string;
20
- active(): boolean;
21
- activate(): Promise<void>;
22
- deactivate(): Promise<void>;
23
- };
24
- }
25
-
26
- export const cameraTools = (components: OBC.Components): CameraTools => {
27
- const fragments = components.get(OBC.FragmentsManager);
28
-
29
- return {
30
- fitAll: {
31
- label: "Zoom to fit",
32
- icon: "mdi:fit-to-page-outline",
33
- async run() {
34
- const controls = firstWorld(components)?.camera?.controls;
35
- if (!controls) return;
36
- // Union every loaded model's bounding box, then frame its sphere —
37
- // fitToSphere preserves the current view direction (no rotation snap).
38
- const box = new THREE.Box3();
39
- for (const model of fragments.list.values()) {
40
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
- const b = (model as any).box as THREE.Box3 | undefined;
42
- if (b && !b.isEmpty()) box.union(b);
43
- }
44
- if (box.isEmpty()) return;
45
- const sphere = box.getBoundingSphere(new THREE.Sphere());
46
- await controls.fitToSphere(sphere, true);
47
- },
48
- },
49
- orthoToggle: {
50
- label: "Orthographic",
51
- icon: "mdi:angle-right",
52
- active() {
53
- return firstWorld(components)?.camera?.projection?.current === "Orthographic";
54
- },
55
- async activate() {
56
- await firstWorld(components)?.camera?.projection?.set?.("Orthographic");
57
- },
58
- async deactivate() {
59
- await firstWorld(components)?.camera?.projection?.set?.("Perspective");
60
- },
61
- },
62
- };
63
- };
1
+ import * as THREE from "three";
2
+ import * as OBC from "@thatopen/components";
3
+
4
+ /**
5
+ * Camera tools for the viewer toolbar. Returns controllers matching the
6
+ * toolbar's integration contract:
7
+ * - `fitAll` — ACTION: fit the camera to the whole loaded model.
8
+ * - `orthoToggle` — TOGGLE: switch the projection Perspective ⇄ Orthographic.
9
+ */
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ const firstWorld = (components: OBC.Components): any =>
13
+ [...components.get(OBC.Worlds).list.values()][0];
14
+
15
+ export interface CameraTools {
16
+ fitAll: { label: string; icon: string; run(): Promise<void> };
17
+ orthoToggle: {
18
+ label: string;
19
+ icon: string;
20
+ active(): boolean;
21
+ activate(): Promise<void>;
22
+ deactivate(): Promise<void>;
23
+ };
24
+ }
25
+
26
+ export const cameraTools = (components: OBC.Components): CameraTools => {
27
+ const fragments = components.get(OBC.FragmentsManager);
28
+
29
+ return {
30
+ fitAll: {
31
+ label: "Zoom to fit",
32
+ icon: "mdi:fit-to-page-outline",
33
+ async run() {
34
+ const controls = firstWorld(components)?.camera?.controls;
35
+ if (!controls) return;
36
+ // Union every loaded model's bounding box, then frame its sphere —
37
+ // fitToSphere preserves the current view direction (no rotation snap).
38
+ const box = new THREE.Box3();
39
+ for (const model of fragments.list.values()) {
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ const b = (model as any).box as THREE.Box3 | undefined;
42
+ if (b && !b.isEmpty()) box.union(b);
43
+ }
44
+ if (box.isEmpty()) return;
45
+ const sphere = box.getBoundingSphere(new THREE.Sphere());
46
+ await controls.fitToSphere(sphere, true);
47
+ },
48
+ },
49
+ orthoToggle: {
50
+ label: "Orthographic",
51
+ icon: "mdi:angle-right",
52
+ active() {
53
+ return firstWorld(components)?.camera?.projection?.current === "Orthographic";
54
+ },
55
+ async activate() {
56
+ await firstWorld(components)?.camera?.projection?.set?.("Orthographic");
57
+ },
58
+ async deactivate() {
59
+ await firstWorld(components)?.camera?.projection?.set?.("Perspective");
60
+ },
61
+ },
62
+ };
63
+ };
@@ -89,7 +89,7 @@ export const ensureSectionStyle = (components: OBC.Components) => {
89
89
  });
90
90
  };
91
91
 
92
- export const clipperTool = (components: OBC.Components): ClipperTool => {
92
+ const clipperToolImpl = (components: OBC.Components): ClipperTool => {
93
93
  const clipper = components.get(OBC.Clipper);
94
94
  clipper.localClippingPlanes = true; // before any plane is created
95
95
  clipper.enabled = true;
@@ -752,3 +752,22 @@ export const clipperTool = (components: OBC.Components): ClipperTool => {
752
752
  isStylingVisible: () => styler.visible,
753
753
  };
754
754
  };
755
+
756
+ // Real OBC.Component wrapper so the ClipperTool wrapper is a per-`components`
757
+ // singleton, resolved by UUID via `components.get(...)`. Because `components` is
758
+ // a single shared instance across bundle boundaries, the toolbar built-in and
759
+ // the app's side panels (clipper-panel, objects) all get the SAME ClipperTool.
760
+ export class ClipperToolComponent extends OBC.Component {
761
+ static readonly uuid = "9b2c7e10-3a4f-4c21-8b6d-1f0a2c3d4e51" as const;
762
+ enabled = true;
763
+ readonly tool: ClipperTool;
764
+ constructor(components: OBC.Components) {
765
+ super(components);
766
+ components.add(ClipperToolComponent.uuid, this);
767
+ this.tool = clipperToolImpl(components);
768
+ }
769
+ }
770
+
771
+ /** Per-Components singleton accessor for the clipping tool. */
772
+ export const clipperTool = (components: OBC.Components): ClipperTool =>
773
+ components.get(ClipperToolComponent).tool;
@@ -1,75 +1,75 @@
1
- import * as OBC from "@thatopen/components";
2
- import * as OBF from "@thatopen/components-front";
3
- import type { ModeTool } from "./tool-mode";
4
-
5
- /**
6
- * Section / clipping-planes tool. Wraps `OBC.Clipper` as a toolbar mode tool:
7
- *
8
- * - double-click on the model → create a clip plane on the surface under the
9
- * pointer (raycast; the plane's normal is the hit face normal),
10
- * - drag the plane's arrow gizmo to move it along its normal (handled by the
11
- * Clipper's built-in TransformControls while the mode is active),
12
- * - Delete / Backspace → delete the plane under the cursor,
13
- * - Shift + Delete / Shift + Backspace → clear ALL section planes.
14
- *
15
- * Clipping is applied through `renderer.three.clippingPlanes` with
16
- * `localClippingEnabled = true` (set by the SimpleRenderer), so the cut is
17
- * honored during the deferred pipeline's single capture render — no
18
- * deferred-lib change is needed. (Worth an explicit live check that the section
19
- * actually cuts geometry in deferred mode.)
20
- *
21
- * Leaving the mode keeps the existing section planes cutting the model (so a
22
- * section persists while navigating); it only stops creating/dragging new ones.
23
- */
24
- export const clipper = (
25
- components: OBC.Components,
26
- world: OBC.World,
27
- ): ModeTool => {
28
- const clipperComp = components.get(OBC.Clipper);
29
-
30
- const canvas = (world.renderer as OBF.PostproductionRenderer | null)?.three
31
- .domElement;
32
- if (!canvas) {
33
- throw new Error("clipper setup: world has no renderer with a canvas");
34
- }
35
-
36
- const onDblClick = () => {
37
- void clipperComp.create(world);
38
- };
39
-
40
- const onKeyDown = (event: KeyboardEvent) => {
41
- if (event.code !== "Delete" && event.code !== "Backspace") return;
42
- if (event.shiftKey) {
43
- clipperComp.deleteAll();
44
- } else {
45
- // No-op when nothing is hovered (the Clipper raycasts for a plane).
46
- void clipperComp.delete(world);
47
- }
48
- };
49
-
50
- let active = false;
51
-
52
- return {
53
- label: "Section",
54
- icon: "mdi:scissors-cutting",
55
- activate() {
56
- if (active) return;
57
- active = true;
58
- clipperComp.enabled = true; // allow create + drag-to-move
59
- clipperComp.visible = true;
60
- canvas.addEventListener("dblclick", onDblClick);
61
- window.addEventListener("keydown", onKeyDown);
62
- canvas.style.cursor = "crosshair";
63
- },
64
- deactivate() {
65
- if (!active) return;
66
- active = false;
67
- // Stop creating / dragging, but keep the planes in the renderer's
68
- // clippingPlanes so the section stays cut while navigating.
69
- clipperComp.enabled = false;
70
- canvas.removeEventListener("dblclick", onDblClick);
71
- window.removeEventListener("keydown", onKeyDown);
72
- canvas.style.cursor = "";
73
- },
74
- };
75
- };
1
+ import * as OBC from "@thatopen/components";
2
+ import * as OBF from "@thatopen/components-front";
3
+ import type { ModeTool } from "./tool-mode";
4
+
5
+ /**
6
+ * Section / clipping-planes tool. Wraps `OBC.Clipper` as a toolbar mode tool:
7
+ *
8
+ * - double-click on the model → create a clip plane on the surface under the
9
+ * pointer (raycast; the plane's normal is the hit face normal),
10
+ * - drag the plane's arrow gizmo to move it along its normal (handled by the
11
+ * Clipper's built-in TransformControls while the mode is active),
12
+ * - Delete / Backspace → delete the plane under the cursor,
13
+ * - Shift + Delete / Shift + Backspace → clear ALL section planes.
14
+ *
15
+ * Clipping is applied through `renderer.three.clippingPlanes` with
16
+ * `localClippingEnabled = true` (set by the SimpleRenderer), so the cut is
17
+ * honored during the deferred pipeline's single capture render — no
18
+ * deferred-lib change is needed. (Worth an explicit live check that the section
19
+ * actually cuts geometry in deferred mode.)
20
+ *
21
+ * Leaving the mode keeps the existing section planes cutting the model (so a
22
+ * section persists while navigating); it only stops creating/dragging new ones.
23
+ */
24
+ export const clipper = (
25
+ components: OBC.Components,
26
+ world: OBC.World,
27
+ ): ModeTool => {
28
+ const clipperComp = components.get(OBC.Clipper);
29
+
30
+ const canvas = (world.renderer as OBF.PostproductionRenderer | null)?.three
31
+ .domElement;
32
+ if (!canvas) {
33
+ throw new Error("clipper setup: world has no renderer with a canvas");
34
+ }
35
+
36
+ const onDblClick = () => {
37
+ void clipperComp.create(world);
38
+ };
39
+
40
+ const onKeyDown = (event: KeyboardEvent) => {
41
+ if (event.code !== "Delete" && event.code !== "Backspace") return;
42
+ if (event.shiftKey) {
43
+ clipperComp.deleteAll();
44
+ } else {
45
+ // No-op when nothing is hovered (the Clipper raycasts for a plane).
46
+ void clipperComp.delete(world);
47
+ }
48
+ };
49
+
50
+ let active = false;
51
+
52
+ return {
53
+ label: "Section",
54
+ icon: "mdi:scissors-cutting",
55
+ activate() {
56
+ if (active) return;
57
+ active = true;
58
+ clipperComp.enabled = true; // allow create + drag-to-move
59
+ clipperComp.visible = true;
60
+ canvas.addEventListener("dblclick", onDblClick);
61
+ window.addEventListener("keydown", onKeyDown);
62
+ canvas.style.cursor = "crosshair";
63
+ },
64
+ deactivate() {
65
+ if (!active) return;
66
+ active = false;
67
+ // Stop creating / dragging, but keep the planes in the renderer's
68
+ // clippingPlanes so the section stays cut while navigating.
69
+ clipperComp.enabled = false;
70
+ canvas.removeEventListener("dblclick", onDblClick);
71
+ window.removeEventListener("keydown", onKeyDown);
72
+ canvas.style.cursor = "";
73
+ },
74
+ };
75
+ };
@@ -1,33 +1,23 @@
1
- export * from "./ui-manager";
2
- export * from "./cloud-runner";
3
- export * from "./properties-panel";
4
- export * from "./fps-indicator";
5
- export * from "./active-tool-hud";
6
- export * from "./files-panel";
7
- export * from "./model-tree";
8
- export * from "./right-sidebar";
9
- export * from "./card-header";
10
- export * from "./toolbar";
11
- export * from "./visibility-toolbar";
12
- export * from "./hider";
13
- export * from "./tool-mode";
14
- export * from "./clipper";
15
- export * from "./measurements";
16
- export * from "./styles";
17
- export * from "./helper-panel";
18
- export * from "./styles-panel";
19
- export * from "./graphics-panel";
20
- export * from "./commands-panel";
21
- export * from "./clipper-tool";
22
- export * from "./clipper-panel";
23
- export * from "./plans-panel";
24
- export * from "./navigation-gizmo";
25
- export * from "./exploded-view";
26
- export * from "./measurement-tool";
27
- export * from "./measurement-panel";
28
- export * from "./data-table-panel";
29
- export * from "./walkthrough";
30
- export * from "./tool-mode-manager";
31
- // reality-capture-viewer is intentionally NOT re-exported here: it spins up
32
- // decode workers at module load, so it must be LAZY-imported (dynamic import in
33
- // files-panel's .3tz "View" handler), never pulled onto the app boot path.
1
+ export * from "./ui-manager";
2
+ export * from "./cloud-runner";
3
+ export * from "./right-sidebar";
4
+ export * from "./card-header";
5
+ export * from "./toolbar";
6
+ export * from "./hider";
7
+ export * from "./tool-mode";
8
+ export * from "./clipper";
9
+ export * from "./measurements";
10
+ export * from "./styles";
11
+ export * from "./helper-panel";
12
+ export * from "./styles-panel";
13
+ export * from "./clipper-tool";
14
+ export * from "./plans-panel";
15
+ export * from "./navigation-gizmo";
16
+ export * from "./exploded-view";
17
+ export * from "./measurement-tool";
18
+ export * from "./measurement-panel";
19
+ export * from "./tool-mode-manager";
20
+ // reality-capture-viewer is intentionally NOT re-exported here: it spins up
21
+ // decode workers at module load, so it must be LAZY-imported (dynamic import in
22
+ // main.ts's app-registered .3tz loader for <top-models-list>), never pulled onto
23
+ // the app boot path.
@@ -1,136 +1,136 @@
1
- import * as OBC from "@thatopen/components";
2
- import type { ClipperTool } from "./clipper-tool";
3
- import type { MeasurementTool } from "./measurement-tool";
4
-
5
- /**
6
- * Unified "Objects" outliner API (W2's new panel). Enumerates every created clip
7
- * plane and every measurement as a flat list of {@link InstanceRow}s with
8
- * per-instance HIDE / DISABLE / DELETE, so W2 can render rows without knowing
9
- * about the individual Clipping / Measurement tools.
10
- *
11
- * Shapes are owned here and produced by each tool's `instances()`; this module
12
- * just merges them and fans the two tools' change events into one.
13
- */
14
- export type InstanceKind = "clip" | "measurement";
15
-
16
- export interface InstanceRow {
17
- /** Stable id (plane id for clips; per-measurement id) — use as the row key. */
18
- id: string;
19
- kind: InstanceKind;
20
- /** "Clip plane" | "Length" | "Area" | "Angle". */
21
- type: string;
22
- /** User-facing row label (includes the measured value for measurements). */
23
- label: string;
24
- /** Currently shown in the scene. */
25
- visible: boolean;
26
- /** Clip planes only: whether the plane is actively cutting. `undefined` for measurements. */
27
- enabled?: boolean;
28
- /** Show/hide this instance without deleting it. */
29
- setVisible(on: boolean): void;
30
- /** Clip planes only: toggle the cut on/off (present only when `enabled` is defined). */
31
- setEnabled?(on: boolean): void;
32
- /** Permanently remove this instance. */
33
- remove(): void;
34
- }
35
-
36
- export interface InspectionInstances {
37
- /** Snapshot of all clip planes + measurements, in that order. */
38
- list(): InstanceRow[];
39
- /** Fires whenever the set or state of instances changes (re-query `list()`). */
40
- readonly onChanged: OBC.Event<void>;
41
- }
42
-
43
- /**
44
- * A toolbar action W2 wires into the new "Inspection" tab. Each `activate()`
45
- * routes through the toolModeManager (exclusive — entering one exits any other
46
- * tool). W2 drives the Select button + active highlighting via
47
- * `toolModeManager.selectMode()` / `getActiveId()` / `onActiveChanged`; each
48
- * action's own `isActive()` is provided for convenience.
49
- */
50
- export interface InspectionAction {
51
- id: string;
52
- label: string;
53
- /** mdi icon name (e.g. "mdi:ruler"). */
54
- icon: string;
55
- activate(): void;
56
- isActive(): boolean;
57
- }
58
-
59
- /**
60
- * Inspection toolbar actions: clip plane + the measurement types/modes. Order is
61
- * the toolbar order. measure-edge/face reuse Length/Area with a forced sub-mode;
62
- * measure-length/area are the plain "free" modes (isActive distinguishes them by
63
- * sub-mode). Volume is a distinct measurer.
64
- */
65
- export const inspectionActions = (
66
- clipperTool: ClipperTool,
67
- measurementTool: MeasurementTool,
68
- ): InspectionAction[] => {
69
- const m = measurementTool;
70
- const isMeasure = (mode: string, sub: string) =>
71
- m.getMode() === mode && m.getSubMode() === sub;
72
- return [
73
- {
74
- id: "clip-plane",
75
- label: "Clip plane",
76
- icon: "mdi:scissors-cutting",
77
- activate: () => clipperTool.setPlacing(true),
78
- isActive: () => clipperTool.isPlacing(),
79
- },
80
- {
81
- id: "measure-length",
82
- label: "Length",
83
- icon: "mdi:ruler",
84
- activate: () => m.setMode("length", "free"),
85
- isActive: () => isMeasure("length", "free"),
86
- },
87
- {
88
- id: "measure-area",
89
- label: "Area",
90
- icon: "mdi:vector-polygon",
91
- activate: () => m.setMode("area", "free"),
92
- isActive: () => isMeasure("area", "free"),
93
- },
94
- {
95
- id: "measure-angle",
96
- label: "Angle",
97
- icon: "mdi:angle-acute",
98
- activate: () => m.setMode("angle"),
99
- isActive: () => m.getMode() === "angle",
100
- },
101
- {
102
- id: "measure-edge",
103
- label: "Edge",
104
- icon: "mdi:vector-line",
105
- activate: () => m.setMode("length", "edge"),
106
- isActive: () => isMeasure("length", "edge"),
107
- },
108
- {
109
- id: "measure-face",
110
- label: "Face",
111
- icon: "mdi:vector-square",
112
- activate: () => m.setMode("area", "face"),
113
- isActive: () => isMeasure("area", "face"),
114
- },
115
- {
116
- id: "volume",
117
- label: "Volume",
118
- icon: "mdi:cube-outline",
119
- activate: () => m.setMode("volume"),
120
- isActive: () => m.getMode() === "volume",
121
- },
122
- ];
123
- };
124
-
125
- export const inspectionInstances = (
126
- clipperTool: ClipperTool,
127
- measurementTool: MeasurementTool,
128
- ): InspectionInstances => {
129
- const onChanged = new OBC.Event<void>();
130
- clipperTool.onChanged.add(() => onChanged.trigger());
131
- measurementTool.onChanged.add(() => onChanged.trigger());
132
- return {
133
- list: () => [...clipperTool.instances(), ...measurementTool.instances()],
134
- onChanged,
135
- };
136
- };
1
+ import * as OBC from "@thatopen/components";
2
+ import type { ClipperTool } from "./clipper-tool";
3
+ import type { MeasurementTool } from "./measurement-tool";
4
+
5
+ /**
6
+ * Unified "Objects" outliner API (W2's new panel). Enumerates every created clip
7
+ * plane and every measurement as a flat list of {@link InstanceRow}s with
8
+ * per-instance HIDE / DISABLE / DELETE, so W2 can render rows without knowing
9
+ * about the individual Clipping / Measurement tools.
10
+ *
11
+ * Shapes are owned here and produced by each tool's `instances()`; this module
12
+ * just merges them and fans the two tools' change events into one.
13
+ */
14
+ export type InstanceKind = "clip" | "measurement";
15
+
16
+ export interface InstanceRow {
17
+ /** Stable id (plane id for clips; per-measurement id) — use as the row key. */
18
+ id: string;
19
+ kind: InstanceKind;
20
+ /** "Clip plane" | "Length" | "Area" | "Angle". */
21
+ type: string;
22
+ /** User-facing row label (includes the measured value for measurements). */
23
+ label: string;
24
+ /** Currently shown in the scene. */
25
+ visible: boolean;
26
+ /** Clip planes only: whether the plane is actively cutting. `undefined` for measurements. */
27
+ enabled?: boolean;
28
+ /** Show/hide this instance without deleting it. */
29
+ setVisible(on: boolean): void;
30
+ /** Clip planes only: toggle the cut on/off (present only when `enabled` is defined). */
31
+ setEnabled?(on: boolean): void;
32
+ /** Permanently remove this instance. */
33
+ remove(): void;
34
+ }
35
+
36
+ export interface InspectionInstances {
37
+ /** Snapshot of all clip planes + measurements, in that order. */
38
+ list(): InstanceRow[];
39
+ /** Fires whenever the set or state of instances changes (re-query `list()`). */
40
+ readonly onChanged: OBC.Event<void>;
41
+ }
42
+
43
+ /**
44
+ * A toolbar action W2 wires into the new "Inspection" tab. Each `activate()`
45
+ * routes through the toolModeManager (exclusive — entering one exits any other
46
+ * tool). W2 drives the Select button + active highlighting via
47
+ * `toolModeManager.selectMode()` / `getActiveId()` / `onActiveChanged`; each
48
+ * action's own `isActive()` is provided for convenience.
49
+ */
50
+ export interface InspectionAction {
51
+ id: string;
52
+ label: string;
53
+ /** mdi icon name (e.g. "mdi:ruler"). */
54
+ icon: string;
55
+ activate(): void;
56
+ isActive(): boolean;
57
+ }
58
+
59
+ /**
60
+ * Inspection toolbar actions: clip plane + the measurement types/modes. Order is
61
+ * the toolbar order. measure-edge/face reuse Length/Area with a forced sub-mode;
62
+ * measure-length/area are the plain "free" modes (isActive distinguishes them by
63
+ * sub-mode). Volume is a distinct measurer.
64
+ */
65
+ export const inspectionActions = (
66
+ clipperTool: ClipperTool,
67
+ measurementTool: MeasurementTool,
68
+ ): InspectionAction[] => {
69
+ const m = measurementTool;
70
+ const isMeasure = (mode: string, sub: string) =>
71
+ m.getMode() === mode && m.getSubMode() === sub;
72
+ return [
73
+ {
74
+ id: "clip-plane",
75
+ label: "Clip plane",
76
+ icon: "mdi:scissors-cutting",
77
+ activate: () => clipperTool.setPlacing(true),
78
+ isActive: () => clipperTool.isPlacing(),
79
+ },
80
+ {
81
+ id: "measure-length",
82
+ label: "Length",
83
+ icon: "mdi:ruler",
84
+ activate: () => m.setMode("length", "free"),
85
+ isActive: () => isMeasure("length", "free"),
86
+ },
87
+ {
88
+ id: "measure-area",
89
+ label: "Area",
90
+ icon: "mdi:vector-polygon",
91
+ activate: () => m.setMode("area", "free"),
92
+ isActive: () => isMeasure("area", "free"),
93
+ },
94
+ {
95
+ id: "measure-angle",
96
+ label: "Angle",
97
+ icon: "mdi:angle-acute",
98
+ activate: () => m.setMode("angle"),
99
+ isActive: () => m.getMode() === "angle",
100
+ },
101
+ {
102
+ id: "measure-edge",
103
+ label: "Edge",
104
+ icon: "mdi:vector-line",
105
+ activate: () => m.setMode("length", "edge"),
106
+ isActive: () => isMeasure("length", "edge"),
107
+ },
108
+ {
109
+ id: "measure-face",
110
+ label: "Face",
111
+ icon: "mdi:vector-square",
112
+ activate: () => m.setMode("area", "face"),
113
+ isActive: () => isMeasure("area", "face"),
114
+ },
115
+ {
116
+ id: "volume",
117
+ label: "Volume",
118
+ icon: "mdi:cube-outline",
119
+ activate: () => m.setMode("volume"),
120
+ isActive: () => m.getMode() === "volume",
121
+ },
122
+ ];
123
+ };
124
+
125
+ export const inspectionInstances = (
126
+ clipperTool: ClipperTool,
127
+ measurementTool: MeasurementTool,
128
+ ): InspectionInstances => {
129
+ const onChanged = new OBC.Event<void>();
130
+ clipperTool.onChanged.add(() => onChanged.trigger());
131
+ measurementTool.onChanged.add(() => onChanged.trigger());
132
+ return {
133
+ list: () => [...clipperTool.instances(), ...measurementTool.instances()],
134
+ onChanged,
135
+ };
136
+ };