@thatopen/services 0.1.3 → 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 (120) hide show
  1. package/README.md +59 -46
  2. package/dist/built-in/index.d.ts +486 -563
  3. package/dist/cli/lib/npmrc.d.ts +28 -0
  4. package/dist/cli/lib/npmrc.test.d.ts +1 -0
  5. package/dist/cli.js +91 -7
  6. package/dist/core/client.d.ts +10 -0
  7. package/dist/index.cjs.js +2 -2
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.es.js +42 -32
  10. package/dist/types/npm.d.ts +14 -0
  11. package/dist/vite-env.d.ts +1 -1
  12. package/docs/builtin/paths.json +74 -70
  13. package/docs/cli/create.md +28 -28
  14. package/docs/cli/local-server.md +38 -38
  15. package/docs/cli/login.md +32 -32
  16. package/docs/cli/publish.md +31 -31
  17. package/docs/cli/run.md +26 -26
  18. package/docs/cli/serve.md +43 -43
  19. package/docs/cli/swap.md +30 -30
  20. package/package.json +100 -100
  21. package/resources/AGENTS.md +40 -40
  22. package/src/built-in/CDEManager/example.ts +214 -214
  23. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  24. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  25. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  26. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  27. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  28. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  29. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  30. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  31. package/src/built-in/UIManager/src/app/example.ts +99 -99
  32. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
  33. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
  34. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
  35. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
  36. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
  37. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
  38. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
  39. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
  40. package/src/built-in/index.ts +649 -755
  41. package/src/cli/templates/app/README.md +241 -241
  42. package/src/cli/templates/app/index.html +35 -27
  43. package/src/cli/templates/app/package.json +31 -27
  44. package/src/cli/templates/app/src/app.ts +37 -0
  45. package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
  46. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
  47. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  48. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  49. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  50. package/src/cli/templates/app/src/globals.ts +1 -0
  51. package/src/cli/templates/app/src/main.ts +340 -56
  52. package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
  53. package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
  54. package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
  55. package/src/cli/templates/app/src/setups/card-header.ts +35 -0
  56. package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
  57. package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
  58. package/src/cli/templates/app/src/setups/clipper.ts +75 -0
  59. package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
  60. package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
  61. package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
  62. package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
  63. package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
  64. package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
  65. package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
  66. package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
  67. package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
  68. package/src/cli/templates/app/src/setups/hider.ts +98 -0
  69. package/src/cli/templates/app/src/setups/index.ts +33 -3
  70. package/src/cli/templates/app/src/setups/inspection.ts +136 -0
  71. package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
  72. package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
  73. package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
  74. package/src/cli/templates/app/src/setups/measurements.ts +133 -0
  75. package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
  76. package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
  77. package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
  78. package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
  79. package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
  80. package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
  81. package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
  82. package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
  83. package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
  84. package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
  85. package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
  86. package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
  87. package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
  88. package/src/cli/templates/app/src/setups/styles.ts +375 -0
  89. package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
  90. package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
  91. package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
  92. package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
  93. package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
  94. package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
  95. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
  96. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
  97. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
  98. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
  99. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
  100. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
  101. package/src/cli/templates/app/src/ui-components/index.ts +2 -1
  102. package/src/cli/templates/app/tsconfig.json +16 -17
  103. package/src/cli/templates/app/vite.config.js +45 -42
  104. package/src/cli/templates/cloud-component/README.md +205 -205
  105. package/src/cli/templates/cloud-component/_thatopen +5 -5
  106. package/src/cli/templates/cloud-component/declarations.json +4 -4
  107. package/src/cli/templates/cloud-component/package.json +21 -21
  108. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  109. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  110. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  111. package/src/cli/templates/shared/AGENTS.md +8 -8
  112. package/src/cli/templates/shared/CLAUDE.md +1 -1
  113. package/src/cli/templates/shared/_gitignore +5 -4
  114. package/src/built-in/AppManager/example.ts +0 -86
  115. package/src/built-in/FileList/example.ts +0 -107
  116. package/src/built-in/TabbedNavigation/example.ts +0 -115
  117. package/src/built-in/ViewportsManager/example.ts +0 -106
  118. package/src/cli/templates/app/src/setups/fragments.ts +0 -18
  119. package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
  120. package/src/cli/templates/app/src/ui-components/AppPanel/index.ts +0 -160
@@ -1,56 +1,340 @@
1
- import { html } from "lit";
2
- import * as THREE from "three";
3
- import * as OBC from "@thatopen/components";
4
- import * as OBF from "@thatopen/components-front";
5
- import * as FRAGS from "@thatopen/fragments";
6
- import * as BUI from "@thatopen/ui";
7
- import { PlatformClient, UIManager } from "@thatopen/services";
8
- import { initFragments } from "./setups/fragments";
9
- import { initIfcLoader } from "./setups/ifc-loader";
10
- import "./ui-components";
11
-
12
- async function main() {
13
- const client = PlatformClient.fromPlatformContext();
14
-
15
- const { components } = await client.setup(
16
- { OBC, OBF, BUI, THREE, FRAGS },
17
- { uuid: UIManager.uuid },
18
- ) as { components: OBC.Components };
19
-
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- (components.get(UIManager as any) as any).init();
22
-
23
- const app = document.createElement("top-app") as any;
24
-
25
- app.setup = (waitUntil: (promise: Promise<unknown>, label: string) => void) => {
26
- waitUntil(initFragments(components), "Fragments Core");
27
- waitUntil(initIfcLoader(components), "IFC Loader");
28
- return { components, client };
29
- };
30
-
31
- app.elements = {
32
- viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
33
- panel: () => html`<app-panel></app-panel>`,
34
- };
35
-
36
- app.layouts = {
37
- main: {
38
- label: "Main",
39
- icon: "solar:home-bold",
40
- template: `"panel" 1fr / 22rem`,
41
- },
42
- };
43
-
44
- app.base = "viewer";
45
- app.layout = "main";
46
-
47
- app.addEventListener("thatopen:app-ready", () => {
48
- app.showToast("App ready", "success");
49
- });
50
-
51
- const container = document.getElementById("that-open-app") ?? document.body;
52
- container.appendChild(app);
53
- document.body.style.margin = "0";
54
- }
55
-
56
- main().catch(console.error);
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
+ // Inlines the fragments worker so it runs inside the platform's sandboxed iframe.
6
+ import "@thatopen/fragments/inline";
7
+ import * as BUI from "@thatopen/ui";
8
+ import * as MARKERJS from "@markerjs/markerjs3";
9
+ import { PlatformClient, UIManager } from "@thatopen/services";
10
+ import { setAppContext } from "./app";
11
+ import {
12
+ propertiesPanel,
13
+ modelTree,
14
+ filesPanel,
15
+ graphicsPanel,
16
+ clipperTool,
17
+ clipperPanel,
18
+ commandsPanel,
19
+ measurementTool,
20
+ dataTablePanel,
21
+ fpsIndicator,
22
+ activeToolHud,
23
+ navigationGizmo,
24
+ visibilityToolbar,
25
+ walkthrough,
26
+ } from "./setups";
27
+ import { objectsPanel } from "./setups/objects-panel";
28
+ import { inspectionInstances, inspectionActions } from "./setups/inspection";
29
+ import { measurementSettingsPanel } from "./setups/measurement-settings-panel";
30
+ import { settingsPanel } from "./setups/settings-panel";
31
+
32
+ // ─── A2 migration — PHASES 1+2: boot on UIManager + re-dock panels ───────────
33
+ // Juan consolidated the old AppManager (layout) + ViewportsManager (viewport)
34
+ // built-ins into the single UIManager built-in, which ships `top-app` (shell +
35
+ // layout) and `top-viewer` (deferred-PEN viewport). This boots the bim-viewer on
36
+ // that model.
37
+ // Phase 1 — top-app shell hosting one top-viewer + auto-load. [done]
38
+ // Phase 2 — re-dock the side panels (tree/properties/files/data/objects/
39
+ // settings) into top-app's layouts + sidebar. [this file]
40
+ // Phase 3 — the viewer-overlay tools (fps/HUD/gizmo/bottom toolbar/measure +
41
+ // clip handles) that mount over the canvas. [next]
42
+ // The pre-A2 rich main is preserved at `main.rich.ts.bak`.
43
+ async function main() {
44
+ const client = PlatformClient.fromPlatformContext();
45
+
46
+ // Brand accent (purple) — drives layout-selector active state, dividers, etc.
47
+ document.documentElement.style.setProperty("--bim-ui_accent-base", "#6528d7");
48
+
49
+ // The dev `thatopen serve` wrapper HTML doesn't zero the UA body margin (8px),
50
+ // which insets the whole app inside the platform iframe. Kill it here so it's
51
+ // fixed in both dev and production regardless of the host page.
52
+ document.body.style.margin = "0";
53
+
54
+ // UIManager must be in the setup call: it registers the platform web
55
+ // components (top-app, top-viewer, top-viewer-tools, …) before the DOM renders.
56
+ const { components } = await client.setup<OBC.Components>(
57
+ { OBC, OBF, BUI, THREE, FRAGS, MARKERJS },
58
+ { uuid: UIManager.uuid },
59
+ );
60
+ components.get(UIManager).init();
61
+
62
+ // One STABLE top-viewer node, returned by reference so re-rendering top-app
63
+ // (when we add the panels below) reuses it instead of disposing/recreating
64
+ // its world. No <top-viewer-tools>: the bim-viewer mounts its own tabbed
65
+ // visibility/inspect toolbar (see below), so the platform default would just
66
+ // duplicate it.
67
+ const viewerEl = document.createElement("top-viewer");
68
+
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ const app = document.createElement("top-app") as any;
71
+
72
+ app.setup = (waitUntil: (p: Promise<void>, label?: string) => void) => {
73
+ waitUntil(
74
+ (async () => {
75
+ const fragments = components.get(OBC.FragmentsManager);
76
+ const workerUrl = await FRAGS.FragmentsModels.getWorker();
77
+ fragments.init(await FRAGS.toClassicWorker(workerUrl), {
78
+ classicWorker: true,
79
+ });
80
+ })(),
81
+ "Fragments Core",
82
+ );
83
+ return { components, client };
84
+ };
85
+
86
+ // Mount minimally first (viewer only) so top-viewer creates the world; the
87
+ // panels + their tools are built once that world exists (below).
88
+ app.elements = { viewer: () => BUI.html`${viewerEl}` };
89
+ app.layouts = {
90
+ Main: {
91
+ label: "Main",
92
+ icon: "solar:3d-square-bold",
93
+ template: `"viewer" 1fr / 1fr`,
94
+ },
95
+ };
96
+ app.layout = "Main";
97
+
98
+ const container = document.getElementById("that-open-app") ?? document.body;
99
+ container.appendChild(app);
100
+
101
+ // Wait for top-viewer's world before building world-dependent tools/panels.
102
+ const world = await firstWorld(components.get(OBC.Worlds));
103
+ // Auto-anchor: the orbit pivot follows the surface picked on left-press
104
+ // (library dynamicAnchor). top-viewer doesn't enable it, so set it here.
105
+ world.dynamicAnchor = true;
106
+ // …and show that pivot as an on-screen dot (projected from 3D each frame).
107
+ setupAnchorDot(world, viewerEl);
108
+
109
+ // Platform client + project data for the AppManager-shim consumers
110
+ // (CloudRunner, data-table-panel, app-info-section).
111
+ const projectId: string | undefined = client?.context?.projectId;
112
+ let projectData;
113
+ try {
114
+ if (projectId) projectData = await client.getProjectData(projectId);
115
+ } catch {
116
+ /* dev/no-project → consumers degrade gracefully */
117
+ }
118
+ setAppContext(client, projectData);
119
+
120
+ // Headless tools — drive the panels; they resolve the world via `components`.
121
+ const clipTool = clipperTool(components);
122
+ const measureTool = measurementTool(components);
123
+
124
+ // Panels.
125
+ const treeEl = modelTree(components);
126
+ const propsEl = propertiesPanel(components);
127
+ const filesEl = filesPanel(components, client) as unknown as HTMLElement;
128
+ const dataTableEl = dataTablePanel(components) as unknown as HTMLElement;
129
+ // FPS counter — mounted into the viewer overlay; the Graphics panel "Show FPS"
130
+ // switch drives it (pass the controller in).
131
+ const fps = fpsIndicator(viewerEl);
132
+ const graphicsEl = graphicsPanel(components, fps) as unknown as HTMLElement;
133
+ const clippingEl = clipperPanel(components, clipTool) as unknown as HTMLElement;
134
+ const commandsEl = commandsPanel(components) as unknown as HTMLElement;
135
+ const measureSettingsEl = measurementSettingsPanel(
136
+ measureTool,
137
+ ) as unknown as HTMLElement;
138
+ const inspection = inspectionInstances(clipTool, measureTool);
139
+ const objectsEl = objectsPanel(inspection) as unknown as HTMLElement;
140
+ const settingsEl = settingsPanel([
141
+ { label: "Graphics", icon: "mdi:tune", el: graphicsEl },
142
+ { label: "Clip styling", icon: "mdi:scissors-cutting", el: clippingEl },
143
+ { label: "Measurement", icon: "mdi:ruler", el: measureSettingsEl },
144
+ { label: "Commands", icon: "mdi:keyboard", el: commandsEl },
145
+ ]) as unknown as HTMLElement;
146
+
147
+ for (const el of [
148
+ treeEl,
149
+ propsEl,
150
+ filesEl,
151
+ dataTableEl,
152
+ objectsEl,
153
+ settingsEl,
154
+ ] as HTMLElement[]) {
155
+ el.style.width = "100%";
156
+ el.style.height = "100%";
157
+ // top-app's grid areas are overflow:hidden; border-box keeps the panel's own
158
+ // border inside the area so it isn't clipped on the bottom/right edges.
159
+ el.style.boxSizing = "border-box";
160
+ }
161
+
162
+ // Re-dock: same stable viewer + the panels, under the bim-viewer's named
163
+ // layouts with the activity-bar sidebar (Explorer · Assets · Data · Settings).
164
+ const wrap = (el: HTMLElement) => () => BUI.html`${el}`;
165
+ app.elements = {
166
+ viewer: () => BUI.html`${viewerEl}`,
167
+ tree: wrap(treeEl),
168
+ properties: wrap(propsEl),
169
+ files: wrap(filesEl),
170
+ dataTable: wrap(dataTableEl),
171
+ objects: wrap(objectsEl),
172
+ settings: wrap(settingsEl),
173
+ };
174
+ // No `label` → the sidebar renders icon-only activity-bar buttons (matching
175
+ // the pre-A2 look), background only on the active one.
176
+ app.layouts = {
177
+ Explorer: {
178
+ icon: "mdi:file-tree",
179
+ template: `"tree viewer" 1fr "properties viewer" 1fr / 22rem 1fr`,
180
+ },
181
+ Assets: {
182
+ icon: "mdi:folder-multiple-outline",
183
+ template: `"files viewer" 1fr "objects viewer" 1fr / 22rem 1fr`,
184
+ },
185
+ Data: {
186
+ icon: "mdi:table",
187
+ template: `"dataTable viewer" 1fr / 22rem 1fr`,
188
+ },
189
+ Settings: {
190
+ icon: "mdi:cog",
191
+ template: `"settings viewer" 1fr / 22rem 1fr`,
192
+ },
193
+ };
194
+ app.layout = "Explorer";
195
+ app.sidebar = true;
196
+
197
+ // ── Viewer-overlay tools — appended to the top-viewer host, so they slot into
198
+ // top-viewer's pointer-events:none overlay over the canvas (interactive parts
199
+ // opt back into pointer events themselves). ────────────────────────────────
200
+ activeToolHud(viewerEl, components);
201
+
202
+ let walk;
203
+ try {
204
+ walk = walkthrough(components);
205
+ } catch (e) {
206
+ console.warn("[a2] walkthrough controller failed to init", e);
207
+ }
208
+
209
+ // Floating bottom toolbar (tabbed): "View" = visibility (hide/show/ghost/
210
+ // isolate + projection + walkthrough); "Inspect" = Select / Clip / Measure,
211
+ // routed through the tool-mode manager.
212
+ visibilityToolbar(
213
+ components,
214
+ viewerEl,
215
+ walk,
216
+ undefined,
217
+ inspectionActions(clipTool, measureTool),
218
+ );
219
+
220
+ // Navigation gizmo / view-cube (top-right): live orientation + click-to-orient.
221
+ try {
222
+ navigationGizmo(components, viewerEl);
223
+ } catch (e) {
224
+ console.warn("[a2] navigationGizmo failed to mount", e);
225
+ }
226
+
227
+ // Auto-load one model (top-viewer's world wires fragments→scene itself).
228
+ void autoLoadFirstModel(components, client);
229
+ }
230
+
231
+ /** Resolves with the first world once it exists (top-viewer creates it async). */
232
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
+ function firstWorld(worlds: any): Promise<any> {
234
+ const existing = [...worlds.list.values()][0];
235
+ if (existing) return Promise.resolve(existing);
236
+ return new Promise((resolve) => {
237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
+ const handler = ({ value }: any) => {
239
+ worlds.list.onItemSet.remove(handler);
240
+ resolve(value);
241
+ };
242
+ worlds.list.onItemSet.add(handler);
243
+ });
244
+ }
245
+
246
+ /**
247
+ * On-screen pivot dot for the dynamic anchor: an HTML element projected from the
248
+ * 3D anchor point each frame. Appended to the top-viewer host so it slots into
249
+ * the viewer overlay over the canvas. Revealed once a drag starts (so a click-to-
250
+ * select doesn't flash it) and kept glued to the pivot as the camera orbits.
251
+ */
252
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
+ function setupAnchorDot(world: any, element: HTMLElement) {
254
+ const dot = document.createElement("div");
255
+ dot.style.cssText =
256
+ "position:absolute; width:13px; height:13px; border-radius:50%;" +
257
+ "background:#6528d7; transform:translate(-50%,-50%);" +
258
+ "pointer-events:none; display:none; z-index:5;" +
259
+ "box-shadow:0 0 0 2px rgba(255,255,255,0.35);";
260
+ element.appendChild(dot);
261
+
262
+ let anchor: THREE.Vector3 | null = null;
263
+ const place = () => {
264
+ if (!anchor) return;
265
+ const ndc = anchor.clone().project(world.camera.three);
266
+ dot.style.left = `${(ndc.x * 0.5 + 0.5) * element.clientWidth}px`;
267
+ dot.style.top = `${(-ndc.y * 0.5 + 0.5) * element.clientHeight}px`;
268
+ };
269
+
270
+ let shown = false;
271
+ world.onDynamicAnchorSet.add((p: THREE.Vector3) => {
272
+ anchor = p.clone();
273
+ shown = false;
274
+ place();
275
+ });
276
+ world.onDynamicAnchorClear.add(() => {
277
+ anchor = null;
278
+ shown = false;
279
+ dot.style.display = "none";
280
+ });
281
+
282
+ // Reveal only once a real drag starts (DRAG px), so a click-to-select doesn't
283
+ // flash the dot.
284
+ const DRAG = 6;
285
+ let press: { x: number; y: number } | null = null;
286
+ element.addEventListener("pointerdown", (e) => {
287
+ if (e.button === 0) press = { x: e.clientX, y: e.clientY };
288
+ });
289
+ element.addEventListener("pointermove", (e) => {
290
+ if (!press || !anchor || shown) return;
291
+ const dx = e.clientX - press.x;
292
+ const dy = e.clientY - press.y;
293
+ if (dx * dx + dy * dy >= DRAG * DRAG) {
294
+ shown = true;
295
+ place();
296
+ dot.style.display = "block";
297
+ }
298
+ });
299
+ const clearPress = () => {
300
+ press = null;
301
+ };
302
+ element.addEventListener("pointerup", clearPress);
303
+ element.addEventListener("pointercancel", clearPress);
304
+
305
+ // Keep the dot glued to the 3D pivot as the camera orbits around it.
306
+ world.renderer?.onBeforeUpdate.add(place);
307
+ }
308
+
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310
+ async function autoLoadFirstModel(components: OBC.Components, client: any) {
311
+ const fragments = components.get(OBC.FragmentsManager);
312
+ const projectId: string | undefined = client?.context?.projectId;
313
+ if (!projectId) {
314
+ console.warn("[a2] no projectId — skipping auto-load");
315
+ return;
316
+ }
317
+ try {
318
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
+ const items = (await client.listFiles({ projectId })) as any[];
320
+ const frags = items.filter((it) =>
321
+ (it.name ?? "").toLowerCase().endsWith(".frag"),
322
+ );
323
+ const frag =
324
+ frags.find((it) => (it.name ?? "").toLowerCase().includes("bloxhub")) ??
325
+ frags[0];
326
+ if (!frag) {
327
+ console.warn("[a2] no .frag in project to auto-load");
328
+ return;
329
+ }
330
+ const resp = await client.downloadFile(String(frag._id));
331
+ const buffer = await resp.arrayBuffer();
332
+ await fragments.core.load(buffer, { modelId: String(frag._id) });
333
+ await fragments.core.update(true);
334
+ console.log("[a2] auto-loaded model:", frag.name);
335
+ } catch (error) {
336
+ console.warn("[a2] auto-load failed", error);
337
+ }
338
+ }
339
+
340
+ main().catch(console.error);
@@ -0,0 +1,69 @@
1
+ import * as OBC from "@thatopen/components";
2
+ import { toolModeManager, type ManagedTool } from "./tool-mode-manager";
3
+
4
+ /**
5
+ * Active-tool HUD — a floating text overlay (top-left of the viewport) showing
6
+ * what the user is currently doing, e.g. "Drawing clipping plane" /
7
+ * "Measuring length".
8
+ *
9
+ * Fully DECOUPLED from any specific tool: it only subscribes to the global
10
+ * toolModeManager's `onActiveChanged` and renders the active tool's registered
11
+ * {@link ManagedTool.label} (+ optional icon), hidden when no tool is active. Any
12
+ * future tool that registers a label with the manager shows up here with zero
13
+ * HUD changes.
14
+ *
15
+ * Styled like the other in-viewer overlays (fps counter): panel surface + 1px
16
+ * contrast-20 border + theme text, pointer-events off.
17
+ */
18
+ export interface ActiveToolHud {
19
+ element: HTMLElement;
20
+ }
21
+
22
+ export const activeToolHud = (
23
+ parent: HTMLElement,
24
+ components: OBC.Components,
25
+ ): ActiveToolHud => {
26
+ const manager = toolModeManager(components);
27
+
28
+ const el = document.createElement("div");
29
+ el.style.cssText = `
30
+ position: absolute; top: 0.6rem; left: 0.6rem; z-index: 10;
31
+ display: none; align-items: center; gap: 0.4rem;
32
+ padding: 0.25rem 0.6rem; border-radius: 0.5rem;
33
+ background: var(--bim-ui_bg-contrast-10, #262629);
34
+ border: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
35
+ color: var(--bim-ui_bg-contrast-100, #e3e3e3);
36
+ font: 600 0.74rem/1.2 "Plus Jakarta Sans", sans-serif;
37
+ pointer-events: none; user-select: none;
38
+ `;
39
+ const icon = document.createElement("bim-icon");
40
+ icon.style.fontSize = "0.95rem";
41
+ const text = document.createElement("span");
42
+ el.append(icon, text);
43
+
44
+ // The viewer is position:relative, so an absolutely-positioned child overlays
45
+ // the canvas correctly.
46
+ if (!parent.style.position) parent.style.position = "relative";
47
+ parent.append(el);
48
+
49
+ const render = (tool: ManagedTool | null) => {
50
+ if (!tool) {
51
+ el.style.display = "none";
52
+ return;
53
+ }
54
+ text.textContent =
55
+ typeof tool.label === "function" ? tool.label() : tool.label;
56
+ if (tool.icon) {
57
+ icon.setAttribute("icon", tool.icon);
58
+ icon.style.display = "";
59
+ } else {
60
+ icon.style.display = "none";
61
+ }
62
+ el.style.display = "flex";
63
+ };
64
+
65
+ manager.onActiveChanged.add(render);
66
+ render(manager.active);
67
+
68
+ return { element: el };
69
+ };
@@ -0,0 +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
+ };