@thatopen/services 0.11.0 → 0.13.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.
- package/dist/built-in/index.d.ts +232 -0
- package/dist/cli/lib/engine-script.d.ts +9 -2
- package/dist/cli.js +17 -4
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +4 -3
- package/docs/builtin/paths.json +16 -0
- package/package.json +1 -1
- package/src/built-in/ProjectManager/example.ts +163 -0
- package/src/built-in/UIManager/src/settings-panel/example.ts +298 -0
- package/src/built-in/UIManager/src/viewer/example.ts +182 -0
- package/src/built-in/UIManager/src/viewer-toolbar/example.ts +252 -0
- package/src/built-in/index.ts +233 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## top-settings-panel
|
|
3
|
+
---
|
|
4
|
+
Every viewer app ends up needing the same settings screen: rendering
|
|
5
|
+
options, clip/measurement styling, a "which cloud component converts this
|
|
6
|
+
format" picker, a keyboard-shortcut reference — plus a search box that
|
|
7
|
+
filters all of it and a "restore defaults" button that resets whatever has
|
|
8
|
+
persisted state. Building that, and keeping it in sync with the tools it
|
|
9
|
+
configures, is real work before you've added a single setting of your own.
|
|
10
|
+
|
|
11
|
+
`top-settings-panel` is a ready-made web component that delivers exactly
|
|
12
|
+
that. Mounted as-is, it's a complete, self-wiring Settings panel. But like
|
|
13
|
+
`top-viewer-toolbar` (see that tutorial), it's built out of standalone,
|
|
14
|
+
individually-importable `<top-*-settings>` elements — so you can drop the
|
|
15
|
+
panel it builds for you and compose your own layout out of the same
|
|
16
|
+
pieces, mixed freely with sections of your own.
|
|
17
|
+
|
|
18
|
+
This tutorial covers the prerequisites; the default, batteries-included
|
|
19
|
+
panel; what it manages automatically; building your own section with
|
|
20
|
+
`bim-panel-section`; and composing a custom layout with `headless`.
|
|
21
|
+
|
|
22
|
+
By the end, you'll have a working default panel plus your own custom
|
|
23
|
+
section, both wired into the same shared search box and reset button as
|
|
24
|
+
the stock ones.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { html, LitElement } from "lit";
|
|
28
|
+
import { customElement, state } from "lit/decorators.js";
|
|
29
|
+
import { consume } from "@lit/context";
|
|
30
|
+
import * as THREE from "three";
|
|
31
|
+
import * as OBC from "@thatopen/components";
|
|
32
|
+
import * as OBF from "@thatopen/components-front";
|
|
33
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
34
|
+
import * as BUI from "@thatopen/ui";
|
|
35
|
+
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
36
|
+
// TODO(#34 follow-up): `settingsSearchContext` is not part of the published
|
|
37
|
+
// `@thatopen/services` surface yet — see the TODO in settings-panel/index.ts.
|
|
38
|
+
// Imported by local relative path until the distribute pipeline exports
|
|
39
|
+
// context objects too; same workaround `app/examples/Contexts/example.ts`
|
|
40
|
+
// uses for `componentsContext`/`clientContext`.
|
|
41
|
+
import { settingsSearchContext } from "./src/context";
|
|
42
|
+
import type { App } from "../app/index";
|
|
43
|
+
|
|
44
|
+
/* MD
|
|
45
|
+
### ✅ Prerequisites
|
|
46
|
+
|
|
47
|
+
`UIManager` must be in the setup call so `<top-settings-panel>` and every
|
|
48
|
+
`<top-*-settings>` element are registered. `<top-settings-panel>` must be
|
|
49
|
+
mounted as a descendant of `<top-app>` — it and its sections consume
|
|
50
|
+
`componentsContext`/`clientContext`, which `top-app` provides. It does
|
|
51
|
+
**not** need to be a descendant of `<top-viewer>` specifically — but the
|
|
52
|
+
sections that talk to the 3D engine (Graphics, Clip styling, Measurement,
|
|
53
|
+
Commands) render an empty placeholder until a `<top-viewer>` exists
|
|
54
|
+
somewhere in the same app, since that's what actually builds the tools they
|
|
55
|
+
configure.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const client = PlatformClient.fromPlatformContext();
|
|
59
|
+
|
|
60
|
+
const { components } = (await client.setup(
|
|
61
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
62
|
+
{ uuid: UIManager.uuid },
|
|
63
|
+
)) as { components: OBC.Components };
|
|
64
|
+
|
|
65
|
+
components.get(UIManager).init();
|
|
66
|
+
|
|
67
|
+
/* MD
|
|
68
|
+
### 🖥️ Default panel (batteries included)
|
|
69
|
+
|
|
70
|
+
Add it as a plain element anywhere under `top-app` — a side-by-side layout
|
|
71
|
+
with the 3D viewer is the most natural arrangement, since most of its
|
|
72
|
+
sections act on what's in the scene:
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<top-viewer>...</top-viewer>
|
|
76
|
+
<top-settings-panel></top-settings-panel>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 🤖 What the default panel manages automatically
|
|
80
|
+
|
|
81
|
+
**Graphics** — point/line/edge styling, background, shadows, ambient
|
|
82
|
+
occlusion — whatever `top-viewer` supports, persisted across reloads.
|
|
83
|
+
|
|
84
|
+
**Clip styling** — fill/edge color, opacity, width for the clip tool,
|
|
85
|
+
shared live with `top-viewer-toolbar`'s clip button.
|
|
86
|
+
|
|
87
|
+
**Measurement** — units (length/area/angle), color, decimals, per-snap-kind
|
|
88
|
+
toggles, shared live with the toolbar's measurement buttons.
|
|
89
|
+
|
|
90
|
+
**File conversion** — which provisioned cloud component converts each
|
|
91
|
+
format when a file is opened, per project.
|
|
92
|
+
|
|
93
|
+
**Commands** — a click-to-run shortcut reference (focus, hide, isolate,
|
|
94
|
+
ghost, show all, clear selection) that also registers the actual keyboard
|
|
95
|
+
shortcuts while mounted.
|
|
96
|
+
|
|
97
|
+
**Search box** — filters every section's own rows by label, stock or
|
|
98
|
+
custom, as long as a section opts in (see below).
|
|
99
|
+
|
|
100
|
+
**Restore defaults** — a two-click-confirm button that resets every
|
|
101
|
+
section with persisted state (currently: Graphics) back to its defaults.
|
|
102
|
+
|
|
103
|
+
### 🧩 Building your own section
|
|
104
|
+
|
|
105
|
+
A custom section is a `LitElement` that renders its own `<bim-panel-section>`
|
|
106
|
+
— nothing about it is specific to `top-settings-panel` beyond that. Consume
|
|
107
|
+
`settingsSearchContext` if you want the shared search box to filter your
|
|
108
|
+
rows too; skip it and your section simply never filters.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
@customElement("my-lidar-settings")
|
|
112
|
+
class MyLidarSettings extends LitElement {
|
|
113
|
+
@consume({ context: settingsSearchContext, subscribe: true })
|
|
114
|
+
searchQuery = "";
|
|
115
|
+
|
|
116
|
+
@state() private _pointBudget = 2_000_000;
|
|
117
|
+
@state() private _showIntensity = false;
|
|
118
|
+
|
|
119
|
+
render() {
|
|
120
|
+
const rows = [
|
|
121
|
+
{
|
|
122
|
+
label: "Point budget",
|
|
123
|
+
control: html`<bim-number-input
|
|
124
|
+
label="Point budget"
|
|
125
|
+
slider
|
|
126
|
+
value=${this._pointBudget}
|
|
127
|
+
min="100000"
|
|
128
|
+
max="10000000"
|
|
129
|
+
step="100000"
|
|
130
|
+
@change=${(e: Event) => {
|
|
131
|
+
this._pointBudget = Number((e.target as HTMLInputElement).value);
|
|
132
|
+
}}
|
|
133
|
+
></bim-number-input>`,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: "Show intensity",
|
|
137
|
+
control: html`<bim-checkbox
|
|
138
|
+
label="Show intensity"
|
|
139
|
+
toggle
|
|
140
|
+
?checked=${this._showIntensity}
|
|
141
|
+
@change=${(e: Event) => {
|
|
142
|
+
this._showIntensity = (e.target as HTMLInputElement).checked;
|
|
143
|
+
}}
|
|
144
|
+
></bim-checkbox>`,
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const q = this.searchQuery.trim().toLowerCase();
|
|
149
|
+
const visible = q
|
|
150
|
+
? rows.filter((r) => r.label.toLowerCase().includes(q))
|
|
151
|
+
: rows;
|
|
152
|
+
|
|
153
|
+
return html`<bim-panel-section label="LIDAR" icon="mdi:radar">
|
|
154
|
+
${visible.length
|
|
155
|
+
? visible.map((r) => r.control)
|
|
156
|
+
: html`<bim-label style="opacity: 0.6; font-size: 0.78rem;"
|
|
157
|
+
>No settings match "${this.searchQuery}".</bim-label
|
|
158
|
+
>`}
|
|
159
|
+
</bim-panel-section>`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* MD
|
|
164
|
+
### 🧱 Composing a custom layout with `headless`
|
|
165
|
+
|
|
166
|
+
Set the `headless` attribute to skip the panel's own 5 stock sections —
|
|
167
|
+
everything else (the search box, the reset button, both contexts) keeps
|
|
168
|
+
running. Slot whatever `top-*-settings` elements and/or your own sections
|
|
169
|
+
as light-DOM children, in whatever order you want; that's exactly what
|
|
170
|
+
renders.
|
|
171
|
+
|
|
172
|
+
The most common case: keep every stock section and just add your own —
|
|
173
|
+
this is the live demo below.
|
|
174
|
+
|
|
175
|
+
```html
|
|
176
|
+
<top-settings-panel headless>
|
|
177
|
+
<top-graphic-settings></top-graphic-settings>
|
|
178
|
+
<top-clipstyling-settings></top-clipstyling-settings>
|
|
179
|
+
<top-measurement-settings></top-measurement-settings>
|
|
180
|
+
<top-fileconversion-settings></top-fileconversion-settings>
|
|
181
|
+
<top-commands-settings></top-commands-settings>
|
|
182
|
+
<my-lidar-settings></my-lidar-settings>
|
|
183
|
+
</top-settings-panel>
|
|
184
|
+
```
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
188
|
+
|
|
189
|
+
app.setup = (waitUntil) => {
|
|
190
|
+
waitUntil(
|
|
191
|
+
(async () => {
|
|
192
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
193
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
194
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
|
|
195
|
+
})(),
|
|
196
|
+
"Fragments Core",
|
|
197
|
+
);
|
|
198
|
+
return { components, client };
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
app.elements = {
|
|
202
|
+
viewer: () =>
|
|
203
|
+
html`<top-viewer><top-viewer-toolbar></top-viewer-toolbar></top-viewer>`,
|
|
204
|
+
settings: () => html`
|
|
205
|
+
<top-settings-panel headless>
|
|
206
|
+
<top-graphic-settings></top-graphic-settings>
|
|
207
|
+
<top-clipstyling-settings></top-clipstyling-settings>
|
|
208
|
+
<top-measurement-settings></top-measurement-settings>
|
|
209
|
+
<top-fileconversion-settings></top-fileconversion-settings>
|
|
210
|
+
<top-commands-settings></top-commands-settings>
|
|
211
|
+
<my-lidar-settings></my-lidar-settings>
|
|
212
|
+
</top-settings-panel>
|
|
213
|
+
`,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
app.layouts = {
|
|
217
|
+
main: {
|
|
218
|
+
label: "Main",
|
|
219
|
+
icon: "solar:3d-square-bold",
|
|
220
|
+
template: `"viewer settings" 1fr / 1fr 22rem`,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
app.layout = "main";
|
|
225
|
+
|
|
226
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
227
|
+
container.appendChild(app);
|
|
228
|
+
document.body.style.margin = "0";
|
|
229
|
+
|
|
230
|
+
/* MD
|
|
231
|
+
### 🔀 A few other ways to compose it
|
|
232
|
+
|
|
233
|
+
A lighter viewer that only needs Graphics plus its own section — drop
|
|
234
|
+
everything else:
|
|
235
|
+
|
|
236
|
+
```html
|
|
237
|
+
<top-settings-panel headless>
|
|
238
|
+
<top-graphic-settings></top-graphic-settings>
|
|
239
|
+
<my-lidar-settings></my-lidar-settings>
|
|
240
|
+
</top-settings-panel>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Reordering — `headless` respects DOM order, so Commands first is just:
|
|
244
|
+
|
|
245
|
+
```html
|
|
246
|
+
<top-settings-panel headless>
|
|
247
|
+
<top-commands-settings></top-commands-settings>
|
|
248
|
+
<top-graphic-settings></top-graphic-settings>
|
|
249
|
+
<top-clipstyling-settings></top-clipstyling-settings>
|
|
250
|
+
<top-measurement-settings></top-measurement-settings>
|
|
251
|
+
<top-fileconversion-settings></top-fileconversion-settings>
|
|
252
|
+
</top-settings-panel>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Swapping a single stock section for your own (e.g. Measurement with
|
|
256
|
+
different units) — the same `headless` list, just with that one tag
|
|
257
|
+
replaced:
|
|
258
|
+
|
|
259
|
+
```html
|
|
260
|
+
<top-settings-panel headless>
|
|
261
|
+
<top-graphic-settings></top-graphic-settings>
|
|
262
|
+
<top-clipstyling-settings></top-clipstyling-settings>
|
|
263
|
+
<my-measurement-settings></my-measurement-settings>
|
|
264
|
+
<top-fileconversion-settings></top-fileconversion-settings>
|
|
265
|
+
<top-commands-settings></top-commands-settings>
|
|
266
|
+
</top-settings-panel>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Keeping the full stock default and only ADDING a section, without going
|
|
270
|
+
`headless` at all — the non-headless layout also projects a trailing
|
|
271
|
+
`<slot>`, so any light-DOM children you give it render after the 5 stock
|
|
272
|
+
sections:
|
|
273
|
+
|
|
274
|
+
```html
|
|
275
|
+
<top-settings-panel>
|
|
276
|
+
<my-lidar-settings></my-lidar-settings>
|
|
277
|
+
</top-settings-panel>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### ⚠️ Gotchas
|
|
281
|
+
|
|
282
|
+
- **Never `document.createElement` a `bim-*` element** — same rule as
|
|
283
|
+
`top-viewer-toolbar`. Render them with lit's `html`/`render` (or, as
|
|
284
|
+
here, from inside your own `LitElement`'s `render()`); never build them
|
|
285
|
+
imperatively.
|
|
286
|
+
- **A custom section renders its OWN `<bim-panel-section>`**, in its own
|
|
287
|
+
shadow root — there's no API to inject rows into a stock section, only
|
|
288
|
+
to sit alongside it.
|
|
289
|
+
- **`fixed` layout quirk**: `<bim-panel-section>`'s default logic checks
|
|
290
|
+
`this.closest("bim-panel")`, which can't see past your own section's
|
|
291
|
+
shadow boundary — so, like the stock sections, a custom one may render
|
|
292
|
+
with `fixed` behavior until the upstream `@thatopen/ui` fix ships. Not
|
|
293
|
+
something to work around here; tracked separately.
|
|
294
|
+
- **Search filtering is opt-in per section** — consuming
|
|
295
|
+
`settingsSearchContext` is what makes a section respond to the shared
|
|
296
|
+
search box; forgetting it just means that section never filters, not an
|
|
297
|
+
error.
|
|
298
|
+
*/
|
|
@@ -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
|
+
*/
|