@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,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
|
+
*/
|
package/src/built-in/index.ts
CHANGED
|
@@ -1576,6 +1576,239 @@ export type HelloWorld = InstanceType<typeof _HelloWorld>;
|
|
|
1576
1576
|
*/
|
|
1577
1577
|
export const HelloWorld = { uuid: '2c4ae432-fc24-43e9-9783-0c960c674e96' } as typeof _HelloWorld & { uuid: '2c4ae432-fc24-43e9-9783-0c960c674e96' };
|
|
1578
1578
|
|
|
1579
|
+
export type ProjectNamespaceKey = "origin" | "georeferencing" | "appSettings";
|
|
1580
|
+
export interface ProjectNamespace {
|
|
1581
|
+
label: string;
|
|
1582
|
+
description?: string;
|
|
1583
|
+
}
|
|
1584
|
+
export interface ProjectEntity<T = unknown> {
|
|
1585
|
+
namespaceId: ProjectNamespaceKey;
|
|
1586
|
+
label: string;
|
|
1587
|
+
description?: string;
|
|
1588
|
+
data: T;
|
|
1589
|
+
}
|
|
1590
|
+
/** GIS origin point that fixes the scene's (0,0,0) to a real-world coordinate. */
|
|
1591
|
+
export interface OriginData {
|
|
1592
|
+
lat: number;
|
|
1593
|
+
lon: number;
|
|
1594
|
+
height: number;
|
|
1595
|
+
rotation: number;
|
|
1596
|
+
}
|
|
1597
|
+
export interface BimSite {
|
|
1598
|
+
label: string;
|
|
1599
|
+
description?: string;
|
|
1600
|
+
lat: number;
|
|
1601
|
+
lon: number;
|
|
1602
|
+
height: number;
|
|
1603
|
+
rotation: number;
|
|
1604
|
+
/** Column-major 4x4 matrix (16 values) mapping BIM local space to the site's georeferenced frame. */
|
|
1605
|
+
baseMatrix: number[];
|
|
1606
|
+
}
|
|
1607
|
+
export interface BimCoordinatesData {
|
|
1608
|
+
defaultSiteId?: string;
|
|
1609
|
+
sites: Record<string, BimSite>;
|
|
1610
|
+
}
|
|
1611
|
+
export type AssetCoordinateType = "splat" | "ifc" | "point-cloud" | (string & {});
|
|
1612
|
+
export interface AssetCoordinate {
|
|
1613
|
+
fileId: string;
|
|
1614
|
+
type: AssetCoordinateType;
|
|
1615
|
+
lat: number;
|
|
1616
|
+
lon: number;
|
|
1617
|
+
height: number;
|
|
1618
|
+
rotation: number;
|
|
1619
|
+
}
|
|
1620
|
+
export interface GraphicsSettingsData {
|
|
1621
|
+
stylePreset: string;
|
|
1622
|
+
postproduction: boolean;
|
|
1623
|
+
edges: {
|
|
1624
|
+
enabled: boolean;
|
|
1625
|
+
color: string;
|
|
1626
|
+
opacity: number;
|
|
1627
|
+
};
|
|
1628
|
+
ambientOcclusion: {
|
|
1629
|
+
enabled: boolean;
|
|
1630
|
+
value: number;
|
|
1631
|
+
};
|
|
1632
|
+
tonalShading: {
|
|
1633
|
+
enabled: boolean;
|
|
1634
|
+
value: number;
|
|
1635
|
+
};
|
|
1636
|
+
surfaceColor: {
|
|
1637
|
+
enabled: boolean;
|
|
1638
|
+
};
|
|
1639
|
+
transparentBackground: {
|
|
1640
|
+
enabled: boolean;
|
|
1641
|
+
color: string;
|
|
1642
|
+
};
|
|
1643
|
+
grid: {
|
|
1644
|
+
enabled: boolean;
|
|
1645
|
+
color: string;
|
|
1646
|
+
};
|
|
1647
|
+
gridHeight: {
|
|
1648
|
+
auto: boolean;
|
|
1649
|
+
value: number;
|
|
1650
|
+
};
|
|
1651
|
+
antiAliasing: boolean;
|
|
1652
|
+
nativeResolution: boolean;
|
|
1653
|
+
adaptiveResolution: {
|
|
1654
|
+
enabled: boolean;
|
|
1655
|
+
value: number;
|
|
1656
|
+
};
|
|
1657
|
+
minResolution: number;
|
|
1658
|
+
renderScale: number;
|
|
1659
|
+
}
|
|
1660
|
+
export interface ProjectManagerData {
|
|
1661
|
+
schemaVersion: 1;
|
|
1662
|
+
namespaces: Record<ProjectNamespaceKey, ProjectNamespace>;
|
|
1663
|
+
entities: {
|
|
1664
|
+
origin: ProjectEntity<OriginData>;
|
|
1665
|
+
bimCoordinates: ProjectEntity<BimCoordinatesData>;
|
|
1666
|
+
assetCoordinates: ProjectEntity<AssetCoordinate[]>;
|
|
1667
|
+
graphicsSettings: ProjectEntity<GraphicsSettingsData>;
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
export interface SingletonEntity<T> {
|
|
1671
|
+
get(): T | undefined;
|
|
1672
|
+
set(data: T): Promise<void>;
|
|
1673
|
+
update(partial: Partial<T>): Promise<void>;
|
|
1674
|
+
}
|
|
1675
|
+
export interface BimCoordinatesEntity {
|
|
1676
|
+
get(): BimCoordinatesData;
|
|
1677
|
+
getSites(): Record<string, BimSite>;
|
|
1678
|
+
getSite(siteId: string): BimSite | undefined;
|
|
1679
|
+
getDefaultSite(): BimSite | undefined;
|
|
1680
|
+
/** Creates a new site. `ProjectManager` generates the opaque `siteId` — callers
|
|
1681
|
+
* never invent their own (see the class-level JSDoc on `ProjectManager` for why). */
|
|
1682
|
+
createSite(site: BimSite): Promise<string>;
|
|
1683
|
+
/** Updates a site that already exists (from `createSite`). Throws if `siteId`
|
|
1684
|
+
* isn't in `sites` — this is not an upsert; use `createSite` to add one. */
|
|
1685
|
+
setSite(siteId: string, site: BimSite): Promise<void>;
|
|
1686
|
+
setDefaultSite(siteId: string): Promise<void>;
|
|
1687
|
+
removeSite(siteId: string): Promise<void>;
|
|
1688
|
+
}
|
|
1689
|
+
export interface AssetCoordinatesEntity {
|
|
1690
|
+
getAll(): AssetCoordinate[];
|
|
1691
|
+
get(fileId: string): AssetCoordinate | undefined;
|
|
1692
|
+
getByType(type: AssetCoordinateType): AssetCoordinate[];
|
|
1693
|
+
set(fileId: string, data: Omit<AssetCoordinate, "fileId">): Promise<void>;
|
|
1694
|
+
remove(fileId: string): Promise<void>;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* ProjectManager is the platform's canonical store for project-level
|
|
1699
|
+
* configuration and data — origin/georeferencing/app-settings today, with
|
|
1700
|
+
* room to grow. It persists a single JSON file (`${ProjectManager.uuid}.json`)
|
|
1701
|
+
* loose directly inside `__project_data` (same folder convention `GISManager`
|
|
1702
|
+
* already used) — NOT in a per-app subfolder like `collider`/`cde`, which are
|
|
1703
|
+
* for secondary, per-builtin data. Sharing that root folder with other
|
|
1704
|
+
* builtins doesn't change its role: it's still the one canonical project
|
|
1705
|
+
* record, meant to stop each builtin from inventing its own ad-hoc
|
|
1706
|
+
* project-persistence pattern.
|
|
1707
|
+
*
|
|
1708
|
+
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1709
|
+
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1710
|
+
* closed set this code defines, never created at runtime.
|
|
1711
|
+
* - `bimCoordinates.data.sites` is the one open, runtime-created collection
|
|
1712
|
+
* today, so its keys are opaque generated ids instead of slugs.
|
|
1713
|
+
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1714
|
+
* are collections.
|
|
1715
|
+
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1716
|
+
* optional `description` (omitted, not stored empty), free-form `data`.
|
|
1717
|
+
*
|
|
1718
|
+
* Id generation rule: whenever a collection needs an opaque id (today just
|
|
1719
|
+
* `sites`, potentially more collections later), `ProjectManager` generates it
|
|
1720
|
+
* internally (`crypto.randomUUID()`, no prefix) — callers never invent their
|
|
1721
|
+
* own. `createSite` is the current example; follow the same pattern for any
|
|
1722
|
+
* future `create*` method.
|
|
1723
|
+
*/
|
|
1724
|
+
declare class _ProjectManager extends OBC.Component {
|
|
1725
|
+
static readonly uuid: "6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f";
|
|
1726
|
+
enabled: boolean;
|
|
1727
|
+
/** True once init(client) has been called — safe to use the entity accessors. */
|
|
1728
|
+
get ready(): boolean;
|
|
1729
|
+
/** Suppress auto-save while bulk-loading data. */
|
|
1730
|
+
private _loadingData;
|
|
1731
|
+
readonly onSaveStart: OBC.Event<void>;
|
|
1732
|
+
readonly onSaveComplete: OBC.Event<boolean>;
|
|
1733
|
+
private _client?;
|
|
1734
|
+
private _fileId?;
|
|
1735
|
+
private _data;
|
|
1736
|
+
private _saveTimer?;
|
|
1737
|
+
private _savePromise?;
|
|
1738
|
+
private _scheduleSave;
|
|
1739
|
+
private _queueSave;
|
|
1740
|
+
private _doSave;
|
|
1741
|
+
/** Cancels the pending debounce and runs the save immediately. Awaits completion. */
|
|
1742
|
+
flush(): Promise<void>;
|
|
1743
|
+
init(client: PlatformClient): Promise<void>;
|
|
1744
|
+
/** Opaque id generator for any collection needing one (currently just
|
|
1745
|
+
* `bimCoordinates.sites`) — see the class-level JSDoc's id-generation rule. */
|
|
1746
|
+
private _generateId;
|
|
1747
|
+
origin: SingletonEntity<OriginData>;
|
|
1748
|
+
graphicsSettings: SingletonEntity<GraphicsSettingsData>;
|
|
1749
|
+
bimCoordinates: BimCoordinatesEntity;
|
|
1750
|
+
assetCoordinates: AssetCoordinatesEntity;
|
|
1751
|
+
getNamespaces(): Record<ProjectNamespaceKey, ProjectNamespace>;
|
|
1752
|
+
getNamespace(key: ProjectNamespaceKey): Record<string, unknown>;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* ProjectManager is the platform's canonical store for project-level
|
|
1757
|
+
* configuration and data — origin/georeferencing/app-settings today, with
|
|
1758
|
+
* room to grow. It persists a single JSON file (`${ProjectManager.uuid}.json`)
|
|
1759
|
+
* loose directly inside `__project_data` (same folder convention `GISManager`
|
|
1760
|
+
* already used) — NOT in a per-app subfolder like `collider`/`cde`, which are
|
|
1761
|
+
* for secondary, per-builtin data. Sharing that root folder with other
|
|
1762
|
+
* builtins doesn't change its role: it's still the one canonical project
|
|
1763
|
+
* record, meant to stop each builtin from inventing its own ad-hoc
|
|
1764
|
+
* project-persistence pattern.
|
|
1765
|
+
*
|
|
1766
|
+
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1767
|
+
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1768
|
+
* closed set this code defines, never created at runtime.
|
|
1769
|
+
* - `bimCoordinates.data.sites` is the one open, runtime-created collection
|
|
1770
|
+
* today, so its keys are opaque generated ids instead of slugs.
|
|
1771
|
+
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1772
|
+
* are collections.
|
|
1773
|
+
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1774
|
+
* optional `description` (omitted, not stored empty), free-form `data`.
|
|
1775
|
+
*
|
|
1776
|
+
* Id generation rule: whenever a collection needs an opaque id (today just
|
|
1777
|
+
* `sites`, potentially more collections later), `ProjectManager` generates it
|
|
1778
|
+
* internally (`crypto.randomUUID()`, no prefix) — callers never invent their
|
|
1779
|
+
* own. `createSite` is the current example; follow the same pattern for any
|
|
1780
|
+
* future `create*` method.
|
|
1781
|
+
*/
|
|
1782
|
+
export type ProjectManager = InstanceType<typeof _ProjectManager>;
|
|
1783
|
+
/**
|
|
1784
|
+
* ProjectManager is the platform's canonical store for project-level
|
|
1785
|
+
* configuration and data — origin/georeferencing/app-settings today, with
|
|
1786
|
+
* room to grow. It persists a single JSON file (`${ProjectManager.uuid}.json`)
|
|
1787
|
+
* loose directly inside `__project_data` (same folder convention `GISManager`
|
|
1788
|
+
* already used) — NOT in a per-app subfolder like `collider`/`cde`, which are
|
|
1789
|
+
* for secondary, per-builtin data. Sharing that root folder with other
|
|
1790
|
+
* builtins doesn't change its role: it's still the one canonical project
|
|
1791
|
+
* record, meant to stop each builtin from inventing its own ad-hoc
|
|
1792
|
+
* project-persistence pattern.
|
|
1793
|
+
*
|
|
1794
|
+
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1795
|
+
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1796
|
+
* closed set this code defines, never created at runtime.
|
|
1797
|
+
* - `bimCoordinates.data.sites` is the one open, runtime-created collection
|
|
1798
|
+
* today, so its keys are opaque generated ids instead of slugs.
|
|
1799
|
+
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1800
|
+
* are collections.
|
|
1801
|
+
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1802
|
+
* optional `description` (omitted, not stored empty), free-form `data`.
|
|
1803
|
+
*
|
|
1804
|
+
* Id generation rule: whenever a collection needs an opaque id (today just
|
|
1805
|
+
* `sites`, potentially more collections later), `ProjectManager` generates it
|
|
1806
|
+
* internally (`crypto.randomUUID()`, no prefix) — callers never invent their
|
|
1807
|
+
* own. `createSite` is the current example; follow the same pattern for any
|
|
1808
|
+
* future `create*` method.
|
|
1809
|
+
*/
|
|
1810
|
+
export const ProjectManager = { uuid: '6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f' } as typeof _ProjectManager & { uuid: '6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f' };
|
|
1811
|
+
|
|
1579
1812
|
/** `main.bcf`'s path relative to `__project_data/` — shared with
|
|
1580
1813
|
* `requestBcfSave` (`UIManager/src/utils/bcf-storage.ts`) so both sides of
|
|
1581
1814
|
* the save/load round-trip agree on where the file lives. */
|