@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.
@@ -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
+ */