@thatopen/services 0.12.0 → 0.14.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 +73 -63
- package/dist/cli/lib/engine-script.d.ts +9 -2
- package/dist/cli.js +17 -4
- package/docs/builtin/paths.json +4 -0
- package/package.json +1 -1
- package/src/built-in/ProjectManager/example.ts +4 -43
- package/src/built-in/UIManager/src/settings-panel/example.ts +298 -0
- package/src/built-in/index.ts +73 -63
package/dist/built-in/index.d.ts
CHANGED
|
@@ -1585,19 +1585,21 @@ export interface OriginData {
|
|
|
1585
1585
|
height: number;
|
|
1586
1586
|
rotation: number;
|
|
1587
1587
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1588
|
+
/** Project-wide coordinate reference system + vertical datum config, shared by
|
|
1589
|
+
* every asset type that needs to decode raw projected coordinates (point
|
|
1590
|
+
* clouds, BIM) — see automatic-file-derived-georeferencing.md
|
|
1591
|
+
* (VisorUnificadoAqualia/docs/adrs). One value per project, not per asset:
|
|
1592
|
+
* every file at the same real-world site shares the same CRS/geoid. */
|
|
1593
|
+
export interface CrsData {
|
|
1594
|
+
/** EPSG code of the project's projected CRS (e.g. 25830 for ETRS89/UTM 30N).
|
|
1595
|
+
* EPSG is a global registry, not region-specific — every project has an
|
|
1596
|
+
* applicable code, most commonly a standard UTM zone. */
|
|
1597
|
+
epsgCode: number;
|
|
1598
|
+
/** Geoid undulation in meters at the project's origin (orthometric ->
|
|
1599
|
+
* ellipsoidal height correction). Sampled once from a geoid model
|
|
1600
|
+
* (client-supplied, or a global EGM2008 fallback) — varies negligibly
|
|
1601
|
+
* across a single project's extent, so one scalar per project suffices. */
|
|
1602
|
+
geoidUndulation: number;
|
|
1601
1603
|
}
|
|
1602
1604
|
export type AssetCoordinateType = "splat" | "ifc" | "point-cloud" | (string & {});
|
|
1603
1605
|
export interface AssetCoordinate {
|
|
@@ -1653,7 +1655,7 @@ export interface ProjectManagerData {
|
|
|
1653
1655
|
namespaces: Record<ProjectNamespaceKey, ProjectNamespace>;
|
|
1654
1656
|
entities: {
|
|
1655
1657
|
origin: ProjectEntity<OriginData>;
|
|
1656
|
-
|
|
1658
|
+
crs: ProjectEntity<CrsData>;
|
|
1657
1659
|
assetCoordinates: ProjectEntity<AssetCoordinate[]>;
|
|
1658
1660
|
graphicsSettings: ProjectEntity<GraphicsSettingsData>;
|
|
1659
1661
|
};
|
|
@@ -1663,19 +1665,15 @@ export interface SingletonEntity<T> {
|
|
|
1663
1665
|
set(data: T): Promise<void>;
|
|
1664
1666
|
update(partial: Partial<T>): Promise<void>;
|
|
1665
1667
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
* isn't in `sites` — this is not an upsert; use `createSite` to add one. */
|
|
1676
|
-
setSite(siteId: string, site: BimSite): Promise<void>;
|
|
1677
|
-
setDefaultSite(siteId: string): Promise<void>;
|
|
1678
|
-
removeSite(siteId: string): Promise<void>;
|
|
1668
|
+
/**
|
|
1669
|
+
* A singleton entity that also announces its own changes — for values other
|
|
1670
|
+
* code needs to react to live (recompute/reposition something) rather than
|
|
1671
|
+
* just read on demand. `onChange` fires with the new value on every
|
|
1672
|
+
* `set`/`update`, regardless of whether that change has been persisted yet
|
|
1673
|
+
* (see `ProjectManager.dirty`/`onDirtyChange`).
|
|
1674
|
+
*/
|
|
1675
|
+
export interface ReactiveSingletonEntity<T> extends SingletonEntity<T> {
|
|
1676
|
+
onChange: import('@thatopen/components').Event<T>;
|
|
1679
1677
|
}
|
|
1680
1678
|
export interface AssetCoordinatesEntity {
|
|
1681
1679
|
getAll(): AssetCoordinate[];
|
|
@@ -1698,18 +1696,10 @@ export interface AssetCoordinatesEntity {
|
|
|
1698
1696
|
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1699
1697
|
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1700
1698
|
* closed set this code defines, never created at runtime.
|
|
1701
|
-
* - `
|
|
1702
|
-
*
|
|
1703
|
-
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1704
|
-
* are collections.
|
|
1699
|
+
* - `origin`/`crs`/`graphicsSettings` are singletons; `assetCoordinates` is
|
|
1700
|
+
* a collection.
|
|
1705
1701
|
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1706
1702
|
* optional `description` (omitted, not stored empty), free-form `data`.
|
|
1707
|
-
*
|
|
1708
|
-
* Id generation rule: whenever a collection needs an opaque id (today just
|
|
1709
|
-
* `sites`, potentially more collections later), `ProjectManager` generates it
|
|
1710
|
-
* internally (`crypto.randomUUID()`, no prefix) — callers never invent their
|
|
1711
|
-
* own. `createSite` is the current example; follow the same pattern for any
|
|
1712
|
-
* future `create*` method.
|
|
1713
1703
|
*/
|
|
1714
1704
|
declare class _ProjectManager extends OBC.Component {
|
|
1715
1705
|
static readonly uuid: "6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f";
|
|
@@ -1720,6 +1710,31 @@ declare class _ProjectManager extends OBC.Component {
|
|
|
1720
1710
|
private _loadingData;
|
|
1721
1711
|
readonly onSaveStart: OBC.Event<void>;
|
|
1722
1712
|
readonly onSaveComplete: OBC.Event<boolean>;
|
|
1713
|
+
/**
|
|
1714
|
+
* Entity ids (e.g. `"origin"`) with changes not yet persisted. Most entities
|
|
1715
|
+
* auto-save (see `_scheduleSave`) so they're only ever dirty for the instant
|
|
1716
|
+
* before their debounce fires — `origin` is the exception: its `set`/`update`
|
|
1717
|
+
* mark it dirty WITHOUT scheduling a save (see `origin` below), so a caller
|
|
1718
|
+
* driving it continuously (e.g. a drag gizmo) can update it every frame
|
|
1719
|
+
* without spamming the network, and a UI can surface "unsaved changes" +
|
|
1720
|
+
* call `flush()` once the user explicitly confirms.
|
|
1721
|
+
*
|
|
1722
|
+
* KNOWN GAP: `_doSave()` persists the whole `_data` blob (one JSON file for
|
|
1723
|
+
* every entity), so an auto-saving entity's save will also flush out
|
|
1724
|
+
* whatever `origin` currently holds in memory, even if the user hasn't
|
|
1725
|
+
* confirmed it yet — there's no separate "committed vs. draft" copy of
|
|
1726
|
+
* `origin`'s data. Accepted for now as a v1 gap (same spirit as this
|
|
1727
|
+
* package's other documented v1 gaps, see the ProjectManager ADR); revisit
|
|
1728
|
+
* if that coincidence turns out to matter in practice.
|
|
1729
|
+
*/
|
|
1730
|
+
private _dirtyEntities;
|
|
1731
|
+
/** True while any entity has unsaved changes. */
|
|
1732
|
+
get dirty(): boolean;
|
|
1733
|
+
/** Ids of entities with unsaved changes right now. */
|
|
1734
|
+
get dirtyEntities(): string[];
|
|
1735
|
+
/** Fires with the current `dirtyEntities` list every time it changes. */
|
|
1736
|
+
readonly onDirtyChange: OBC.Event<string[]>;
|
|
1737
|
+
private _markDirty;
|
|
1723
1738
|
private _client?;
|
|
1724
1739
|
private _fileId?;
|
|
1725
1740
|
private _data;
|
|
@@ -1728,15 +1743,26 @@ declare class _ProjectManager extends OBC.Component {
|
|
|
1728
1743
|
private _scheduleSave;
|
|
1729
1744
|
private _queueSave;
|
|
1730
1745
|
private _doSave;
|
|
1731
|
-
/**
|
|
1746
|
+
/**
|
|
1747
|
+
* Cancels the pending debounce (if any) and runs the save immediately.
|
|
1748
|
+
* Also the only way to persist an entity that marks itself dirty WITHOUT
|
|
1749
|
+
* auto-scheduling (currently just `origin`) — call this once the user
|
|
1750
|
+
* explicitly confirms. Awaits completion.
|
|
1751
|
+
*/
|
|
1732
1752
|
flush(): Promise<void>;
|
|
1733
1753
|
init(client: PlatformClient): Promise<void>;
|
|
1734
|
-
/**
|
|
1735
|
-
*
|
|
1736
|
-
|
|
1737
|
-
|
|
1754
|
+
/**
|
|
1755
|
+
* NOT auto-saving, unlike every other entity below — `set`/`update` mark it
|
|
1756
|
+
* dirty and fire `onChange` immediately, but leave persisting up to an
|
|
1757
|
+
* explicit `flush()` call. This is what lets a continuous interaction (e.g.
|
|
1758
|
+
* a drag gizmo) update `origin` every frame — live for anyone listening to
|
|
1759
|
+
* `onChange` — without writing to the network on every tick, while still
|
|
1760
|
+
* surfacing "there's an unconfirmed change" via `dirty`/`onDirtyChange` for
|
|
1761
|
+
* a UI to gate behind an explicit save action.
|
|
1762
|
+
*/
|
|
1763
|
+
origin: ReactiveSingletonEntity<OriginData>;
|
|
1764
|
+
crs: ReactiveSingletonEntity<CrsData>;
|
|
1738
1765
|
graphicsSettings: SingletonEntity<GraphicsSettingsData>;
|
|
1739
|
-
bimCoordinates: BimCoordinatesEntity;
|
|
1740
1766
|
assetCoordinates: AssetCoordinatesEntity;
|
|
1741
1767
|
getNamespaces(): Record<ProjectNamespaceKey, ProjectNamespace>;
|
|
1742
1768
|
getNamespace(key: ProjectNamespaceKey): Record<string, unknown>;
|
|
@@ -1755,18 +1781,10 @@ declare class _ProjectManager extends OBC.Component {
|
|
|
1755
1781
|
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1756
1782
|
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1757
1783
|
* closed set this code defines, never created at runtime.
|
|
1758
|
-
* - `
|
|
1759
|
-
*
|
|
1760
|
-
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1761
|
-
* are collections.
|
|
1784
|
+
* - `origin`/`crs`/`graphicsSettings` are singletons; `assetCoordinates` is
|
|
1785
|
+
* a collection.
|
|
1762
1786
|
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1763
1787
|
* optional `description` (omitted, not stored empty), free-form `data`.
|
|
1764
|
-
*
|
|
1765
|
-
* Id generation rule: whenever a collection needs an opaque id (today just
|
|
1766
|
-
* `sites`, potentially more collections later), `ProjectManager` generates it
|
|
1767
|
-
* internally (`crypto.randomUUID()`, no prefix) — callers never invent their
|
|
1768
|
-
* own. `createSite` is the current example; follow the same pattern for any
|
|
1769
|
-
* future `create*` method.
|
|
1770
1788
|
*/
|
|
1771
1789
|
export type ProjectManager = InstanceType<typeof _ProjectManager>;
|
|
1772
1790
|
/**
|
|
@@ -1783,18 +1801,10 @@ export type ProjectManager = InstanceType<typeof _ProjectManager>;
|
|
|
1783
1801
|
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1784
1802
|
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1785
1803
|
* closed set this code defines, never created at runtime.
|
|
1786
|
-
* - `
|
|
1787
|
-
*
|
|
1788
|
-
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1789
|
-
* are collections.
|
|
1804
|
+
* - `origin`/`crs`/`graphicsSettings` are singletons; `assetCoordinates` is
|
|
1805
|
+
* a collection.
|
|
1790
1806
|
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1791
1807
|
* optional `description` (omitted, not stored empty), free-form `data`.
|
|
1792
|
-
*
|
|
1793
|
-
* Id generation rule: whenever a collection needs an opaque id (today just
|
|
1794
|
-
* `sites`, potentially more collections later), `ProjectManager` generates it
|
|
1795
|
-
* internally (`crypto.randomUUID()`, no prefix) — callers never invent their
|
|
1796
|
-
* own. `createSite` is the current example; follow the same pattern for any
|
|
1797
|
-
* future `create*` method.
|
|
1798
1808
|
*/
|
|
1799
1809
|
export declare const ProjectManager: typeof _ProjectManager & {
|
|
1800
1810
|
uuid: "6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f";
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
/** Mirrors the platform-supplied `executionContext` global (see cloud-component template). */
|
|
2
|
+
export interface EngineExecutionContext {
|
|
3
|
+
projectId?: string;
|
|
4
|
+
executionId: string;
|
|
5
|
+
toolId: string;
|
|
6
|
+
toolVersion: string;
|
|
7
|
+
}
|
|
1
8
|
/**
|
|
2
9
|
* Generates the local execution engine script.
|
|
3
10
|
*
|
|
4
11
|
* Mirrors the server-side `v1/thatOpenEngine.js` template:
|
|
5
12
|
* - Provides the same globals (OBC, THREE, WEBIFC, fs, executionReporter,
|
|
6
|
-
* thatOpenServices, executionParams).
|
|
13
|
+
* thatOpenServices, executionParams, executionContext).
|
|
7
14
|
* - Inlines the IIFE bundle code (which ends with `var main = ...`).
|
|
8
15
|
* - Calls `main()` and sends the result back via IPC.
|
|
9
16
|
*/
|
|
10
|
-
export declare function buildEngineScript(bundleCode: string, accessToken: string, apiUrl: string, executionParams: object): string;
|
|
17
|
+
export declare function buildEngineScript(bundleCode: string, accessToken: string, apiUrl: string, executionParams: object, executionContext: EngineExecutionContext): string;
|
package/dist/cli.js
CHANGED
|
@@ -11246,7 +11246,7 @@ function detectGlobalName(cwd) {
|
|
|
11246
11246
|
}
|
|
11247
11247
|
return "ThatOpenApp";
|
|
11248
11248
|
}
|
|
11249
|
-
function buildEngineScript(bundleCode, accessToken, apiUrl, executionParams) {
|
|
11249
|
+
function buildEngineScript(bundleCode, accessToken, apiUrl, executionParams, executionContext) {
|
|
11250
11250
|
return `/* eslint-disable */
|
|
11251
11251
|
const { EngineServicesClient } = require('@thatopen/services');
|
|
11252
11252
|
|
|
@@ -11271,6 +11271,7 @@ const executionReporter = {
|
|
|
11271
11271
|
};
|
|
11272
11272
|
|
|
11273
11273
|
let thatOpenServices, executionParams;
|
|
11274
|
+
const executionContext = ${JSON.stringify(executionContext)};
|
|
11274
11275
|
|
|
11275
11276
|
// --- User bundle code (IIFE) ---
|
|
11276
11277
|
${bundleCode}
|
|
@@ -11356,13 +11357,20 @@ const runCommand = new Command2("run").description("Build and run a cloud compon
|
|
|
11356
11357
|
process.exit(1);
|
|
11357
11358
|
}
|
|
11358
11359
|
const bundleCode = require$$3.readFileSync(bundlePath, "utf-8");
|
|
11360
|
+
const executionId = node_crypto.randomUUID();
|
|
11361
|
+
const localConfig = readLocalConfig(cwd);
|
|
11359
11362
|
const engineScript = buildEngineScript(
|
|
11360
11363
|
bundleCode,
|
|
11361
11364
|
config.accessToken,
|
|
11362
11365
|
config.apiUrl,
|
|
11363
|
-
executionParams
|
|
11366
|
+
executionParams,
|
|
11367
|
+
{
|
|
11368
|
+
executionId,
|
|
11369
|
+
toolId: localConfig?.componentId ?? "local",
|
|
11370
|
+
toolVersion: "local"
|
|
11371
|
+
}
|
|
11364
11372
|
);
|
|
11365
|
-
const tmpFile = require$$2.join(node_os.tmpdir(), `thatopen-run-${
|
|
11373
|
+
const tmpFile = require$$2.join(node_os.tmpdir(), `thatopen-run-${executionId}.js`);
|
|
11366
11374
|
require$$3.writeFileSync(tmpFile, engineScript);
|
|
11367
11375
|
console.log("Running cloud component...\n");
|
|
11368
11376
|
const startTime = Date.now();
|
|
@@ -11478,7 +11486,12 @@ class ExecutionManager {
|
|
|
11478
11486
|
bundleCode,
|
|
11479
11487
|
config.accessToken,
|
|
11480
11488
|
config.apiUrl,
|
|
11481
|
-
executionParams
|
|
11489
|
+
executionParams,
|
|
11490
|
+
{
|
|
11491
|
+
executionId,
|
|
11492
|
+
toolId: componentId,
|
|
11493
|
+
toolVersion: "local"
|
|
11494
|
+
}
|
|
11482
11495
|
);
|
|
11483
11496
|
const tmpFile = require$$2.join(node_os.tmpdir(), `thatopen-local-${executionId}.js`);
|
|
11484
11497
|
require$$3.writeFileSync(tmpFile, engineScript);
|
package/docs/builtin/paths.json
CHANGED
|
@@ -95,6 +95,10 @@
|
|
|
95
95
|
"path": "src/built-in/UIManager/src/models-list/example.ts",
|
|
96
96
|
"description": "top-models-list — top-models-list is the project's 3D-viewable-files tree: it mirrors the real storage folder structure, converts IFC to fragments, and loads `.frag`/`.potree`/`.splat` models into the 3D scene. Its loaders are PLUGGABLE — the panel owns the UI and the orchestration (progress, refresh, association), but the HOST decides how each extra file format is loaded, so you can teach it new formats without forking the built-in."
|
|
97
97
|
},
|
|
98
|
+
{
|
|
99
|
+
"path": "src/built-in/UIManager/src/settings-panel/example.ts",
|
|
100
|
+
"description": "top-settings-panel — Every viewer app ends up needing the same settings screen: rendering options, clip/measurement styling, a \"which cloud component converts this format\" picker, a keyboard-shortcut reference — plus a search box that filters all of it and a \"restore defaults\" button that resets whatever has persisted state. Building that, and keeping it in sync with the tools it configures, is real work before you've added a single setting of your own."
|
|
101
|
+
},
|
|
98
102
|
{
|
|
99
103
|
"path": "src/built-in/UIManager/src/topics-panel/example.ts",
|
|
100
104
|
"description": "topics-panel — Every coordination workflow needs a place to track issues, clashes, and action items against a 3D model — who's responsible, what still needs review, and where in the model it actually is. Building that from scratch means a BCF-compatible data model, a form, comment threads, viewpoint capture/navigation, document links, and a persistence layer — all before a single issue gets tracked."
|
package/package.json
CHANGED
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
per-builtin data.
|
|
15
15
|
|
|
16
16
|
This tutorial covers initializing the manager from the platform client; the
|
|
17
|
-
`origin` and `graphicsSettings` singletons; the `
|
|
18
|
-
collection used for
|
|
19
|
-
|
|
20
|
-
read-only namespace introspection API.
|
|
17
|
+
`origin` and `graphicsSettings` singletons; the `assetCoordinates`
|
|
18
|
+
collection used for splats and point clouds; auto-save and its events; and
|
|
19
|
+
the read-only namespace introspection API.
|
|
21
20
|
|
|
22
21
|
ProjectManager has no built-in UI panel — it's a pure data layer. Apps that
|
|
23
22
|
need georeferencing or project-settings UI (like Aqualia's viewer) build it
|
|
@@ -76,44 +75,6 @@ await manager.flush();
|
|
|
76
75
|
|
|
77
76
|
console.log("origin after update+flush:", manager.origin.get());
|
|
78
77
|
|
|
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
78
|
/* MD
|
|
118
79
|
### 🖼️ assetCoordinates — the files collection
|
|
119
80
|
`assetCoordinates` georeferences individual files — splats, point clouds —
|
|
@@ -159,5 +120,5 @@ console.log("georeferencing namespace:", manager.getNamespace("georeferencing"))
|
|
|
159
120
|
### 🤖 What's not covered here
|
|
160
121
|
ProjectManager ships without a UI panel — everything above is the data
|
|
161
122
|
layer only. Building georeferencing or project-settings UI on top of
|
|
162
|
-
`origin`/`
|
|
123
|
+
`origin`/`crs`/`assetCoordinates` is up to the consuming app.
|
|
163
124
|
*/
|
|
@@ -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
|
+
*/
|
package/src/built-in/index.ts
CHANGED
|
@@ -1594,19 +1594,21 @@ export interface OriginData {
|
|
|
1594
1594
|
height: number;
|
|
1595
1595
|
rotation: number;
|
|
1596
1596
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1597
|
+
/** Project-wide coordinate reference system + vertical datum config, shared by
|
|
1598
|
+
* every asset type that needs to decode raw projected coordinates (point
|
|
1599
|
+
* clouds, BIM) — see automatic-file-derived-georeferencing.md
|
|
1600
|
+
* (VisorUnificadoAqualia/docs/adrs). One value per project, not per asset:
|
|
1601
|
+
* every file at the same real-world site shares the same CRS/geoid. */
|
|
1602
|
+
export interface CrsData {
|
|
1603
|
+
/** EPSG code of the project's projected CRS (e.g. 25830 for ETRS89/UTM 30N).
|
|
1604
|
+
* EPSG is a global registry, not region-specific — every project has an
|
|
1605
|
+
* applicable code, most commonly a standard UTM zone. */
|
|
1606
|
+
epsgCode: number;
|
|
1607
|
+
/** Geoid undulation in meters at the project's origin (orthometric ->
|
|
1608
|
+
* ellipsoidal height correction). Sampled once from a geoid model
|
|
1609
|
+
* (client-supplied, or a global EGM2008 fallback) — varies negligibly
|
|
1610
|
+
* across a single project's extent, so one scalar per project suffices. */
|
|
1611
|
+
geoidUndulation: number;
|
|
1610
1612
|
}
|
|
1611
1613
|
export type AssetCoordinateType = "splat" | "ifc" | "point-cloud" | (string & {});
|
|
1612
1614
|
export interface AssetCoordinate {
|
|
@@ -1662,7 +1664,7 @@ export interface ProjectManagerData {
|
|
|
1662
1664
|
namespaces: Record<ProjectNamespaceKey, ProjectNamespace>;
|
|
1663
1665
|
entities: {
|
|
1664
1666
|
origin: ProjectEntity<OriginData>;
|
|
1665
|
-
|
|
1667
|
+
crs: ProjectEntity<CrsData>;
|
|
1666
1668
|
assetCoordinates: ProjectEntity<AssetCoordinate[]>;
|
|
1667
1669
|
graphicsSettings: ProjectEntity<GraphicsSettingsData>;
|
|
1668
1670
|
};
|
|
@@ -1672,19 +1674,15 @@ export interface SingletonEntity<T> {
|
|
|
1672
1674
|
set(data: T): Promise<void>;
|
|
1673
1675
|
update(partial: Partial<T>): Promise<void>;
|
|
1674
1676
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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>;
|
|
1677
|
+
/**
|
|
1678
|
+
* A singleton entity that also announces its own changes — for values other
|
|
1679
|
+
* code needs to react to live (recompute/reposition something) rather than
|
|
1680
|
+
* just read on demand. `onChange` fires with the new value on every
|
|
1681
|
+
* `set`/`update`, regardless of whether that change has been persisted yet
|
|
1682
|
+
* (see `ProjectManager.dirty`/`onDirtyChange`).
|
|
1683
|
+
*/
|
|
1684
|
+
export interface ReactiveSingletonEntity<T> extends SingletonEntity<T> {
|
|
1685
|
+
onChange: import("@thatopen/components").Event<T>;
|
|
1688
1686
|
}
|
|
1689
1687
|
export interface AssetCoordinatesEntity {
|
|
1690
1688
|
getAll(): AssetCoordinate[];
|
|
@@ -1708,18 +1706,10 @@ export interface AssetCoordinatesEntity {
|
|
|
1708
1706
|
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1709
1707
|
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1710
1708
|
* closed set this code defines, never created at runtime.
|
|
1711
|
-
* - `
|
|
1712
|
-
*
|
|
1713
|
-
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1714
|
-
* are collections.
|
|
1709
|
+
* - `origin`/`crs`/`graphicsSettings` are singletons; `assetCoordinates` is
|
|
1710
|
+
* a collection.
|
|
1715
1711
|
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1716
1712
|
* 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
1713
|
*/
|
|
1724
1714
|
declare class _ProjectManager extends OBC.Component {
|
|
1725
1715
|
static readonly uuid: "6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f";
|
|
@@ -1730,6 +1720,31 @@ declare class _ProjectManager extends OBC.Component {
|
|
|
1730
1720
|
private _loadingData;
|
|
1731
1721
|
readonly onSaveStart: OBC.Event<void>;
|
|
1732
1722
|
readonly onSaveComplete: OBC.Event<boolean>;
|
|
1723
|
+
/**
|
|
1724
|
+
* Entity ids (e.g. `"origin"`) with changes not yet persisted. Most entities
|
|
1725
|
+
* auto-save (see `_scheduleSave`) so they're only ever dirty for the instant
|
|
1726
|
+
* before their debounce fires — `origin` is the exception: its `set`/`update`
|
|
1727
|
+
* mark it dirty WITHOUT scheduling a save (see `origin` below), so a caller
|
|
1728
|
+
* driving it continuously (e.g. a drag gizmo) can update it every frame
|
|
1729
|
+
* without spamming the network, and a UI can surface "unsaved changes" +
|
|
1730
|
+
* call `flush()` once the user explicitly confirms.
|
|
1731
|
+
*
|
|
1732
|
+
* KNOWN GAP: `_doSave()` persists the whole `_data` blob (one JSON file for
|
|
1733
|
+
* every entity), so an auto-saving entity's save will also flush out
|
|
1734
|
+
* whatever `origin` currently holds in memory, even if the user hasn't
|
|
1735
|
+
* confirmed it yet — there's no separate "committed vs. draft" copy of
|
|
1736
|
+
* `origin`'s data. Accepted for now as a v1 gap (same spirit as this
|
|
1737
|
+
* package's other documented v1 gaps, see the ProjectManager ADR); revisit
|
|
1738
|
+
* if that coincidence turns out to matter in practice.
|
|
1739
|
+
*/
|
|
1740
|
+
private _dirtyEntities;
|
|
1741
|
+
/** True while any entity has unsaved changes. */
|
|
1742
|
+
get dirty(): boolean;
|
|
1743
|
+
/** Ids of entities with unsaved changes right now. */
|
|
1744
|
+
get dirtyEntities(): string[];
|
|
1745
|
+
/** Fires with the current `dirtyEntities` list every time it changes. */
|
|
1746
|
+
readonly onDirtyChange: OBC.Event<string[]>;
|
|
1747
|
+
private _markDirty;
|
|
1733
1748
|
private _client?;
|
|
1734
1749
|
private _fileId?;
|
|
1735
1750
|
private _data;
|
|
@@ -1738,15 +1753,26 @@ declare class _ProjectManager extends OBC.Component {
|
|
|
1738
1753
|
private _scheduleSave;
|
|
1739
1754
|
private _queueSave;
|
|
1740
1755
|
private _doSave;
|
|
1741
|
-
/**
|
|
1756
|
+
/**
|
|
1757
|
+
* Cancels the pending debounce (if any) and runs the save immediately.
|
|
1758
|
+
* Also the only way to persist an entity that marks itself dirty WITHOUT
|
|
1759
|
+
* auto-scheduling (currently just `origin`) — call this once the user
|
|
1760
|
+
* explicitly confirms. Awaits completion.
|
|
1761
|
+
*/
|
|
1742
1762
|
flush(): Promise<void>;
|
|
1743
1763
|
init(client: PlatformClient): Promise<void>;
|
|
1744
|
-
/**
|
|
1745
|
-
*
|
|
1746
|
-
|
|
1747
|
-
|
|
1764
|
+
/**
|
|
1765
|
+
* NOT auto-saving, unlike every other entity below — `set`/`update` mark it
|
|
1766
|
+
* dirty and fire `onChange` immediately, but leave persisting up to an
|
|
1767
|
+
* explicit `flush()` call. This is what lets a continuous interaction (e.g.
|
|
1768
|
+
* a drag gizmo) update `origin` every frame — live for anyone listening to
|
|
1769
|
+
* `onChange` — without writing to the network on every tick, while still
|
|
1770
|
+
* surfacing "there's an unconfirmed change" via `dirty`/`onDirtyChange` for
|
|
1771
|
+
* a UI to gate behind an explicit save action.
|
|
1772
|
+
*/
|
|
1773
|
+
origin: ReactiveSingletonEntity<OriginData>;
|
|
1774
|
+
crs: ReactiveSingletonEntity<CrsData>;
|
|
1748
1775
|
graphicsSettings: SingletonEntity<GraphicsSettingsData>;
|
|
1749
|
-
bimCoordinates: BimCoordinatesEntity;
|
|
1750
1776
|
assetCoordinates: AssetCoordinatesEntity;
|
|
1751
1777
|
getNamespaces(): Record<ProjectNamespaceKey, ProjectNamespace>;
|
|
1752
1778
|
getNamespace(key: ProjectNamespaceKey): Record<string, unknown>;
|
|
@@ -1766,18 +1792,10 @@ declare class _ProjectManager extends OBC.Component {
|
|
|
1766
1792
|
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1767
1793
|
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1768
1794
|
* closed set this code defines, never created at runtime.
|
|
1769
|
-
* - `
|
|
1770
|
-
*
|
|
1771
|
-
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1772
|
-
* are collections.
|
|
1795
|
+
* - `origin`/`crs`/`graphicsSettings` are singletons; `assetCoordinates` is
|
|
1796
|
+
* a collection.
|
|
1773
1797
|
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1774
1798
|
* 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
1799
|
*/
|
|
1782
1800
|
export type ProjectManager = InstanceType<typeof _ProjectManager>;
|
|
1783
1801
|
/**
|
|
@@ -1794,18 +1812,10 @@ export type ProjectManager = InstanceType<typeof _ProjectManager>;
|
|
|
1794
1812
|
* Schema shape (see `src/types.ts` for the full rationale):
|
|
1795
1813
|
* - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
|
|
1796
1814
|
* closed set this code defines, never created at runtime.
|
|
1797
|
-
* - `
|
|
1798
|
-
*
|
|
1799
|
-
* - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
|
|
1800
|
-
* are collections.
|
|
1815
|
+
* - `origin`/`crs`/`graphicsSettings` are singletons; `assetCoordinates` is
|
|
1816
|
+
* a collection.
|
|
1801
1817
|
* - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
|
|
1802
1818
|
* 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
1819
|
*/
|
|
1810
1820
|
export const ProjectManager = { uuid: '6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f' } as typeof _ProjectManager & { uuid: '6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f' };
|
|
1811
1821
|
|