@thatopen/services 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +59 -59
  2. package/dist/built-in/index.d.ts +486 -563
  3. package/dist/cli.js +10 -0
  4. package/dist/index.cjs.js +2 -2
  5. package/dist/index.es.js +3 -5
  6. package/dist/vite-env.d.ts +1 -1
  7. package/docs/builtin/paths.json +74 -70
  8. package/docs/cli/create.md +28 -28
  9. package/docs/cli/local-server.md +38 -38
  10. package/docs/cli/login.md +32 -32
  11. package/docs/cli/publish.md +31 -31
  12. package/docs/cli/run.md +26 -26
  13. package/docs/cli/serve.md +43 -43
  14. package/docs/cli/swap.md +30 -30
  15. package/package.json +100 -100
  16. package/resources/AGENTS.md +40 -40
  17. package/src/built-in/CDEManager/example.ts +214 -214
  18. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  19. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  20. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  21. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  22. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  23. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  24. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  25. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  26. package/src/built-in/UIManager/src/app/example.ts +99 -99
  27. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
  28. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
  29. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
  30. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
  31. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
  32. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
  33. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
  34. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
  35. package/src/built-in/index.ts +649 -755
  36. package/src/cli/templates/app/README.md +241 -241
  37. package/src/cli/templates/app/index.html +35 -27
  38. package/src/cli/templates/app/package.json +31 -27
  39. package/src/cli/templates/app/src/app.ts +37 -0
  40. package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
  41. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
  42. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  43. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  44. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  45. package/src/cli/templates/app/src/globals.ts +1 -0
  46. package/src/cli/templates/app/src/main.ts +340 -56
  47. package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
  48. package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
  49. package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
  50. package/src/cli/templates/app/src/setups/card-header.ts +35 -0
  51. package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
  52. package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
  53. package/src/cli/templates/app/src/setups/clipper.ts +75 -0
  54. package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
  55. package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
  56. package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
  57. package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
  58. package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
  59. package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
  60. package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
  61. package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
  62. package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
  63. package/src/cli/templates/app/src/setups/hider.ts +98 -0
  64. package/src/cli/templates/app/src/setups/index.ts +33 -3
  65. package/src/cli/templates/app/src/setups/inspection.ts +136 -0
  66. package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
  67. package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
  68. package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
  69. package/src/cli/templates/app/src/setups/measurements.ts +133 -0
  70. package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
  71. package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
  72. package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
  73. package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
  74. package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
  75. package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
  76. package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
  77. package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
  78. package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
  79. package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
  80. package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
  81. package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
  82. package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
  83. package/src/cli/templates/app/src/setups/styles.ts +375 -0
  84. package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
  85. package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
  86. package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
  87. package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
  88. package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
  89. package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
  90. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
  91. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
  92. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
  93. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
  94. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
  95. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
  96. package/src/cli/templates/app/src/ui-components/index.ts +2 -1
  97. package/src/cli/templates/app/tsconfig.json +16 -17
  98. package/src/cli/templates/app/vite.config.js +45 -42
  99. package/src/cli/templates/cloud-component/README.md +205 -205
  100. package/src/cli/templates/cloud-component/_thatopen +5 -5
  101. package/src/cli/templates/cloud-component/declarations.json +4 -4
  102. package/src/cli/templates/cloud-component/package.json +21 -21
  103. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  104. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  105. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  106. package/src/cli/templates/shared/AGENTS.md +8 -8
  107. package/src/cli/templates/shared/CLAUDE.md +1 -1
  108. package/src/cli/templates/shared/_gitignore +5 -5
  109. package/src/built-in/AppManager/example.ts +0 -86
  110. package/src/built-in/FileList/example.ts +0 -107
  111. package/src/built-in/TabbedNavigation/example.ts +0 -115
  112. package/src/built-in/ViewportsManager/example.ts +0 -106
  113. package/src/cli/templates/app/src/setups/fragments.ts +0 -18
  114. package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
  115. package/src/cli/templates/app/src/ui-components/AppPanel/index.ts +0 -160
@@ -0,0 +1,375 @@
1
+ import * as THREE from "three";
2
+ import * as OBC from "@thatopen/components";
3
+ import * as OBF from "@thatopen/components-front";
4
+
5
+ /**
6
+ * STYLES tool — a typed, generic descriptor over the viewer's visual settings so
7
+ * a UI (the bottom-left helper panel) can render controls without poking the
8
+ * postproduction renderer internals.
9
+ *
10
+ * Every entry reads/writes LIVE: the deferred composite re-reads its `settings`
11
+ * each frame, the AO/FXAA flags are read per-frame, and `renderScale` /
12
+ * `adaptiveResolution` apply on assignment — so no rebuild or lib change is
13
+ * needed. Mutating a setting takes effect on the next rendered frame.
14
+ *
15
+ * NOTE on presets: setting `Style preset` runs the engine's
16
+ * `_applyDeferredStyle`, which overwrites `Surface color`, `Edges` and
17
+ * `Ambient occlusion` to the preset's combination. After changing the preset,
18
+ * re-read all settings to refresh the panel.
19
+ */
20
+ export type StyleSetting =
21
+ | {
22
+ key: string;
23
+ label: string;
24
+ group: string;
25
+ type: "bool";
26
+ default: boolean;
27
+ get(): boolean;
28
+ set(v: boolean): void;
29
+ }
30
+ | {
31
+ key: string;
32
+ label: string;
33
+ group: string;
34
+ type: "number";
35
+ default: number;
36
+ min: number;
37
+ max: number;
38
+ step: number;
39
+ get(): number;
40
+ set(v: number): void;
41
+ }
42
+ | {
43
+ key: string;
44
+ label: string;
45
+ group: string;
46
+ type: "color"; // value is a "#rrggbb" hex string
47
+ default: string;
48
+ get(): string;
49
+ set(v: string): void;
50
+ }
51
+ | {
52
+ key: string;
53
+ label: string;
54
+ group: string;
55
+ type: "enum";
56
+ default: string | number;
57
+ options: { label: string; value: string | number }[];
58
+ get(): string | number;
59
+ set(v: string | number): void;
60
+ };
61
+
62
+ const hex = (c: THREE.Color) => `#${c.getHexString()}`;
63
+
64
+ // Background state is MODULE-scoped so it survives styles() re-creation (the
65
+ // graphics panel rebuilds the descriptor on every refresh — a per-instance
66
+ // `lastBg` re-clobbered a user-picked background to 0x202020 one frame after the
67
+ // pick, #30). `stylesLastBg` is the remembered OPAQUE colour: it persists even
68
+ // while transparent mode zeroes the renderer's clear colour, so toggling back to
69
+ // opaque restores the picked hue. The 0x202020 default is applied to the
70
+ // renderer exactly once.
71
+ let stylesBgInitialized = false;
72
+ const stylesLastBg = new THREE.Color(0x202020);
73
+
74
+ export const styles = (
75
+ components: OBC.Components,
76
+ world: OBC.World,
77
+ ): { settings: StyleSetting[] } => {
78
+ const renderer = world.renderer as OBF.PostproductionRenderer;
79
+
80
+ // Lazy accessors — resolved at call time so they always hit the live objects
81
+ // (the deferred pipeline is allocated once postproduction is up).
82
+ const pp = () => renderer.postproduction;
83
+ const deferred = () => renderer.postproduction.deferred;
84
+ const composite = () => renderer.postproduction.deferred.composite.settings;
85
+ const grid = () => components.get(OBC.Grids).list.get(world.uuid);
86
+ const scene = world.scene.three as THREE.Scene;
87
+ const three = () => renderer.three as THREE.WebGLRenderer;
88
+
89
+ // Background is driven through the RENDERER CLEAR colour/alpha, not
90
+ // scene.background. Under the deferred pen pipeline the composite passes the
91
+ // captured clear straight through for background pixels, so the clear colour IS
92
+ // the viewport background; setting scene.background to a THREE.Color instead
93
+ // makes three force-clear the capture G-buffer mid-pass and the whole model
94
+ // vanishes (#30). Keep scene.background null and own the clear here.
95
+ scene.background = null;
96
+ // Apply the default background ONCE (never on a rebuild — that was the #30
97
+ // reset). Default is TRANSPARENT (clear alpha 0) to match the
98
+ // `transparentBackground` setting's default:true, so on load the viewport shows
99
+ // whatever is behind it instead of an opaque black. The remembered hue lives in
100
+ // the module-scoped `stylesLastBg` for when transparency is toggled off.
101
+ if (!stylesBgInitialized) {
102
+ stylesBgInitialized = true;
103
+ three().setClearColor(0x000000, 0);
104
+ }
105
+
106
+ const A = OBF.PostproductionAspect;
107
+
108
+ const settings: StyleSetting[] = [
109
+ // ── Preset ──────────────────────────────────────────────────────
110
+ {
111
+ key: "preset",
112
+ label: "Style preset",
113
+ group: "Preset",
114
+ type: "enum",
115
+ default: A.COLOR_PEN_SHADOWS,
116
+ options: [
117
+ { label: "Color + edges + shadows", value: A.COLOR_PEN_SHADOWS },
118
+ { label: "Color", value: A.COLOR },
119
+ { label: "Color + edges", value: A.COLOR_PEN },
120
+ { label: "Color + shadows", value: A.COLOR_SHADOWS },
121
+ { label: "Pen (edges only)", value: A.PEN },
122
+ { label: "Pen + shadows", value: A.PEN_SHADOWS },
123
+ ],
124
+ get: () => pp().style,
125
+ set: (v) => {
126
+ pp().style = v as OBF.PostproductionAspect;
127
+ },
128
+ },
129
+ {
130
+ key: "postproductionEnabled",
131
+ label: "Postproduction",
132
+ group: "Preset",
133
+ type: "bool",
134
+ default: true,
135
+ get: () => pp().enabled,
136
+ set: (v) => {
137
+ pp().enabled = v;
138
+ },
139
+ },
140
+
141
+ // ── Edges (contour) ─────────────────────────────────────────────
142
+ {
143
+ key: "edges",
144
+ label: "Edges",
145
+ group: "Edges",
146
+ type: "bool",
147
+ default: true,
148
+ get: () => composite().contourEnabled,
149
+ set: (v) => {
150
+ composite().contourEnabled = v;
151
+ },
152
+ },
153
+ {
154
+ key: "edgeColor",
155
+ label: "Edge color",
156
+ group: "Edges",
157
+ type: "color",
158
+ default: "#000000",
159
+ get: () => hex(composite().edgeColor),
160
+ set: (v) => composite().edgeColor.set(v),
161
+ },
162
+ {
163
+ key: "edgeStrength",
164
+ label: "Edge strength",
165
+ group: "Edges",
166
+ type: "number",
167
+ default: 0.8,
168
+ min: 0,
169
+ max: 1,
170
+ step: 0.05,
171
+ get: () => composite().edgeStrength,
172
+ set: (v) => {
173
+ composite().edgeStrength = v;
174
+ },
175
+ },
176
+
177
+ // ── Shading ─────────────────────────────────────────────────────
178
+ {
179
+ key: "surfaceColor",
180
+ label: "Surface color (off = paper)",
181
+ group: "Shading",
182
+ type: "bool",
183
+ default: true,
184
+ get: () => composite().colorEnabled,
185
+ set: (v) => {
186
+ composite().colorEnabled = v;
187
+ },
188
+ },
189
+ {
190
+ key: "ao",
191
+ label: "Ambient occlusion",
192
+ group: "Shading",
193
+ type: "bool",
194
+ default: true,
195
+ get: () => deferred().settings.aoEnabled,
196
+ set: (v) => {
197
+ deferred().settings.aoEnabled = v;
198
+ },
199
+ },
200
+ {
201
+ key: "aoStrength",
202
+ label: "AO strength",
203
+ group: "Shading",
204
+ type: "number",
205
+ default: 1.0,
206
+ min: 0,
207
+ max: 2,
208
+ step: 0.05,
209
+ get: () => composite().aoStrength,
210
+ set: (v) => {
211
+ composite().aoStrength = v;
212
+ },
213
+ },
214
+ {
215
+ key: "tonalShading",
216
+ label: "Tonal shading",
217
+ group: "Shading",
218
+ type: "bool",
219
+ default: true,
220
+ get: () => composite().tonalShadingEnabled,
221
+ set: (v) => {
222
+ composite().tonalShadingEnabled = v;
223
+ },
224
+ },
225
+ {
226
+ key: "tonalFloor",
227
+ label: "Tonal floor",
228
+ group: "Shading",
229
+ type: "number",
230
+ default: 0.7,
231
+ min: 0,
232
+ max: 1,
233
+ step: 0.05,
234
+ get: () => composite().tonalFloor,
235
+ set: (v) => {
236
+ composite().tonalFloor = v;
237
+ },
238
+ },
239
+
240
+ // ── Scene ───────────────────────────────────────────────────────
241
+ {
242
+ key: "transparentBackground",
243
+ label: "Transparent background",
244
+ group: "Scene",
245
+ type: "bool",
246
+ default: true,
247
+ // Transparent === clear alpha 0 (page/scene behind shows through).
248
+ get: () => three().getClearAlpha() === 0,
249
+ set: (v) => {
250
+ scene.background = null;
251
+ if (v) {
252
+ // TRUE transparency: zero the RGB too, not just alpha. The composite
253
+ // passes the clear straight through and the canvas composites
254
+ // premultiplied, so a non-zero rgb at alpha 0 ADDS that colour over the
255
+ // page (the picked colour bled through as a tint). (0,0,0,0) = clean
256
+ // page-through with no tint. The hue is remembered in stylesLastBg.
257
+ three().setClearColor(0x000000, 0);
258
+ } else {
259
+ three().setClearColor(stylesLastBg, 1);
260
+ }
261
+ },
262
+ },
263
+ {
264
+ key: "backgroundColor",
265
+ label: "Background color",
266
+ group: "Scene",
267
+ type: "color",
268
+ // Reflect the remembered hue even while transparent (clear rgb is zeroed).
269
+ default: `#${stylesLastBg.getHexString()}`,
270
+ get: () => hex(stylesLastBg),
271
+ set: (v) => {
272
+ stylesLastBg.set(v);
273
+ scene.background = null;
274
+ // Choosing a colour means you want to SEE it → make the background
275
+ // opaque (alpha 1). The Transparent toggle re-reads alpha on refresh and
276
+ // flips off to match.
277
+ three().setClearColor(stylesLastBg, 1);
278
+ },
279
+ },
280
+ {
281
+ key: "grid",
282
+ label: "Grid",
283
+ group: "Scene",
284
+ type: "bool",
285
+ default: true,
286
+ get: () => grid()?.config.visible ?? false,
287
+ set: (v) => {
288
+ const g = grid();
289
+ if (g) g.config.visible = v;
290
+ },
291
+ },
292
+ {
293
+ key: "gridColor",
294
+ label: "Grid color",
295
+ group: "Scene",
296
+ type: "color",
297
+ default: "#d3d3d3",
298
+ get: () => {
299
+ const g = grid();
300
+ return g ? hex(g.config.color) : "#d3d3d3";
301
+ },
302
+ set: (v) => {
303
+ const g = grid();
304
+ if (g) g.config.color = new THREE.Color(v);
305
+ },
306
+ },
307
+
308
+ // ── Quality / performance ───────────────────────────────────────
309
+ {
310
+ key: "fxaa",
311
+ label: "Anti-aliasing (FXAA)",
312
+ group: "Quality",
313
+ type: "bool",
314
+ default: true,
315
+ get: () => deferred().settings.fxaaEnabled,
316
+ set: (v) => {
317
+ deferred().settings.fxaaEnabled = v;
318
+ },
319
+ },
320
+ {
321
+ key: "renderScale",
322
+ label: "Render scale",
323
+ group: "Quality",
324
+ type: "number",
325
+ default: 1.0,
326
+ min: 0.25,
327
+ max: 1,
328
+ step: 0.05,
329
+ get: () => deferred().renderScale,
330
+ set: (v) => {
331
+ deferred().renderScale = v;
332
+ },
333
+ },
334
+ {
335
+ key: "adaptiveResolution",
336
+ label: "Adaptive resolution",
337
+ group: "Quality",
338
+ type: "bool",
339
+ default: true,
340
+ get: () => renderer.adaptiveResolution,
341
+ set: (v) => {
342
+ renderer.adaptiveResolution = v;
343
+ },
344
+ },
345
+ {
346
+ key: "targetFps",
347
+ label: "Target FPS",
348
+ group: "Quality",
349
+ type: "number",
350
+ default: 60,
351
+ min: 24,
352
+ max: 120,
353
+ step: 5,
354
+ get: () => renderer.adaptiveTargetFps,
355
+ set: (v) => {
356
+ renderer.adaptiveTargetFps = v;
357
+ },
358
+ },
359
+ {
360
+ key: "highResOutline",
361
+ label: "High-res selection outline",
362
+ group: "Quality",
363
+ type: "bool",
364
+ default: true,
365
+ // false → render the selection outline at half resolution (cheaper when a
366
+ // selection covers much of the screen, slightly softer); true → full res.
367
+ get: () => components.get(OBF.Outliner).resolutionScale >= 1,
368
+ set: (v) => {
369
+ components.get(OBF.Outliner).resolutionScale = v ? 1 : 0.5;
370
+ },
371
+ },
372
+ ];
373
+
374
+ return { settings };
375
+ };
@@ -0,0 +1,150 @@
1
+ import * as OBC from "@thatopen/components";
2
+ import * as OBF from "@thatopen/components-front";
3
+
4
+ /**
5
+ * Central single-active-tool manager (explicit-state discipline per the grimoire
6
+ * interaction-editing/modal-tools/state-machine-discipline): exactly ONE modal
7
+ * tool (Section / Measure / future walkthrough …) is active at a time, and while
8
+ * ANY tool is active the viewer's passive interactions — the Hoverer overlay and
9
+ * the Highlighter (hover + click-select) — are suppressed so they don't fight the
10
+ * tool's own picking. Both are restored when the last tool exits.
11
+ *
12
+ * A tool registers a {@link ManagedTool} and calls {@link setActive} on enter /
13
+ * {@link clearActive} on exit. When another tool takes over, the manager calls
14
+ * the previous tool's {@link ManagedTool.onDeactivate} (LOCAL teardown only — it
15
+ * must not call back into the manager, to avoid re-entrancy).
16
+ *
17
+ * Reachable as a per-Components singleton via {@link toolModeManager} so panels
18
+ * (and later walkthrough/others) can route through the same instance. (Flagging:
19
+ * a plain memoised singleton, not an OBC.Component — say if you'd rather it be
20
+ * components.get()-able and I'll wrap it.)
21
+ */
22
+ export interface ManagedTool {
23
+ readonly id: string;
24
+ /**
25
+ * Human-readable description of what the user is doing while this tool is
26
+ * active (e.g. "Drawing clipping plane", "Measuring length"). Surfaced by the
27
+ * active-tool HUD — which only reads this, so any future tool that registers a
28
+ * label shows up there with no HUD changes. May be a function for tools whose
29
+ * label varies while active (call {@link ToolModeManager.refresh} to update).
30
+ */
31
+ readonly label: string | (() => string);
32
+ /** Optional mdi icon name (e.g. "mdi:scissors-cutting") shown next to the label. */
33
+ readonly icon?: string;
34
+ /** Called when another tool takes over; do LOCAL teardown only (no manager calls). */
35
+ onDeactivate(): void;
36
+ }
37
+
38
+ export class ToolModeManager {
39
+ // Always a real tool — the resting default is `selectTool` (never null), so
40
+ // SELECT is a first-class state: reflected by the HUD, and entered via the same
41
+ // setActive() exclusivity path as every other tool (so clicking Select
42
+ // deactivates whatever modal tool was active, just like clip↔measure).
43
+ private _active: ManagedTool;
44
+ private _suppressed = false;
45
+ private _prevHovererEnabled = true;
46
+ private _prevHighlighterEnabled = true;
47
+
48
+ /** Fires whenever the active tool changes (carries the new active tool, which
49
+ * is `selectTool` in the default mode). `| null` kept for HUD back-compat. */
50
+ readonly onActiveChanged = new OBC.Event<ManagedTool | null>();
51
+
52
+ /**
53
+ * The default SELECT tool — plain object hover + click-select. It is the
54
+ * resting active tool (on load and after any modal tool exits) and is the ONE
55
+ * tool that does NOT suppress viewer interaction (Select IS that mode).
56
+ */
57
+ private readonly selectTool: ManagedTool = {
58
+ id: ToolModeManager.SELECT_ID,
59
+ label: "Select",
60
+ icon: "mdi:cursor-default",
61
+ onDeactivate: () => {},
62
+ };
63
+
64
+ constructor(private readonly components: OBC.Components) {
65
+ this._active = this.selectTool;
66
+ }
67
+
68
+ /**
69
+ * Id of the implicit default mode: idle / plain object-selection. Active on load
70
+ * and whenever no modal tool is engaged. The Select toolbar button maps to it,
71
+ * and the HUD reflects it as the resting state.
72
+ */
73
+ static readonly SELECT_ID = "select";
74
+
75
+ get active(): ManagedTool {
76
+ return this._active;
77
+ }
78
+
79
+ /** The active tool's id ({@link ToolModeManager.SELECT_ID} in the default mode). */
80
+ getActiveId(): string {
81
+ return this._active.id;
82
+ }
83
+
84
+ /** True in the default object-selection mode (no modal tool active). */
85
+ get isSelectMode(): boolean {
86
+ return this._active === this.selectTool;
87
+ }
88
+
89
+ /**
90
+ * Return to SELECT (the default): exit whatever modal tool is active so the
91
+ * viewer is back to idle hover + click-select. No-op if already in select. The
92
+ * Select toolbar button calls this; individual tools still exit via clearActive.
93
+ */
94
+ selectMode() {
95
+ this.setActive(this.selectTool);
96
+ }
97
+
98
+ /** Make `tool` the single active tool, deactivating the previous one. */
99
+ setActive(tool: ManagedTool) {
100
+ if (this._active === tool) return;
101
+ this._active.onDeactivate(); // tear down the previous tool (selectTool: no-op)
102
+ this._active = tool;
103
+ // Suppress passive hover/select for MODAL tools only — selectTool IS that mode.
104
+ this._applySuppression(tool !== this.selectTool);
105
+ this.onActiveChanged.trigger(this._active);
106
+ }
107
+
108
+ /** Exit `tool` back to the default SELECT tool (no-op unless it's the active one). */
109
+ clearActive(tool: ManagedTool) {
110
+ if (this._active !== tool) return;
111
+ this.setActive(this.selectTool);
112
+ }
113
+
114
+ /**
115
+ * Re-emit the current active tool without changing it — for a tool whose
116
+ * dynamic {@link ManagedTool.label} changed while it stayed active (e.g. the
117
+ * measurement tool switching length → area), so the HUD re-reads the label.
118
+ */
119
+ refresh() {
120
+ this.onActiveChanged.trigger(this._active);
121
+ }
122
+
123
+ private _applySuppression(suppress: boolean) {
124
+ if (suppress === this._suppressed) return; // only act on a real transition
125
+ const hoverer = this.components.get(OBF.Hoverer);
126
+ const highlighter = this.components.get(OBF.Highlighter);
127
+ if (suppress) {
128
+ this._prevHovererEnabled = hoverer.enabled;
129
+ this._prevHighlighterEnabled = highlighter.enabled;
130
+ hoverer.enabled = false;
131
+ highlighter.enabled = false;
132
+ } else {
133
+ hoverer.enabled = this._prevHovererEnabled;
134
+ highlighter.enabled = this._prevHighlighterEnabled;
135
+ }
136
+ this._suppressed = suppress;
137
+ }
138
+ }
139
+
140
+ const instances = new WeakMap<OBC.Components, ToolModeManager>();
141
+
142
+ /** Per-Components singleton accessor. */
143
+ export const toolModeManager = (components: OBC.Components): ToolModeManager => {
144
+ let manager = instances.get(components);
145
+ if (!manager) {
146
+ manager = new ToolModeManager(components);
147
+ instances.set(components, manager);
148
+ }
149
+ return manager;
150
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Controller returned by each toolbar mode-tool setup (`clipper`,
3
+ * `lengthMeasurement`, `areaMeasurement`, …). The toolbar — owned by Worker 2 —
4
+ * calls {@link activate} to enter the tool's mode and {@link deactivate} to exit
5
+ * it, and guarantees only one mode is active at a time (mutual exclusion).
6
+ *
7
+ * Contract:
8
+ * - `activate()` — enable the tool's pointer/keyboard listeners + cursor.
9
+ * - `deactivate()` — remove those listeners and drop any transient in-progress
10
+ * state. It must NOT discard finished results (e.g. existing
11
+ * section planes / measurements persist across mode switches).
12
+ * Both are idempotent.
13
+ */
14
+ export interface ModeTool {
15
+ /** Human label, used for the toolbar button tooltip. */
16
+ label: string;
17
+ /** Iconify icon name, e.g. "mdi:scissors-cutting". */
18
+ icon: string;
19
+ /** Enter the tool's mode. */
20
+ activate(): void;
21
+ /** Exit the tool's mode and clean up transient state. */
22
+ deactivate(): void;
23
+ }