@thatopen/services 0.11.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.
- package/dist/built-in/index.d.ts +232 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +4 -3
- package/docs/builtin/paths.json +12 -0
- package/package.json +1 -1
- package/src/built-in/ProjectManager/example.ts +163 -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,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
|
+
*/
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## top-viewer-toolbar
|
|
3
|
+
---
|
|
4
|
+
A 3D viewport is only as useful as the controls around it: selecting a
|
|
5
|
+
target set, hiding/isolating/ghosting elements, clipping, measuring,
|
|
6
|
+
walking through the model. Building that toolbar — and keeping every
|
|
7
|
+
button in sync with tool state and with each other — is real work, before
|
|
8
|
+
you've customized anything.
|
|
9
|
+
|
|
10
|
+
`top-viewer-toolbar` is a ready-made web component that delivers exactly
|
|
11
|
+
that. Mounted as-is, it's a complete, self-wiring bottom toolbar. But it's
|
|
12
|
+
also built out of standalone, individually-importable
|
|
13
|
+
`<top-viewer-*-button>` elements — so if the default layout isn't what your
|
|
14
|
+
app needs, you can drop the bar it builds for you and compose your own out
|
|
15
|
+
of the same pieces, mixed freely with your own custom buttons.
|
|
16
|
+
|
|
17
|
+
This tutorial covers the prerequisites; the default, batteries-included
|
|
18
|
+
toolbar; what it manages automatically; composing a custom layout with
|
|
19
|
+
`headless`; docking a vertical toolbar; and the full list of standalone
|
|
20
|
+
buttons available.
|
|
21
|
+
|
|
22
|
+
By the end, you'll have both a working default toolbar and a fully custom
|
|
23
|
+
one running side by side in the same viewport.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { html } from "lit";
|
|
27
|
+
import * as THREE from "three";
|
|
28
|
+
import * as OBC from "@thatopen/components";
|
|
29
|
+
import * as OBF from "@thatopen/components-front";
|
|
30
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
31
|
+
import * as BUI from "@thatopen/ui";
|
|
32
|
+
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
33
|
+
import type { App } from "../app/index";
|
|
34
|
+
|
|
35
|
+
/* MD
|
|
36
|
+
### ✅ Prerequisites
|
|
37
|
+
|
|
38
|
+
`<top-viewer-toolbar>` must be mounted as a descendant of `<top-viewer>` —
|
|
39
|
+
it consumes the `componentsContext`/`worldContext` that `<top-viewer>`
|
|
40
|
+
provides, and the standalone buttons it's built from consume the same
|
|
41
|
+
contexts. `UIManager` must be in the setup call so both custom elements are
|
|
42
|
+
registered. Nothing else is required — no manager to `init`, no extra
|
|
43
|
+
config.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const client = PlatformClient.fromPlatformContext();
|
|
47
|
+
|
|
48
|
+
const { components } = (await client.setup(
|
|
49
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
50
|
+
{ uuid: UIManager.uuid },
|
|
51
|
+
)) as { components: OBC.Components };
|
|
52
|
+
|
|
53
|
+
components.get(UIManager).init();
|
|
54
|
+
|
|
55
|
+
/* MD
|
|
56
|
+
### 🖥️ Default toolbar (batteries included)
|
|
57
|
+
|
|
58
|
+
Add it as a plain child of `<top-viewer>` — no `slot` needed, it docks
|
|
59
|
+
into `"bottom"` on its own:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<top-viewer>
|
|
63
|
+
<top-viewer-toolbar></top-viewer-toolbar>
|
|
64
|
+
</top-viewer>
|
|
65
|
+
```
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
69
|
+
|
|
70
|
+
app.setup = (waitUntil) => {
|
|
71
|
+
waitUntil(
|
|
72
|
+
(async () => {
|
|
73
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
74
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
75
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
|
|
76
|
+
})(),
|
|
77
|
+
"Fragments Core",
|
|
78
|
+
);
|
|
79
|
+
return { components, client };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
app.elements = {
|
|
83
|
+
viewer: () => html`
|
|
84
|
+
<top-viewer>
|
|
85
|
+
<top-viewer-toolbar></top-viewer-toolbar>
|
|
86
|
+
</top-viewer>
|
|
87
|
+
`,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
app.layouts = {
|
|
91
|
+
main: {
|
|
92
|
+
label: "Main",
|
|
93
|
+
icon: "solar:3d-square-bold",
|
|
94
|
+
template: `"viewer" 1fr / 1fr`,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
app.layout = "main";
|
|
99
|
+
|
|
100
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
101
|
+
container.appendChild(app);
|
|
102
|
+
document.body.style.margin = "0";
|
|
103
|
+
|
|
104
|
+
/* MD
|
|
105
|
+
### 🤖 What the default toolbar manages automatically
|
|
106
|
+
|
|
107
|
+
**Target mode** — a toggle between "Selected" and "Unselected", which every
|
|
108
|
+
visibility button below operates on. Empty selection while targeting
|
|
109
|
+
"Selected" means the target is empty (actions no-op); targeting
|
|
110
|
+
"Unselected" with an empty selection means the target is the whole model.
|
|
111
|
+
|
|
112
|
+
**Visibility actions** (each reversible via Show/Reset) — Hide, Show,
|
|
113
|
+
Isolate, Ghost (semi-transparent), Focus, and a global Reset that restores
|
|
114
|
+
full visibility and opacity regardless of target mode.
|
|
115
|
+
|
|
116
|
+
**Inspect tab** — Select (default tool), Clip, and six measurement modes
|
|
117
|
+
(length, area, angle, edge, face, volume), all sharing one active-tool
|
|
118
|
+
manager so switching tools un-highlights whichever was active before.
|
|
119
|
+
|
|
120
|
+
**Projection + walkthrough** — a Perspective⇄Orthographic toggle and a
|
|
121
|
+
first-person walkthrough toggle, each reflecting live state.
|
|
122
|
+
|
|
123
|
+
**Active-tool HUD** — a floating overlay over the 3D viewport itself
|
|
124
|
+
showing context for whichever tool is currently active (e.g. clip plane
|
|
125
|
+
controls, measurement readouts).
|
|
126
|
+
|
|
127
|
+
All of the above are per-`components` singletons — any other UI in your
|
|
128
|
+
app that resolves the same tool (e.g. via `components.get(...)`) shares
|
|
129
|
+
the exact same state, not a second, independent instance.
|
|
130
|
+
|
|
131
|
+
### 🧱 Composing a custom layout with `headless`
|
|
132
|
+
|
|
133
|
+
Set the `headless` attribute to skip the default bar's own assembly, while
|
|
134
|
+
everything else — providing context to descendants, the active-tool HUD —
|
|
135
|
+
keeps running. Slot your own buttons as light-DOM children: they consume
|
|
136
|
+
the same context the default bar's buttons do, wherever you place them.
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<top-viewer>
|
|
140
|
+
<top-viewer-toolbar headless>
|
|
141
|
+
<!-- your own content goes here as light-DOM children -->
|
|
142
|
+
</top-viewer-toolbar>
|
|
143
|
+
</top-viewer>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Mixing standalone buttons with a plain, app-owned `bim-button` in the same
|
|
147
|
+
bar — built with lit's `render`/`html` into a plain container element
|
|
148
|
+
(never `document.createElement` for the `bim-*` tags themselves, see the
|
|
149
|
+
gotcha below), then appended as the toolbar's light-DOM content:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const headlessBar = document.createElement("top-viewer-toolbar");
|
|
153
|
+
headlessBar.toggleAttribute("headless", true);
|
|
154
|
+
|
|
155
|
+
const customButtons = document.createElement("bim-toolbar") as unknown as HTMLElement;
|
|
156
|
+
customButtons.setAttribute("style", "pointer-events: auto;");
|
|
157
|
+
render(
|
|
158
|
+
html`
|
|
159
|
+
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
160
|
+
<top-viewer-mode-toggle-button></top-viewer-mode-toggle-button>
|
|
161
|
+
<top-viewer-hide-button></top-viewer-hide-button>
|
|
162
|
+
<top-viewer-select-button></top-viewer-select-button>
|
|
163
|
+
</bim-toolbar-section>
|
|
164
|
+
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
165
|
+
<bim-button
|
|
166
|
+
icon="mdi:fit-to-page-outline"
|
|
167
|
+
@click=${() => myOwnFitToViewLogic()}
|
|
168
|
+
><bim-tooltip placement="top">Zoom to fit</bim-tooltip></bim-button>
|
|
169
|
+
</bim-toolbar-section>
|
|
170
|
+
`,
|
|
171
|
+
customButtons,
|
|
172
|
+
);
|
|
173
|
+
headlessBar.appendChild(customButtons);
|
|
174
|
+
// headlessBar can now be appended anywhere inside <top-viewer>.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 📐 Docking a vertical toolbar
|
|
178
|
+
|
|
179
|
+
The host centers its own content via flexbox, controllable through CSS
|
|
180
|
+
custom properties — including flipping to a vertical stack for a
|
|
181
|
+
sidebar-style dock:
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<top-viewer-toolbar
|
|
185
|
+
headless
|
|
186
|
+
slot="right"
|
|
187
|
+
style="--top-viewer-toolbar-direction: column;"
|
|
188
|
+
>
|
|
189
|
+
...
|
|
190
|
+
</top-viewer-toolbar>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- `--top-viewer-toolbar-direction` — `row` (default) or `column`.
|
|
194
|
+
- `--top-viewer-toolbar-justify` — alignment along the main axis (default
|
|
195
|
+
`center`).
|
|
196
|
+
- `--top-viewer-toolbar-align` — alignment along the cross axis (default
|
|
197
|
+
`center`).
|
|
198
|
+
|
|
199
|
+
Pair `--top-viewer-toolbar-direction: column` with `bim-toolbar`'s own
|
|
200
|
+
`vertical` attribute on the content you render inside, so the buttons
|
|
201
|
+
themselves stack the same way the bar does.
|
|
202
|
+
|
|
203
|
+
### ⚠️ Gotchas
|
|
204
|
+
|
|
205
|
+
- **Never `document.createElement` a `bim-*` element.** `bim-button`,
|
|
206
|
+
`bim-toolbar`, `bim-toolbar-section`, and every other `@thatopen/ui`
|
|
207
|
+
component set attributes on themselves in their own constructor, which
|
|
208
|
+
Custom Elements v1 forbids for elements created imperatively —
|
|
209
|
+
`document.createElement("bim-button")` throws `NotSupportedError`.
|
|
210
|
+
Render them with lit's `render(html\`...\`, container)` instead, exactly
|
|
211
|
+
like the `customButtons` bar above — never build them by hand.
|
|
212
|
+
- **`bim-button` sizes itself** — square when it's icon-only, a consistent
|
|
213
|
+
height when it has a label. Don't set `width`/`height`/`min-width`
|
|
214
|
+
inline on it; that only fights its own layout and produces buttons that
|
|
215
|
+
look inconsistent next to the rest of the bar.
|
|
216
|
+
- **Centering/alignment belongs on the toolbar's host**, via the CSS custom
|
|
217
|
+
properties above — not on the content you slot into it. Setting
|
|
218
|
+
`justify-self` (or similar) on your own buttons instead of using
|
|
219
|
+
`--top-viewer-toolbar-justify` only works by accident, if at all.
|
|
220
|
+
- **A standalone button works from anywhere in the DOM**, as long as it
|
|
221
|
+
ends up a descendant of a `<top-viewer-toolbar>` inside a `<top-viewer>`
|
|
222
|
+
— context requests bubble through shadow-DOM boundaries. It does *not*
|
|
223
|
+
need to be a direct child of the bar it visually sits in.
|
|
224
|
+
|
|
225
|
+
### 🧩 Standalone buttons reference
|
|
226
|
+
|
|
227
|
+
Every button the default bar uses is independently importable/usable as
|
|
228
|
+
its own custom element:
|
|
229
|
+
|
|
230
|
+
| Element | Does |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `<top-viewer-mode-toggle-button>` | Toggles target: Selected ⇄ Unselected |
|
|
233
|
+
| `<top-viewer-hide-button>` | Hides the target set |
|
|
234
|
+
| `<top-viewer-show-button>` | Un-hides the target set |
|
|
235
|
+
| `<top-viewer-isolate-button>` | Shows only the target set |
|
|
236
|
+
| `<top-viewer-ghost-button>` | Renders the target set semi-transparent |
|
|
237
|
+
| `<top-viewer-focus-button>` | Frames the camera on the target set |
|
|
238
|
+
| `<top-viewer-reset-button>` | Restores full visibility + opacity (global) |
|
|
239
|
+
| `<top-viewer-select-button>` | Activates the default Select tool |
|
|
240
|
+
| `<top-viewer-clip-button>` | Activates the Clip tool |
|
|
241
|
+
| `<top-viewer-measure-length-button>` | Activates length measurement |
|
|
242
|
+
| `<top-viewer-measure-area-button>` | Activates area measurement |
|
|
243
|
+
| `<top-viewer-measure-angle-button>` | Activates angle measurement |
|
|
244
|
+
| `<top-viewer-measure-edge-button>` | Activates edge measurement |
|
|
245
|
+
| `<top-viewer-measure-face-button>` | Activates face measurement |
|
|
246
|
+
| `<top-viewer-measure-volume-button>` | Activates volume measurement |
|
|
247
|
+
| `<top-viewer-projection-toggle-button>` | Toggles Perspective ⇄ Orthographic |
|
|
248
|
+
| `<top-viewer-walkthrough-button>` | Toggles first-person walkthrough |
|
|
249
|
+
|
|
250
|
+
Each one self-wires from context — drop it anywhere under `<top-viewer>`
|
|
251
|
+
and it works, with no props to pass in.
|
|
252
|
+
*/
|