@thatopen/services 0.3.6 → 0.3.8

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.
@@ -0,0 +1,285 @@
1
+ /* MD
2
+ ## ClashesManager
3
+ ---
4
+ Structural engineers and BIM coordinators routinely detect geometric clashes
5
+ between disciplines — structural steel penetrating ductwork, pipes clashing
6
+ with slabs — and need to track the status of each issue through resolution
7
+ without leaving their BIM environment.
8
+
9
+ ClashesManager is the platform-side component that owns clash data: it loads and
10
+ persists jobs, runs, and clashes; launches cloud-based clash detection in a Web
11
+ Worker; and exposes the events and methods needed to build review workflows on
12
+ top of it.
13
+
14
+ This tutorial covers initializing the manager from the platform client; the
15
+ recommended `clashes-panel` UI that works out of the box once `init` has been
16
+ called; defining queries and detection matrices for fine-grained control;
17
+ launching a detection run and tracking progress; updating clash status
18
+ programmatically; and rendering sphere markers and element highlights without
19
+ the built-in panel.
20
+
21
+ By the end, you'll understand every public surface of ClashesManager and know
22
+ which parts are handled automatically by `clashes-panel` so you can decide how
23
+ much of the stack you need for your use case.
24
+ */
25
+
26
+ import * as THREE from "three";
27
+ import * as OBC from "@thatopen/components";
28
+ import * as OBF from "@thatopen/components-front";
29
+ import * as FRAGS from "@thatopen/fragments";
30
+ import * as BUI from "@thatopen/ui";
31
+ import { PlatformClient } from "thatopen-services";
32
+ import clashesManagerDef from "./index";
33
+
34
+ const ClashesManager = clashesManagerDef.componentDefinition;
35
+
36
+ const client = PlatformClient.fromPlatformContext();
37
+
38
+ const { components } = (await client.setup(
39
+ { OBC, OBF, BUI, THREE, FRAGS },
40
+ { uuid: ClashesManager.uuid },
41
+ )) as { components: OBC.Components };
42
+
43
+ /* MD
44
+ ### 🚀 Initializing the manager
45
+
46
+ `init(client)` is the only call required before using the manager. It:
47
+
48
+ - Loads all persisted clash data (jobs, runs, clashes, queries, matrices) from
49
+ the project's cloud storage into memory.
50
+ - Registers `save` and `reload` callbacks so writes are debounced to storage
51
+ automatically and the manager can refresh its data after a run completes.
52
+ - Reads any in-flight cloud executions so that a page reload mid-run doesn't
53
+ lose track of the job.
54
+ - Resolves the engine's active `World` in the background — if no world exists
55
+ yet, it waits until one is created, so the order of initialization does not
56
+ matter.
57
+
58
+ After `init` resolves, `manager.ready` is `true` and all data properties
59
+ (`jobs`, `runs`, `clashes`, `queries`, `matrices`) are populated.
60
+ */
61
+
62
+ const manager = components.get(ClashesManager);
63
+ await manager.init(client);
64
+
65
+ console.log(
66
+ `Loaded ${manager.jobs.length} jobs, ` +
67
+ `${manager.runs.length} runs, ` +
68
+ `${manager.clashes.length} clashes.`,
69
+ );
70
+
71
+ /* MD
72
+ ### 🖥️ Adding the built-in panel (recommended)
73
+
74
+ Once `init` has been called, the `<clashes-panel>` web component is the
75
+ recommended way to interact with `ClashesManager`. It covers the full
76
+ review workflow — detection matrix, clash table, sphere markers, status
77
+ filters, run progress — with zero additional wiring.
78
+
79
+ `clashes-panel` is registered when `UIManager` is included in the platform
80
+ setup call. See the **clashes-panel** tutorial for the complete setup and a
81
+ detailed breakdown of everything the panel manages automatically.
82
+
83
+ The sections below are for cases where you need direct control: building a
84
+ custom UI, integrating clash data into your own panels, or automating status
85
+ updates from outside the panel.
86
+ */
87
+
88
+ /* MD
89
+ ### 📐 Queries and matrices
90
+
91
+ A `ClashQuery` defines a named set of model elements: which files to scan and,
92
+ optionally, which IFC categories or attribute conditions to match. Queries have
93
+ no detection logic of their own — they are inputs to a matrix.
94
+
95
+ A `ClashMatrix` pairs queries in an upper-triangle grid. Each active cell
96
+ corresponds to one `ClashJob` that the cloud worker will execute. The matrix
97
+ also carries the clash type (`hard` or `clearance`) and, for clearance checks,
98
+ the tolerance in metres.
99
+
100
+ Assigning to `manager.queries` or `manager.matrices` triggers a debounced
101
+ auto-save (2 s). Call `await manager.flush()` to persist immediately before
102
+ navigating away.
103
+ */
104
+
105
+ const structuralFileId = "file-id-of-structural-model";
106
+ const mepFileId = "file-id-of-mep-model";
107
+
108
+ manager.queries = [
109
+ {
110
+ id: "q-structural",
111
+ name: "Structural",
112
+ modelFileIds: [structuralFileId],
113
+ query: { categories: ["IFCBEAM", "IFCCOLUMN", "IFCSLAB"] },
114
+ },
115
+ {
116
+ id: "q-mep",
117
+ name: "MEP",
118
+ modelFileIds: [mepFileId],
119
+ // Omitting `query` matches all elements with geometry in the listed files.
120
+ },
121
+ ];
122
+
123
+ // The matrix below defines a single hard-clash job between Structural and MEP.
124
+ // `activePairs` lists the upper-triangle cell keys (queryAId:queryBId).
125
+ // `jobIds` is managed by the platform when the matrix is saved; leave it empty
126
+ // on first creation and the backend fills it after the first save.
127
+ manager.matrices = [
128
+ {
129
+ id: "matrix-main",
130
+ name: "Structural vs MEP",
131
+ queryIds: ["q-structural", "q-mep"],
132
+ activePairs: ["q-structural:q-mep"],
133
+ jobIds: [],
134
+ jobType: "hard",
135
+ },
136
+ ];
137
+
138
+ await manager.flush();
139
+
140
+ /* MD
141
+ ### ▶️ Running clash detection
142
+
143
+ `run(jobId)` launches a Web Worker that calls the cloud clash-detection
144
+ endpoint. It returns immediately — all feedback arrives through events.
145
+
146
+ | Event | Payload | When |
147
+ |-------|---------|------|
148
+ | `onRunProgress` | `{ message, progress }` | Periodically while the worker runs |
149
+ | `onRunComplete` | `{ runId, clashCount }` | Worker finished successfully |
150
+ | `onRunError` | `{ code?, message }` | Worker or cloud error |
151
+ | `onRunningJobsChanged` | `Set<string>` | A job starts or finishes |
152
+ | `onReloadingChanged` | `boolean` | Data reload after a successful run |
153
+
154
+ If the page is reloaded while a run is in progress, `init` re-reads the
155
+ in-flight execution from storage and populates `runningJobIds`. Call
156
+ `reconnectIfRunning(jobId)` to resume receiving progress events for that job.
157
+ */
158
+
159
+ manager.onRunProgress.add(({ message, progress }) => {
160
+ console.log(`[${progress}%] ${message}`);
161
+ });
162
+
163
+ manager.onRunComplete.add(({ runId, clashCount }) => {
164
+ console.log(`Run ${runId} complete — ${clashCount} clash(es) found.`);
165
+ });
166
+
167
+ manager.onRunError.add(({ code, message }) => {
168
+ console.error(`Run failed (${code ?? "unknown"}): ${message}`);
169
+ });
170
+
171
+ manager.onReloadingChanged.add((reloading) => {
172
+ // Use this flag to show a loading state in a custom UI while fresh data
173
+ // is fetched from storage after the run. clashes-panel handles this automatically.
174
+ console.log(reloading ? "Reloading clash data…" : "Clash data ready.");
175
+ });
176
+
177
+ // Guard: only run if the manager is ready and the job is not already running.
178
+ const jobToRun = manager.jobs[0];
179
+ if (jobToRun && manager.ready && !manager.runningJobIds.has(jobToRun.id)) {
180
+ manager.run(jobToRun.id);
181
+ }
182
+
183
+ // If a run was already in progress before this page loaded, reconnect to it.
184
+ for (const jobId of manager.runningJobIds) {
185
+ manager.reconnectIfRunning(jobId);
186
+ }
187
+
188
+ /* MD
189
+ ### ✅ Updating clash status
190
+
191
+ Each clash starts with status `"new"`. Reviewers move clashes through a
192
+ workflow by calling `updateClash`. Valid statuses are:
193
+
194
+ - `"new"` — detected, not yet reviewed
195
+ - `"active"` — under investigation
196
+ - `"acknowledged"` — known issue, not blocking
197
+ - `"resolved"` — fixed and verified
198
+
199
+ Status changes and comments are persisted automatically via the same
200
+ auto-save mechanism as queries and matrices.
201
+
202
+ `runsForJob(jobId)` and `clashesForRun(runId)` let you navigate the history:
203
+ a clash's `firstSeenRunId` / `lastSeenRunId` span shows across which runs it
204
+ was present, so you can determine whether a resolution actually closed it.
205
+ */
206
+
207
+ manager.onClashStatusChanged.add((allClashes) => {
208
+ const open = allClashes.filter(c => c.status === "new" || c.status === "active");
209
+ console.log(`Open clashes: ${open.length}`);
210
+ });
211
+
212
+ const clashToAcknowledge = manager.clashes[0];
213
+ if (clashToAcknowledge) {
214
+ manager.updateClash(clashToAcknowledge.id, {
215
+ status: "acknowledged",
216
+ comments: "Coordination with MEP team scheduled for next sprint.",
217
+ });
218
+ }
219
+
220
+ // Navigate run history for the first job.
221
+ const firstJob = manager.jobs[0];
222
+ if (firstJob) {
223
+ const runs = manager.runsForJob(firstJob.id); // newest first
224
+ const latestRun = runs[0];
225
+ if (latestRun) {
226
+ const clashesInRun = manager.clashesForRun(latestRun.id);
227
+ console.log(`Latest run has ${clashesInRun.length} active clash(es).`);
228
+ }
229
+ }
230
+
231
+ /* MD
232
+ ### 📍 Visualization without the built-in panel
233
+
234
+ If you are building a custom UI and not using `clashes-panel`, you can drive
235
+ the visualization layer directly.
236
+
237
+ **Sphere markers** — `showMarkers` places colored 3D spheres at clash
238
+ locations in the viewer. Call `hideMarkers` to remove them and re-enable
239
+ hover highlighting. Points are automatically transformed from IFC world space
240
+ to Three.js viewer space using `FragmentsManager.baseCoordinationMatrix`.
241
+
242
+ **Element highlights** — `highlightAllClashes` colors element A (green) and
243
+ element B (red) for every new/active clash in the provided list.
244
+ `clearClashHighlight` removes both styles. For single-clash focus, use
245
+ `highlightClash` and pair it with `ghostClashes` to dim everything else.
246
+
247
+ **Clustering** — `cluster(clashes, threshold)` groups spatially close clashes
248
+ into a single marker. Pass `threshold = 0` to show one sphere per clash.
249
+ `toViewerPoint` converts a raw IFC point to viewer space if you need to
250
+ position custom objects.
251
+ */
252
+
253
+ const jobClashes = manager.clashes.filter(c => c.jobId === firstJob?.id);
254
+
255
+ // Show one marker per clash, colored by status.
256
+ manager.showMarkers(
257
+ jobClashes.map(c => ({ id: c.id, point: c.point, count: 1, status: c.status })),
258
+ );
259
+
260
+ // Highlight all open clashes across both element sets.
261
+ manager.highlightAllClashes(jobClashes);
262
+
263
+ // --- Clustered view ---
264
+ // Group clashes within 2 m of each other into a single sphere.
265
+ const clusters = manager.cluster(jobClashes, 2);
266
+ manager.showMarkers(
267
+ clusters.map(group => ({
268
+ point: group[0].point,
269
+ count: group.length,
270
+ status: group[0].status,
271
+ })),
272
+ );
273
+
274
+ // Focus on a single clash: ghost everything else and zoom in.
275
+ const focusClash = jobClashes[0];
276
+ if (focusClash) {
277
+ manager.clearClashHighlight();
278
+ manager.highlightClash(focusClash);
279
+ await manager.ghostClash(focusClash);
280
+ }
281
+
282
+ // Restore full scene when done.
283
+ manager.hideMarkers();
284
+ manager.clearClashHighlight();
285
+ await manager.clearGhostClash();
@@ -0,0 +1,164 @@
1
+ /* MD
2
+ ## clashes-panel
3
+ ---
4
+ BIM coordinators need a dedicated review environment where they can see all
5
+ detected clashes, filter by status, navigate to each issue in the 3D viewer,
6
+ and mark items as resolved — without writing any UI code themselves.
7
+
8
+ `clashes-panel` is a ready-made web component that delivers exactly that. It
9
+ connects to `ClashesManager` automatically and manages the entire review
10
+ workflow: the detection matrix, the clash table with status filters, sphere
11
+ markers and element highlights in the viewer, run controls with live progress,
12
+ and a loading skeleton while fresh data reloads after a run completes.
13
+
14
+ This tutorial covers the prerequisites before mounting the panel; dropping it
15
+ into a `top-app` layout alongside a 3D viewer; and a detailed breakdown of
16
+ everything the panel handles automatically so you know what you do not need to
17
+ implement yourself.
18
+
19
+ By the end, you'll have a fully working clash-review environment running in
20
+ your application with a single line of markup.
21
+ */
22
+
23
+ import { html } from "lit";
24
+ import * as THREE from "three";
25
+ import * as OBC from "@thatopen/components";
26
+ import * as OBF from "@thatopen/components-front";
27
+ import * as FRAGS from "@thatopen/fragments";
28
+ import * as BUI from "@thatopen/ui";
29
+ import { PlatformClient } from "thatopen-services";
30
+ import uiManagerDef from "../../../index";
31
+ import clashesManagerDef from "../../../ClashesManager/index";
32
+ import type { App } from "../app/index";
33
+
34
+ const UIManager = uiManagerDef.componentDefinition;
35
+ const ClashesManager = clashesManagerDef.componentDefinition;
36
+
37
+ /* MD
38
+ ### ✅ Prerequisites
39
+
40
+ Two conditions must be met before the panel mounts:
41
+
42
+ 1. **`UIManager` must be in the setup call.** It registers all platform web
43
+ components, including `<clashes-panel>` itself. Without it the element is
44
+ unknown to the browser and renders as an empty box.
45
+
46
+ 2. **`ClashesManager.init(client)` must have resolved.** The panel reads all
47
+ its initial data from the manager in `connectedCallback`. If `init` has not
48
+ finished, the panel mounts into an empty state and never catches up because
49
+ it only subscribes to change events — it does not poll.
50
+
51
+ Both conditions are naturally satisfied when you call `init` before appending
52
+ `top-app` to the document, which is the pattern shown below.
53
+ */
54
+
55
+ const client = PlatformClient.fromPlatformContext();
56
+
57
+ const { components } = (await client.setup(
58
+ { OBC, OBF, BUI, THREE, FRAGS },
59
+ { uuid: UIManager.uuid },
60
+ { uuid: ClashesManager.uuid },
61
+ )) as { components: OBC.Components };
62
+
63
+ components.get(UIManager).init();
64
+
65
+ // init must resolve before top-app (and therefore clashes-panel) mounts.
66
+ await components.get(ClashesManager).init(client);
67
+
68
+ /* MD
69
+ ### 🖥️ Wiring the panel
70
+
71
+ Add `clashes-panel` as a named area in `app.elements` and reference it in your
72
+ layout template. A side-by-side layout with the 3D viewer is the most natural
73
+ arrangement — the panel controls markers and highlights directly in the scene.
74
+ */
75
+
76
+ const app = document.createElement("top-app") as unknown as App;
77
+
78
+ app.setup = (waitUntil) => {
79
+ waitUntil(
80
+ (async () => {
81
+ const fragments = components.get(OBC.FragmentsManager);
82
+ const workerUrl = await FRAGS.FragmentsModels.getWorker();
83
+ fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
84
+ })(),
85
+ "Fragments Core",
86
+ );
87
+
88
+ waitUntil(
89
+ (async () => {
90
+ const loader = components.get(OBC.IfcLoader);
91
+ await loader.setup();
92
+ })(),
93
+ "IFC Loader",
94
+ );
95
+
96
+ return { components, client };
97
+ };
98
+
99
+ app.elements = {
100
+ viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
101
+ clashes: () => html`<clashes-panel></clashes-panel>`,
102
+ };
103
+
104
+ app.layouts = {
105
+ main: {
106
+ label: "Main",
107
+ icon: "solar:3d-square-bold",
108
+ template: `"viewer clashes" 1fr / 1fr 22rem`,
109
+ },
110
+ };
111
+
112
+ app.layout = "main";
113
+
114
+ const container = document.getElementById("that-open-app") ?? document.body;
115
+ container.appendChild(app);
116
+ document.body.style.margin = "0";
117
+
118
+ /* MD
119
+ ### 🤖 What the panel manages automatically
120
+
121
+ Once mounted, `clashes-panel` takes ownership of the following without any
122
+ further code from you:
123
+
124
+ **Data synchronization** — the panel subscribes to every `ClashesManager`
125
+ event (`onJobsChanged`, `onRunsChanged`, `onClashesChanged`,
126
+ `onClashStatusChanged`, `onQueriesChanged`, `onRunningJobsChanged`) and keeps
127
+ its tables in sync. Changes made programmatically through the manager API are
128
+ reflected in the panel immediately.
129
+
130
+ **Run lifecycle** — a progress callout appears as soon as a run starts
131
+ (`onRunProgress`), updates its percentage and message in real time, and
132
+ disappears when the run completes or errors. If a run was already in progress
133
+ when the page loaded, the panel calls `reconnectIfRunning` automatically when
134
+ the user opens that job.
135
+
136
+ **Post-run reload skeleton** — after a successful run the manager reloads clash
137
+ data from storage. During that window (`onReloadingChanged = true`) the panel
138
+ replaces its entire content with an animated skeleton so the user is never left
139
+ looking at stale results that appear final.
140
+
141
+ **Sphere markers** — entering a job view places colored spheres in the 3D scene
142
+ at each clash location, transformed from IFC world space to viewer space.
143
+ Selecting a clash in the table zooms the camera, ghosts unrelated elements, and
144
+ re-focuses the markers on the selection. Leaving the job view removes all
145
+ markers and restores the full scene.
146
+
147
+ **Element highlights** — element A is highlighted green and element B red for
148
+ every new or active clash in the current job. Selecting a single clash refines
149
+ the highlight to that pair only. Clearing the selection or changing status
150
+ restores the full job highlight.
151
+
152
+ **Model awareness** — the panel tracks which fragment models are currently
153
+ loaded in the scene. Markers and highlights are activated only when the relevant
154
+ models are present, and are cleaned up automatically if a model is disposed
155
+ while the panel is open.
156
+
157
+ **Save indicator** — a subtle indicator appears while auto-save is in progress
158
+ (`onSaveStart`) and disappears once the write confirms (`onSaveComplete`), so
159
+ users know their status changes and query edits are persisted.
160
+
161
+ **Disconnection cleanup** — when the panel is removed from the DOM it calls
162
+ `hideMarkers` and `clearClashHighlight` on the manager, leaving the 3D scene
163
+ clean regardless of what was selected at the time.
164
+ */
@@ -0,0 +1,120 @@
1
+ /* MD
2
+ ## top-models-list
3
+ ---
4
+ top-models-list is the project files panel: it lists the project's files, uploads new ones, converts IFC to fragments, and loads `.frag` models into the 3D scene. Its loaders are PLUGGABLE — the panel owns the UI and the orchestration (progress, refresh, association), but the HOST decides how each file format is loaded, so you can teach it new formats without forking the built-in.
5
+
6
+ This tutorial covers the two extension points. First, registering a custom loader for a file extension — either imperatively with `registerLoader(ext, fn)` or up front via the `loaders` property; a loader receives `(fileId, ctx)` where `ctx` carries the engine (`components`), the platform `client`, and per-file alignment persistence. A format with no registered loader simply hides its load action. Second, overriding the IFC to fragments `converter` — omit it to use the built-in default (which drives the project's IfcFragmenter cloud component), or provide one to run conversion elsewhere.
7
+
8
+ By the end you'll have a files panel that knows how to load a custom format and how to run a custom IFC conversion, with the panel's UI untouched.
9
+ */
10
+
11
+ import { html } from "lit";
12
+ import * as THREE from "three";
13
+ import * as OBC from "@thatopen/components";
14
+ import * as OBF from "@thatopen/components-front";
15
+ import * as FRAGS from "@thatopen/fragments";
16
+ import * as BUI from "@thatopen/ui";
17
+ import { PlatformClient } from "thatopen-services";
18
+ import uiManagerDef from "../../index";
19
+ import type { App } from "../app/index";
20
+ import type { ModelsList } from "./index";
21
+
22
+ const UIManager = uiManagerDef.componentDefinition;
23
+
24
+ const client = PlatformClient.fromPlatformContext();
25
+
26
+ // UIManager registers every platform web component (top-app, top-viewer,
27
+ // top-models-list, …) before the DOM renders.
28
+ const { components } = (await client.setup(
29
+ { OBC, OBF, BUI, THREE, FRAGS },
30
+ { uuid: UIManager.uuid },
31
+ )) as { components: OBC.Components };
32
+
33
+ components.get(UIManager).init();
34
+
35
+ const app = document.createElement("top-app") as unknown as App;
36
+
37
+ app.setup = (waitUntil) => {
38
+ waitUntil(
39
+ (async () => {
40
+ const fragments = components.get(OBC.FragmentsManager);
41
+ const workerUrl = await FRAGS.FragmentsModels.getWorker();
42
+ fragments.init(await FRAGS.toClassicWorker(workerUrl), {
43
+ classicWorker: true,
44
+ });
45
+ })(),
46
+ "Fragments Core",
47
+ );
48
+ return { components, client };
49
+ };
50
+
51
+ // One stable <top-models-list>, kept by reference so re-rendering top-app reuses
52
+ // it. The loaders are read LIVE, so registration can happen at any time.
53
+ const modelsList = document.createElement("top-models-list") as ModelsList;
54
+
55
+ // ── Extension point 1: a custom loader, keyed by file extension ──────────────
56
+ // A ModelLoader is `(fileId, ctx) => void | Promise<void>`. The panel calls it
57
+ // when the user loads a file whose extension matches. `ctx` gives you everything
58
+ // you need: { name, ext, components, client, getAlignment, setAlignment }. Do
59
+ // whatever it takes to put the file in the scene — fetch it via the client,
60
+ // parse it, and add geometry to a world from `components`.
61
+ modelsList.registerLoader("xyz", async (fileId, ctx) => {
62
+ // const buffer = await ctx.client
63
+ // .downloadFile(fileId)
64
+ // .then((r: Response) => r.arrayBuffer());
65
+ // ...parse `buffer` and add it to the world here...
66
+ // Persist/restore a placement matrix across reloads via the panel's app-data:
67
+ // const saved = ctx.getAlignment(fileId);
68
+ // ctx.setAlignment(fileId, matrix.toArray());
69
+ console.log(`[xyz loader] would load "${ctx.name}" (${fileId})`, ctx.components);
70
+ });
71
+
72
+ // Equivalent declarative form — hand the whole registry over at once:
73
+ // modelsList.loaders = { xyz: async (fileId, ctx) => { ... } };
74
+ //
75
+ // Registering a loader for an extension is also what makes that format's files
76
+ // APPEAR in the list (recognition follows the loaders registry). For a
77
+ // CONVERT-style action — e.g. a PLY that runs a PLY→3D-tiles cloud component
78
+ // then views the result — register a RICH entry so the button reads correctly:
79
+ // modelsList.registerLoader("ply", {
80
+ // label: "Generate 3D tiles",
81
+ // icon: "mdi:cube-scan",
82
+ // fn: async (fileId, ctx) => {
83
+ // // const { executionId } = await ctx.client.executeComponent(...);
84
+ // // ...poll until done, then open the produced tileset...
85
+ // },
86
+ // });
87
+
88
+ // ── Extension point 2: override the IFC → fragments converter ────────────────
89
+ // OMIT this to keep the built-in default (drives the project's IfcFragmenter
90
+ // cloud component via the platform client). Provide it to run conversion
91
+ // elsewhere / differently — the panel keeps the progress bar, reload-survival,
92
+ // association and refresh; your converter just `start`s and is `poll`ed.
93
+ modelsList.converter = {
94
+ async start(fileId) {
95
+ // kick off your conversion; return an execution id to poll
96
+ return `exec-${fileId}`;
97
+ },
98
+ async poll(executionId) {
99
+ // report progress; when done, hand back the produced .frag id
100
+ return { done: true, ok: true, fragId: "demo-frag", message: "Converted" };
101
+ },
102
+ };
103
+
104
+ // Mount the panel into a one-area layout. (In a real app it sits beside a
105
+ // top-viewer so loaded models have a world to render into.)
106
+ app.elements = {
107
+ files: () => html`${modelsList}`,
108
+ };
109
+ app.layouts = {
110
+ main: {
111
+ label: "Files",
112
+ icon: "mdi:folder-multiple-outline",
113
+ template: `"files" 1fr / 1fr`,
114
+ },
115
+ };
116
+ app.layout = "main";
117
+
118
+ const container = document.getElementById("that-open-app") ?? document.body;
119
+ container.appendChild(app);
120
+ document.body.style.margin = "0";