@thatopen/services 0.10.0 → 0.12.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.
@@ -283,4 +283,9 @@ component, once added to a project, is then triggered by an app or an automation
283
283
  four-point plan and waiting for approval to do what was just requested costs the user a whole turn
284
284
  and buys nothing. Stop and ask only when you are about to change files you did not create, when
285
285
  the request can be read two ways that mean different work, or when the next step is destructive.
286
+ - **Never save to the platform on every change.** Drafts belong in `localStorage` /
287
+ IndexedDB; the platform gets an explicit save. Writes are capped at 30 per minute and a
288
+ `429` loses the write. See
289
+ `node_modules/@thatopen/services/docs/rate-limits.md` before you write any save, sync or
290
+ polling code.
286
291
  - The scaffold already works — **extend it, don't replace it.**
@@ -39,6 +39,10 @@
39
39
  "path": "src/built-in/ClashesManager/example.ts",
40
40
  "description": "ClashesManager — Structural engineers and BIM coordinators routinely detect geometric clashes between disciplines — structural steel penetrating ductwork, pipes clashing with slabs — and need to track the status of each issue through resolution without leaving their BIM environment."
41
41
  },
42
+ {
43
+ "path": "src/built-in/ProjectManager/example.ts",
44
+ "description": "ProjectManager — Project-level configuration and data — GIS origin, georeferencing, app settings, and other project-scoped concerns — need a single canonical place to live. Without it, every builtin that needs to persist something at the project level (not per-file, not per-user) ends up inventing its own file, its own save/load pattern, and its own place to put it."
45
+ },
42
46
  {
43
47
  "path": "src/built-in/UIManager/src/app/example.ts",
44
48
  "description": "top-app — Front-end developers building BIM web apps need to coordinate several async initialization steps before the UI can be shown — loading the engine, preparing fragment workers, registering loaders — and users need visible feedback on what is happening during that wait, rather than a blank screen."
@@ -94,5 +98,13 @@
94
98
  {
95
99
  "path": "src/built-in/UIManager/src/topics-panel/example.ts",
96
100
  "description": "topics-panel — Every coordination workflow needs a place to track issues, clashes, and action items against a 3D model — who's responsible, what still needs review, and where in the model it actually is. Building that from scratch means a BCF-compatible data model, a form, comment threads, viewpoint capture/navigation, document links, and a persistence layer — all before a single issue gets tracked."
101
+ },
102
+ {
103
+ "path": "src/built-in/UIManager/src/viewer/example.ts",
104
+ "description": "top-viewer — Every BIM app needs a 3D viewport: a scene, a camera, a grid, element highlighting, and the plumbing to keep loaded models framed and rendering efficiently as they stream in. Building that from scratch is a project on its own, before a single button or panel gets wired to it."
105
+ },
106
+ {
107
+ "path": "src/built-in/UIManager/src/viewer-toolbar/example.ts",
108
+ "description": "top-viewer-toolbar — A 3D viewport is only as useful as the controls around it: selecting a target set, hiding/isolating/ghosting elements, clipping, measuring, walking through the model. Building that toolbar — and keeping every button in sync with tool state and with each other — is real work, before you've customized anything."
97
109
  }
98
110
  ]
@@ -0,0 +1,123 @@
1
+ # Rate limits, and how to save data without hitting them
2
+
3
+ The platform API is rate limited. Every app and cloud component shares the same
4
+ budget, so **how often you write decides whether your app works**. This page has
5
+ the numbers, the failure mode, and the pattern to use instead.
6
+
7
+ > **The one rule:** never send a request on every change the user makes. No
8
+ > autosave-per-keystroke, no write inside a render loop, no polling loop that
9
+ > hammers a list endpoint. Keep work-in-progress in the browser and talk to the
10
+ > server on an explicit save.
11
+
12
+ ---
13
+
14
+ ## The numbers
15
+
16
+ Limits are per **rolling 60-second window**, counted per user (JWT), or per API
17
+ token owner, or per IP — whichever identifies the caller.
18
+
19
+ | Endpoint | Client method | Limit |
20
+ |---|---|---|
21
+ | `POST /api/item` | `createFile`, `createComponent`, `createApp` | **30 / min** |
22
+ | `POST /api/item/:id/version` | `updateFile` / `updateComponent` **with a `file`** | **30 / min** |
23
+ | `PUT /api/item/:id/version/:tag/metadata` | `updateFileVersionMetadata` | 30 / min |
24
+ | `DELETE /api/item/:id/version/:tag/metadata` | `deleteFileVersionMetadata` | 30 / min |
25
+ | `PUT /api/item/:id/version/:tag/archive` \| `/recover` | `archiveVersion`, `recoverVersion` | 30 / min |
26
+ | `POST /api/item/hidden` \| `/hidden/batch` | `createHiddenFile`, `createHiddenFilesBatch` | 30 / min |
27
+ | `POST /api/processor/:id/execute` | `executeComponent` | 20 / min |
28
+ | `GET /api/item/folder/:id/download`, `POST /api/item/batch/download` | `downloadFolder` | 10 / min |
29
+ | `POST /api/item/batch/versions` \| `/batch/version-metadata` \| `/batch/folders` | `listVersionsBatch`, `getFileVersionMetadataBatch`, `getFoldersBatch` | 60 / min |
30
+ | `POST /api/item/hidden/signed-url/batch` | `getHiddenFileSignedUrlsBatch` | 100 / min |
31
+ | `GET /api/item/hidden/:id/download` | `downloadHiddenFile` | 3000 / min |
32
+ | Everything else | — | 100 / min |
33
+
34
+ Two things follow from the table:
35
+
36
+ - **30 writes per minute is one write every two seconds.** An autosave tied to
37
+ user input passes that in a few seconds of typing or dragging.
38
+ - **Reads are cheap, but not free.** A viewer that mints one signed URL per tile
39
+ will exhaust 100/min quickly — use `getHiddenFileSignedUrlsBatch`, which signs
40
+ up to `STORAGE_BATCH_MAX` files per request.
41
+
42
+ ## What a rate-limited response looks like
43
+
44
+ Status `429`, with a `Retry-After` header (seconds) and this body:
45
+
46
+ ```json
47
+ {
48
+ "statusCode": 429,
49
+ "message": "Rate limit exceeded: max 30 requests per 60s for this endpoint. Retry after 12s.",
50
+ "code": "RATE_LIMITED",
51
+ "details": { "limit": 30, "windowSeconds": 60, "retryAfter": 12, "scope": "user" }
52
+ }
53
+ ```
54
+
55
+ The client surfaces it as a `RequestError`:
56
+
57
+ ```ts
58
+ import { RequestError } from '@thatopen/services';
59
+
60
+ try {
61
+ await client.updateFile(fileId, { file: blob, versionTag: tag });
62
+ } catch (err) {
63
+ if (err instanceof RequestError && err.status === 429) {
64
+ // err.code === 'RATE_LIMITED'
65
+ // err.retryAfter — seconds to wait, from Retry-After or details.retryAfter
66
+ showToast(`Saving is paused for ${err.retryAfter}s — your work is kept locally.`);
67
+ return;
68
+ }
69
+ throw err;
70
+ }
71
+ ```
72
+
73
+ **A 429 means the write did not happen.** Nothing was saved. If the user's only
74
+ copy of the change was in that request, it is gone — which is the real reason
75
+ the local-draft pattern below matters.
76
+
77
+ ## The pattern: local drafts, explicit saves
78
+
79
+ Keep every intermediate state in the browser. Write to the platform only when
80
+ the user asks for it.
81
+
82
+ ```ts
83
+ const draftKey = `draft:${fileId}`;
84
+
85
+ function onChange(state: unknown) {
86
+ localStorage.setItem(draftKey, JSON.stringify({ state, at: Date.now() }));
87
+ }
88
+
89
+ async function onSave(state: unknown) {
90
+ const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
91
+ await client.updateFile(fileId, { file: blob, versionTag: `v${Date.now()}` });
92
+ localStorage.removeItem(draftKey);
93
+ }
94
+ ```
95
+
96
+ On load, if a draft exists for the file, offer to restore it. That gives crash
97
+ recovery — the thing autosave was really for — at zero requests.
98
+
99
+ Rules of thumb:
100
+
101
+ - **Explicit save**, or a timer no faster than **once every 30 seconds**, and
102
+ only when something actually changed.
103
+ - **One request per save**, not one per changed object. Batch the whole document.
104
+ - **Never save while a save is in flight.** Keep a flag per file, and drop or
105
+ queue the second save. Overlapping writes also create versions that are hard
106
+ to reconcile.
107
+ - **Big or binary work-in-progress** belongs in IndexedDB, not `localStorage`
108
+ (about 5 MB per origin).
109
+ - There is **no draft-write method in the client on purpose.** Local storage is
110
+ the draft store; the platform stores versions the user chose to keep.
111
+
112
+ ## Retries
113
+
114
+ The client does not retry by default. When you turn retries on, it backs off
115
+ exponentially, adds jitter, and honours `Retry-After`:
116
+
117
+ ```ts
118
+ const client = new EngineServicesClient(token, apiUrl, { retries: 3 });
119
+ ```
120
+
121
+ Only network failures, `429` and `5xx` are retried. Other `4xx` fail straight
122
+ away, because repeating them cannot help. Never write your own immediate retry
123
+ loop around a 429 — that is what turns a throttled request into an outage.
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "access": "public"
16
16
  },
17
17
  "private": false,
18
- "version": "0.10.0",
18
+ "version": "0.12.0",
19
19
  "main": "dist/index.cjs.js",
20
20
  "module": "dist/index.es.js",
21
21
  "types": "dist/index.d.ts",
@@ -19,6 +19,7 @@ Do this before answering any question or writing any code. These are compact des
19
19
  | Platform built-ins | `node_modules/@thatopen/services/docs/builtin/paths.json` |
20
20
  | Platform client API | `node_modules/@thatopen/services/docs/client/paths.json` |
21
21
  | CLI commands | `node_modules/@thatopen/services/docs/cli/paths.json` |
22
+ | Rate limits + how to save data | `node_modules/@thatopen/services/docs/rate-limits.md` |
22
23
  | Engine components (`OBC`, `OBF`) | `https://raw.githubusercontent.com/ThatOpen/engine_components/refs/heads/main/examples/paths.json` |
23
24
  | Fragments (`FRAGS`) | `https://raw.githubusercontent.com/ThatOpen/engine_fragment/refs/heads/main/examples/paths.json` |
24
25
  | UI components (`BUI`) | `https://raw.githubusercontent.com/ThatOpen/engine_ui-components/refs/heads/main/examples/paths.json` — **skip** entries whose path contains `packages/obc` or `bim-grid` |
@@ -38,4 +39,13 @@ Once you have these, you know everything available on the platform. Only then fe
38
39
 
39
40
  ## Hard rules (always apply)
40
41
 
41
- 1. **All UI must be built with Lit**, using the web components from `@thatopen/ui` (`BUI`) — `bim-button`, `bim-panel`, `bim-panel-section`, `bim-toolbar`, `bim-dropdown`, `bim-input`, and the rest of `packages/core`. Always consult the design system before writing any UI: `https://raw.githubusercontent.com/ThatOpen/engine_ui-components/refs/heads/main/DESIGN.md`.
42
+ 1. **Never write to the platform on every change.** No autosave per keystroke,
43
+ per drag, or inside a render loop. Keep work in progress in `localStorage` /
44
+ IndexedDB and call the platform on an **explicit user save** (or a timer no
45
+ faster than once every 30 seconds). Writes are capped at **30 per minute**;
46
+ crossing that returns `429` and the write is **lost**, not queued.
47
+ If the user asks for autosave, build it against local storage and say so.
48
+ Read `node_modules/@thatopen/services/docs/rate-limits.md` before writing any
49
+ save, sync, or polling code — it has the per-endpoint limits and the pattern.
50
+
51
+ 2. **All UI must be built with Lit**, using the web components from `@thatopen/ui` (`BUI`) — `bim-button`, `bim-panel`, `bim-panel-section`, `bim-toolbar`, `bim-dropdown`, `bim-input`, and the rest of `packages/core`. Always consult the design system before writing any UI: `https://raw.githubusercontent.com/ThatOpen/engine_ui-components/refs/heads/main/DESIGN.md`.
@@ -0,0 +1,163 @@
1
+ /* MD
2
+ ## ProjectManager
3
+ ---
4
+ Project-level configuration and data — GIS origin, georeferencing, app
5
+ settings, and other project-scoped concerns — need a single canonical place
6
+ to live. Without it, every builtin that needs to persist something at the
7
+ project level (not per-file, not per-user) ends up inventing its own file,
8
+ its own save/load pattern, and its own place to put it.
9
+
10
+ ProjectManager is that canonical store. It persists one JSON file
11
+ (`${ProjectManager.uuid}.json`) loose directly inside `__project_data` — the
12
+ same folder convention `GISManager` already used — rather than in a
13
+ per-app subfolder like `collider`/`cde`, which are reserved for secondary,
14
+ per-builtin data.
15
+
16
+ This tutorial covers initializing the manager from the platform client; the
17
+ `origin` and `graphicsSettings` singletons; the `bimCoordinates` sites
18
+ collection used for BIM georeferencing; the `assetCoordinates` collection
19
+ used for splats and point clouds; auto-save and its events; and the
20
+ read-only namespace introspection API.
21
+
22
+ ProjectManager has no built-in UI panel — it's a pure data layer. Apps that
23
+ need georeferencing or project-settings UI (like Aqualia's viewer) build it
24
+ on top of this.
25
+
26
+ ### 🖖 Importing our libraries
27
+ */
28
+
29
+ import * as OBC from "@thatopen/components";
30
+ import { PlatformClient, ProjectManager } from "@thatopen/services";
31
+
32
+
33
+ /* MD
34
+ ### 🚀 Initializing ProjectManager
35
+ Like any other built-in, ProjectManager is set up through the platform
36
+ client. `client.setup(...)` resolves the shared `OBC.Components` instance
37
+ and gives us back everything we need.
38
+ */
39
+
40
+ const client = PlatformClient.fromPlatformContext();
41
+
42
+ const { components } = (await client.setup(
43
+ { OBC },
44
+ { uuid: ProjectManager.uuid },
45
+ )) as { components: OBC.Components };
46
+
47
+ const manager = components.get(ProjectManager);
48
+
49
+ /* MD
50
+ `init(client)` loads the persisted JSON for the current project — or
51
+ creates it with the default schema if it doesn't exist yet. `manager.ready`
52
+ flips to `true` once that's done, and the entity accessors below are only
53
+ safe to use after that.
54
+ */
55
+
56
+ await manager.init(client);
57
+ console.log("ready:", manager.ready);
58
+ console.log("namespaces:", manager.getNamespaces());
59
+
60
+ manager.onSaveStart.add(() => console.log("[event] onSaveStart"));
61
+ manager.onSaveComplete.add((success) => console.log("[event] onSaveComplete:", success));
62
+
63
+ /* MD
64
+ ### 📍 origin — a singleton entity
65
+ `origin` holds the project's GIS origin: latitude, longitude, height, and
66
+ rotation. Singletons expose `get`/`set`/`update`; `set` replaces the whole
67
+ value, `update` merges a partial one. Both schedule a debounced auto-save —
68
+ call `flush()` to force it immediately and await completion.
69
+ */
70
+
71
+ console.log("origin before:", manager.origin.get());
72
+
73
+ await manager.origin.set({ lat: 40.4164, lon: -3.7038, height: 700, rotation: 0 });
74
+ await manager.origin.update({ height: 710 });
75
+ await manager.flush();
76
+
77
+ console.log("origin after update+flush:", manager.origin.get());
78
+
79
+ /* MD
80
+ ### 🗺️ bimCoordinates — the sites collection
81
+ Sites are how a project keeps georeferencing for more than one BIM model,
82
+ each with its own origin and transform matrix. Because sites are created by
83
+ users at runtime, their ids are opaque and generated internally —
84
+ `createSite` mints the id and returns it, while `setSite` only updates a
85
+ site that already exists (it throws otherwise). This split keeps storage
86
+ keys decoupled from mutable data like a site's label.
87
+ */
88
+
89
+ const siteId = await manager.bimCoordinates.createSite({
90
+ label: "Site 1",
91
+ description: "Primary IFC export.",
92
+ lat: 40.41647848134075,
93
+ lon: -3.703451491164702,
94
+ height: 699.970526176243,
95
+ rotation: 0,
96
+ baseMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -498477.20216, -719.2375, 4389662.11289, 1],
97
+ });
98
+
99
+ await manager.bimCoordinates.setDefaultSite(siteId);
100
+ await manager.flush();
101
+
102
+ console.log("default site:", manager.bimCoordinates.getDefaultSite());
103
+ console.log("all sites:", manager.bimCoordinates.getSites());
104
+
105
+ /* MD
106
+ Updating an existing site goes through `setSite`, not `createSite`:
107
+ */
108
+
109
+ await manager.bimCoordinates.setSite(siteId, {
110
+ ...manager.bimCoordinates.getSite(siteId)!,
111
+ label: "Site 1 (revised)",
112
+ });
113
+ await manager.flush();
114
+
115
+ console.log("site after update:", manager.bimCoordinates.getSite(siteId));
116
+
117
+ /* MD
118
+ ### 🖼️ assetCoordinates — the files collection
119
+ `assetCoordinates` georeferences individual files — splats, point clouds —
120
+ that live outside the BIM model tree. Unlike sites, entries are keyed by
121
+ the caller-supplied `fileId`, since that id already comes from the platform
122
+ and is stable by construction.
123
+ */
124
+
125
+ await manager.assetCoordinates.set("splat_1", {
126
+ type: "splat",
127
+ lat: 40.416600807789145,
128
+ lon: -3.703739209785748,
129
+ height: 727.2700125737748,
130
+ rotation: 0,
131
+ });
132
+ await manager.flush();
133
+
134
+ console.log("splats:", manager.assetCoordinates.getByType("splat"));
135
+ console.log("splat_1:", manager.assetCoordinates.get("splat_1"));
136
+
137
+ /* MD
138
+ ### ⚙️ graphicsSettings — another singleton
139
+ Same shape as `origin` — `get`/`set`/`update`, debounced auto-save.
140
+ */
141
+
142
+ await manager.graphicsSettings.update({ /* project-wide graphics defaults */ } as any);
143
+ await manager.flush();
144
+
145
+ console.log("graphicsSettings:", manager.graphicsSettings.get());
146
+
147
+ /* MD
148
+ ### 🔍 Namespaces — read-only introspection
149
+ `getNamespaces()` lists the closed set of namespaces the schema defines;
150
+ `getNamespace(key)` returns every entity registered under one. Useful for
151
+ debugging or building a generic project-data inspector — not for reading
152
+ values you already know the shape of, which is what the entity accessors
153
+ above are for.
154
+ */
155
+
156
+ console.log("georeferencing namespace:", manager.getNamespace("georeferencing"));
157
+
158
+ /* MD
159
+ ### 🤖 What's not covered here
160
+ ProjectManager ships without a UI panel — everything above is the data
161
+ layer only. Building georeferencing or project-settings UI on top of
162
+ `origin`/`bimCoordinates`/`assetCoordinates` is up to the consuming app.
163
+ */
@@ -0,0 +1,182 @@
1
+ /* MD
2
+ ## top-viewer
3
+ ---
4
+ Every BIM app needs a 3D viewport: a scene, a camera, a grid, element
5
+ highlighting, and the plumbing to keep loaded models framed and rendering
6
+ efficiently as they stream in. Building that from scratch is a project on
7
+ its own, before a single button or panel gets wired to it.
8
+
9
+ `top-viewer` is a ready-made web component that delivers exactly that. Drop
10
+ it into a `top-app` layout and it creates the 3D world, wires camera
11
+ controls, sets up the highlighter, keeps every loaded fragment model framed
12
+ and culled correctly, and exposes four docking slots so toolbars and tools
13
+ can attach to it without any manual overlay/positioning code.
14
+
15
+ This tutorial covers the prerequisites; the minimal mount; what the
16
+ viewport does automatically; how docking works; and loading your first
17
+ model into it.
18
+
19
+ By the end, you'll have a fully working 3D viewport running in your
20
+ 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, UIManager } from "@thatopen/services";
30
+ import type { App } from "../app/index";
31
+
32
+ /* MD
33
+ ### ✅ Prerequisites
34
+
35
+ Two conditions must be met before `<top-viewer>` does anything useful:
36
+
37
+ 1. **`UIManager` must be in the setup call.** It registers all platform web
38
+ components, including `<top-viewer>` itself. Without it the element is
39
+ unknown to the browser and renders as an empty box.
40
+
41
+ 2. **`OBC.FragmentsManager` must be initialized** (`fragments.init(...)`)
42
+ before you try to load any model. `top-viewer` itself doesn't need this
43
+ to mount — its world, camera, and grid are ready the moment it connects
44
+ — but nothing will load until the fragments worker is up.
45
+
46
+ Both are naturally satisfied when you do the setup shown below before
47
+ appending `top-app` to the document.
48
+ */
49
+
50
+ const client = PlatformClient.fromPlatformContext();
51
+
52
+ const { components } = (await client.setup(
53
+ { OBC, OBF, BUI, THREE, FRAGS },
54
+ { uuid: UIManager.uuid },
55
+ )) as { components: OBC.Components };
56
+
57
+ components.get(UIManager).init();
58
+
59
+ /* MD
60
+ ### 🖥️ Minimal mount
61
+
62
+ `<top-viewer>` alone is a fully working, empty viewport — camera controls,
63
+ grid, and highlighter are already set up, no further code needed:
64
+
65
+ ```html
66
+ <top-viewer></top-viewer>
67
+ ```
68
+
69
+ It does **not** come with a toolbar. If you want the built-in one, add
70
+ `<top-viewer-toolbar>` as a child (see that component's own tutorial) — a
71
+ bare `<top-viewer>` is intentionally just the viewport.
72
+ */
73
+
74
+ const app = document.createElement("top-app") as unknown as App;
75
+
76
+ app.setup = (waitUntil) => {
77
+ waitUntil(
78
+ (async () => {
79
+ const fragments = components.get(OBC.FragmentsManager);
80
+ const workerUrl = await FRAGS.FragmentsModels.getWorker();
81
+ fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
82
+ })(),
83
+ "Fragments Core",
84
+ );
85
+ return { components, client };
86
+ };
87
+
88
+ app.elements = {
89
+ viewer: () => html`<top-viewer></top-viewer>`,
90
+ };
91
+
92
+ app.layouts = {
93
+ main: {
94
+ label: "Main",
95
+ icon: "solar:3d-square-bold",
96
+ template: `"viewer" 1fr / 1fr`,
97
+ },
98
+ };
99
+
100
+ app.layout = "main";
101
+
102
+ const container = document.getElementById("that-open-app") ?? document.body;
103
+ container.appendChild(app);
104
+ document.body.style.margin = "0";
105
+
106
+ /* MD
107
+ ### 🤖 What's handled automatically
108
+
109
+ Once mounted, `<top-viewer>` takes ownership of the following without any
110
+ further code from you:
111
+
112
+ **World + camera** — an `OBC.SimpleWorld` with a transparent-background
113
+ scene, an `OBC.OrthoPerspectiveCamera` (orbit controls, perspective by
114
+ default), and a reference grid.
115
+
116
+ **Highlighter** — `OBF.Highlighter` is set up for the world, ready for any
117
+ built-in or custom code to select/highlight elements.
118
+
119
+ **Model framing** — every model loaded through `OBC.FragmentsManager`
120
+ (anywhere in your app, not just inside this component) is automatically
121
+ added to the scene, told which camera to use for culling/LOD, and factored
122
+ into the camera's far-plane distance so large models never get clipped.
123
+ You never need to call `scene.add()` or `useCamera()` yourself.
124
+
125
+ **Navigation tools** — the navigation gizmo and active-tool HUD are baked
126
+ onto the viewport overlay the moment the world is ready.
127
+
128
+ **Context for descendants** — `<top-viewer>` provides `worldContext` (the
129
+ `OBC.World` instance) to any descendant, regardless of shadow-DOM
130
+ boundaries. `<top-viewer-toolbar>` and its standalone buttons consume it
131
+ this way — you never pass `world` around by hand.
132
+
133
+ **Cleanup** — disposes the world (and the highlighter) automatically when
134
+ the element is removed from the DOM.
135
+
136
+ ### 🧩 Docking slots
137
+
138
+ `<top-viewer>` exposes four named slots for toolbars/tools, arranged around
139
+ the viewport: `"bottom"`, `"top"`, `"left"`, `"right"`. Any element can use
140
+ them, not just `<top-viewer-toolbar>`:
141
+
142
+ ```html
143
+ <top-viewer>
144
+ <top-viewer-toolbar></top-viewer-toolbar> <!-- no slot → auto-docks "bottom" -->
145
+ <my-custom-panel slot="right"></my-custom-panel>
146
+ </top-viewer>
147
+ ```
148
+
149
+ A `<top-viewer-toolbar>` added **without** a `slot` attribute is
150
+ auto-docked into `"bottom"` for you — that's the only slot with special
151
+ handling; every other element (or every other slot) needs its `slot`
152
+ attribute set explicitly.
153
+
154
+ ### ⚠️ Gotchas
155
+
156
+ - **No toolbar is added for you.** A bare `<top-viewer>` has no bottom bar,
157
+ no buttons — just the viewport. If your app expects one, you must add
158
+ `<top-viewer-toolbar>` yourself.
159
+ - **Only `"bottom"`, `"top"`, `"left"`, `"right"` are recognized slot
160
+ names.** Anything else (a typo, or an app-specific name) is not placed
161
+ into any docking area and simply doesn't render where you'd expect.
162
+ - **`worldContext` starts out `undefined`.** World creation is asynchronous
163
+ — any custom descendant consuming the context must handle the
164
+ not-yet-ready case, the same way `<top-viewer-toolbar>` does.
165
+ - **The host needs real, resolved dimensions from its parent.** `width:
166
+ 100%; height: 100%` alone isn't enough if the parent's own size hasn't
167
+ resolved (e.g. an unsized grid area) — give the viewer's container an
168
+ explicit size.
169
+
170
+ ### 📥 Loading a model
171
+
172
+ Load any `.frag` through the shared `OBC.FragmentsManager` — `<top-viewer>`
173
+ picks it up and frames it automatically, no matter where in your app the
174
+ load happens:
175
+
176
+ ```ts
177
+ const fragments = components.get(OBC.FragmentsManager);
178
+ const file = await fetch("https://your-storage.example.com/model.frag");
179
+ const buffer = await file.arrayBuffer();
180
+ await fragments.core.load(buffer, { modelId: "example-model" });
181
+ ```
182
+ */