@thatopen/services 0.3.1 → 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/builtin/paths.json +74 -74
  6. package/docs/cli/create.md +4 -3
  7. package/package.json +1 -1
  8. package/src/built-in/index.ts +22 -2
  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,340 +1,223 @@
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);
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
+
12
+ // ─── A2 migration — PHASES 1+2: boot on UIManager + re-dock panels ───────────
13
+ // Juan consolidated the old AppManager (layout) + ViewportsManager (viewport)
14
+ // built-ins into the single UIManager built-in, which ships `top-app` (shell +
15
+ // layout) and `top-viewer` (deferred-PEN viewport). This boots the bim-viewer on
16
+ // that model.
17
+ // Phase 1 — top-app shell hosting one top-viewer + auto-load. [done]
18
+ // Phase 2 — re-dock the side panels (tree/properties/files/data/objects/
19
+ // settings) into top-app's layouts + sidebar. [this file]
20
+ // Phase 3 — the viewer-overlay tools (fps/HUD/gizmo/bottom toolbar/measure +
21
+ // clip handles) that mount over the canvas. [next]
22
+ // The pre-A2 rich main is preserved at `main.rich.ts.bak`.
23
+ async function main() {
24
+ const client = PlatformClient.fromPlatformContext();
25
+
26
+ // Brand accent (purple) — drives layout-selector active state, dividers, etc.
27
+ document.documentElement.style.setProperty("--bim-ui_accent-base", "#6528d7");
28
+
29
+ // The dev `thatopen serve` wrapper HTML doesn't zero the UA body margin (8px),
30
+ // which insets the whole app inside the platform iframe. Kill it here so it's
31
+ // fixed in both dev and production regardless of the host page.
32
+ document.body.style.margin = "0";
33
+
34
+ // UIManager must be in the setup call: it registers the platform web
35
+ // components (top-app, top-viewer, top-viewer-tools, …) before the DOM renders.
36
+ const { components } = await client.setup<OBC.Components>(
37
+ { OBC, OBF, BUI, THREE, FRAGS, MARKERJS },
38
+ { uuid: UIManager.uuid },
39
+ );
40
+ components.get(UIManager).init();
41
+
42
+ // One STABLE top-viewer node, returned by reference so re-rendering top-app
43
+ // (when we add the panels below) reuses it instead of disposing/recreating
44
+ // its world. No <top-viewer-tools>: the bim-viewer mounts its own tabbed
45
+ // visibility/inspect toolbar (see below), so the platform default would just
46
+ // duplicate it.
47
+ const viewerEl = document.createElement("top-viewer");
48
+
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ const app = document.createElement("top-app") as any;
51
+
52
+ app.setup = (waitUntil: (p: Promise<void>, label?: string) => void) => {
53
+ waitUntil(
54
+ (async () => {
55
+ const fragments = components.get(OBC.FragmentsManager);
56
+ const workerUrl = await FRAGS.FragmentsModels.getWorker();
57
+ fragments.init(await FRAGS.toClassicWorker(workerUrl), {
58
+ classicWorker: true,
59
+ });
60
+ })(),
61
+ "Fragments Core",
62
+ );
63
+ return { components, client };
64
+ };
65
+
66
+ // Mount minimally first (viewer only) so top-viewer creates the world; the
67
+ // panels + their tools are built once that world exists (below).
68
+ app.elements = { viewer: () => BUI.html`${viewerEl}` };
69
+ app.layouts = {
70
+ Main: {
71
+ label: "Main",
72
+ icon: "solar:3d-square-bold",
73
+ template: `"viewer" 1fr / 1fr`,
74
+ },
75
+ };
76
+ app.layout = "Main";
77
+
78
+ const container = document.getElementById("that-open-app") ?? document.body;
79
+ container.appendChild(app);
80
+
81
+ // Wait for top-viewer's world to exist before building world-dependent panels.
82
+ // The viewer tool suite + dynamic-anchor pivot dot are now baked into
83
+ // <top-viewer> (setupViewerTools), so the app no longer wires them.
84
+ await firstWorld(components.get(OBC.Worlds));
85
+
86
+ // Platform client + project data for the AppManager-shim consumers
87
+ // (CloudRunner, data-table-panel, app-info-section).
88
+ const projectId: string | undefined = client?.context?.projectId;
89
+ let projectData;
90
+ try {
91
+ if (projectId) projectData = await client.getProjectData(projectId);
92
+ } catch {
93
+ /* dev/no-project → consumers degrade gracefully */
94
+ }
95
+ setAppContext(client, projectData);
96
+
97
+ // Pluggable loaders for <top-models-list>. The built-in ships the lightweight
98
+ // defaults (.frag load, IFC→fragments convert); heavy/app-specific loaders are
99
+ // registered here so they stay OUT of the built-in's bundle. The reality-capture
100
+ // .3tz viewer pulls Spark + 3d-tiles-renderer, so it's lazy-imported app-side
101
+ // and plugged in via the loader registry. Alignment persists through the panel's
102
+ // app-data via the loader context (getAlignment/setAlignment).
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ let rcViewer: any = null;
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ const modelLoaders: Record<string, any> = {
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ "3tz": async (fileId: string, ctx: any) => {
109
+ if (!rcViewer) {
110
+ const { realityCaptureViewer } = await import(
111
+ "./setups/reality-capture-viewer"
112
+ );
113
+ rcViewer = realityCaptureViewer(ctx.components, client);
114
+ }
115
+ const saved = ctx.getAlignment(fileId) as number[] | undefined;
116
+ await rcViewer.loadIntoWorld(fileId, {
117
+ keepPostproduction: true,
118
+ transform: saved ? new THREE.Matrix4().fromArray(saved) : undefined,
119
+ onTransformChange: (m: THREE.Matrix4) =>
120
+ ctx.setAlignment(fileId, m.toArray()),
121
+ });
122
+ },
123
+ };
124
+
125
+ // Re-dock: the stable viewer + the panels, under the bim-viewer's named layouts
126
+ // with the activity-bar sidebar (Explorer · Assets · Data · Settings). All panels
127
+ // are now built-ins that self-wire from top-app's componentsContext/clientContext
128
+ // — no `components` plumbing in the app.
129
+ app.elements = {
130
+ viewer: () => BUI.html`${viewerEl}`,
131
+ tree: () => BUI.html`<top-model-tree></top-model-tree>`,
132
+ properties: () => BUI.html`<top-properties-panel></top-properties-panel>`,
133
+ files: () =>
134
+ BUI.html`<top-models-list .loaders=${modelLoaders}></top-models-list>`,
135
+ dataTable: () => BUI.html`<top-data-table-panel></top-data-table-panel>`,
136
+ objects: () => BUI.html`<top-objects-panel></top-objects-panel>`,
137
+ settings: () => BUI.html`<top-settings-panel></top-settings-panel>`,
138
+ };
139
+ // No `label` → the sidebar renders icon-only activity-bar buttons (matching
140
+ // the pre-A2 look), background only on the active one.
141
+ app.layouts = {
142
+ Explorer: {
143
+ icon: "mdi:file-tree",
144
+ template: `"tree viewer" 1fr "properties viewer" 1fr / 22rem 1fr`,
145
+ },
146
+ Assets: {
147
+ icon: "mdi:folder-multiple-outline",
148
+ template: `"files viewer" 1fr "objects viewer" 1fr / 22rem 1fr`,
149
+ },
150
+ Data: {
151
+ icon: "mdi:table",
152
+ template: `"dataTable viewer" 1fr / 22rem 1fr`,
153
+ },
154
+ Settings: {
155
+ icon: "mdi:cog",
156
+ template: `"settings viewer" 1fr / 22rem 1fr`,
157
+ },
158
+ };
159
+ app.layout = "Explorer";
160
+ app.sidebar = true;
161
+
162
+ // ── Viewer toolbar — now a built-in slotted INTO <top-viewer> (it consumes the
163
+ // world + components contexts top-viewer provides). The rich bottom toolbar +
164
+ // active-tool HUD + clip/measure/walkthrough tools all live in
165
+ // <top-viewer-toolbar>; the side panels share the SAME tool singletons via
166
+ // components.get (OBC.Component-wrapped).
167
+ viewerEl.appendChild(document.createElement("top-viewer-toolbar"));
168
+
169
+ // Navigation gizmo is now baked into <top-viewer> (setupViewerTools), so the
170
+ // app no longer mounts it. (Cascade: the rest of the overlay tools follow.)
171
+
172
+ // Auto-load one model (top-viewer's world wires fragments→scene itself).
173
+ void autoLoadFirstModel(components, client);
174
+ }
175
+
176
+ /** Resolves with the first world once it exists (top-viewer creates it async). */
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ function firstWorld(worlds: any): Promise<any> {
179
+ const existing = [...worlds.list.values()][0];
180
+ if (existing) return Promise.resolve(existing);
181
+ return new Promise((resolve) => {
182
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
+ const handler = ({ value }: any) => {
184
+ worlds.list.onItemSet.remove(handler);
185
+ resolve(value);
186
+ };
187
+ worlds.list.onItemSet.add(handler);
188
+ });
189
+ }
190
+
191
+
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ async function autoLoadFirstModel(components: OBC.Components, client: any) {
194
+ const fragments = components.get(OBC.FragmentsManager);
195
+ const projectId: string | undefined = client?.context?.projectId;
196
+ if (!projectId) {
197
+ console.warn("[a2] no projectId — skipping auto-load");
198
+ return;
199
+ }
200
+ try {
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ const items = (await client.listFiles({ projectId })) as any[];
203
+ const frags = items.filter((it) =>
204
+ (it.name ?? "").toLowerCase().endsWith(".frag"),
205
+ );
206
+ const frag =
207
+ frags.find((it) => (it.name ?? "").toLowerCase().includes("bloxhub")) ??
208
+ frags[0];
209
+ if (!frag) {
210
+ console.warn("[a2] no .frag in project to auto-load");
211
+ return;
212
+ }
213
+ const resp = await client.downloadFile(String(frag._id));
214
+ const buffer = await resp.arrayBuffer();
215
+ await fragments.core.load(buffer, { modelId: String(frag._id) });
216
+ await fragments.core.update(true);
217
+ console.log("[a2] auto-loaded model:", frag.name);
218
+ } catch (error) {
219
+ console.warn("[a2] auto-load failed", error);
220
+ }
221
+ }
222
+
223
+ main().catch(console.error);