@thatopen/services 0.3.16 → 0.5.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/README.md +62 -62
- package/dist/cli/commands/rhino.d.ts +11 -0
- package/dist/cli/lib/plugin-install.d.ts +24 -0
- package/dist/cli.js +124 -1
- package/dist/core/client.d.ts +1 -0
- package/dist/core/platform-client.d.ts +7 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +382 -370
- package/dist/vite-env.d.ts +1 -1
- package/docs/ai-quickstart.md +130 -130
- package/docs/cli/create.md +29 -29
- package/docs/cli/local-server.md +38 -38
- package/docs/cli/login.md +32 -32
- package/docs/cli/publish.md +31 -31
- package/docs/cli/run.md +26 -26
- package/docs/cli/serve.md +43 -43
- package/docs/cli/swap.md +30 -30
- package/docs/cloud/GaussianSplatConverter.md +43 -43
- package/docs/cloud/PointCloudConverter.md +53 -53
- package/docs/revit-collab-quickstart.md +256 -256
- package/package.json +95 -95
- package/resources/AGENTS.md +40 -40
- package/src/built-in/CDEManager/example.ts +214 -214
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
- package/src/built-in/ClashesManager/example.ts +283 -283
- package/src/built-in/UIManager/src/app/example.ts +97 -97
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
- package/src/cli/templates/app/README.md +217 -217
- package/src/cli/templates/app/index.html +35 -35
- package/src/cli/templates/app/package.json +32 -32
- package/src/cli/templates/app/src/app.ts +37 -37
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -50
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
- package/src/cli/templates/app/src/bim-components/index.ts +1 -1
- package/src/cli/templates/app/src/globals.ts +1 -1
- package/src/cli/templates/app/src/main.ts +199 -199
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -88
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -166
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -31
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -51
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -1383
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
- package/src/cli/templates/app/src/ui-components/index.ts +2 -2
- package/src/cli/templates/app/tsconfig.json +16 -16
- package/src/cli/templates/app/vite.config.js +57 -57
- package/src/cli/templates/cloud-component/README.md +205 -205
- package/src/cli/templates/cloud-component/_thatopen +5 -5
- package/src/cli/templates/cloud-component/declarations.json +4 -4
- package/src/cli/templates/cloud-component/package.json +21 -21
- package/src/cli/templates/cloud-component/src/main.ts +70 -70
- package/src/cli/templates/cloud-component/tsconfig.json +16 -16
- package/src/cli/templates/cloud-component/vite.config.js +46 -46
- package/src/cli/templates/shared/AGENTS.md +8 -8
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/src/cli/templates/shared/_gitignore +5 -5
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import * as OBC from "@thatopen/components";
|
|
3
|
-
import * as OBF from "@thatopen/components-front";
|
|
4
|
-
import * as FRAGS from "@thatopen/fragments";
|
|
5
|
-
// Inlines the fragments worker so it runs inside the platform's sandboxed iframe.
|
|
6
|
-
import "@thatopen/fragments/inline";
|
|
7
|
-
import * as BUI from "@thatopen/ui";
|
|
8
|
-
import * as MARKERJS from "@markerjs/markerjs3";
|
|
9
|
-
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
10
|
-
import { setAppContext } from "./app";
|
|
11
|
-
|
|
12
|
-
// ─── A2 migration — PHASES 1+2: boot on UIManager + re-dock panels ───────────
|
|
13
|
-
// Juan consolidated the old AppManager (layout) + ViewportsManager (viewport)
|
|
14
|
-
// built-ins into the single UIManager built-in, which ships `top-app` (shell +
|
|
15
|
-
// layout) and `top-viewer` (deferred-PEN viewport). This boots the bim-viewer on
|
|
16
|
-
// that model.
|
|
17
|
-
// Phase 1 — top-app shell hosting one top-viewer + auto-load. [done]
|
|
18
|
-
// Phase 2 — re-dock the side panels (tree/properties/files/data/objects/
|
|
19
|
-
// settings) into top-app's layouts + sidebar. [this file]
|
|
20
|
-
// Phase 3 — the viewer-overlay tools (fps/HUD/gizmo/bottom toolbar/measure +
|
|
21
|
-
// clip handles) that mount over the canvas. [next]
|
|
22
|
-
// The pre-A2 rich main is preserved at `main.rich.ts.bak`.
|
|
23
|
-
async function main() {
|
|
24
|
-
const client = PlatformClient.fromPlatformContext();
|
|
25
|
-
|
|
26
|
-
// Brand accent (purple) — drives layout-selector active state, dividers, etc.
|
|
27
|
-
document.documentElement.style.setProperty("--bim-ui_accent-base", "#6528d7");
|
|
28
|
-
|
|
29
|
-
// The dev `thatopen serve` wrapper HTML doesn't zero the UA body margin (8px),
|
|
30
|
-
// which insets the whole app inside the platform iframe. Kill it here so it's
|
|
31
|
-
// fixed in both dev and production regardless of the host page.
|
|
32
|
-
document.body.style.margin = "0";
|
|
33
|
-
|
|
34
|
-
// UIManager must be in the setup call: it registers the platform web
|
|
35
|
-
// components (top-app, top-viewer, top-viewer-tools, …) before the DOM renders.
|
|
36
|
-
const { components } = await client.setup<OBC.Components>(
|
|
37
|
-
{ OBC, OBF, BUI, THREE, FRAGS, MARKERJS },
|
|
38
|
-
{ uuid: UIManager.uuid },
|
|
39
|
-
);
|
|
40
|
-
components.get(UIManager).init();
|
|
41
|
-
|
|
42
|
-
// One STABLE top-viewer node, returned by reference so re-rendering top-app
|
|
43
|
-
// (when we add the panels below) reuses it instead of disposing/recreating
|
|
44
|
-
// its world. No <top-viewer-tools>: the bim-viewer mounts its own tabbed
|
|
45
|
-
// visibility/inspect toolbar (see below), so the platform default would just
|
|
46
|
-
// duplicate it.
|
|
47
|
-
const viewerEl = document.createElement("top-viewer");
|
|
48
|
-
// Frame the viewport to match the side panels (BUI bim-panel host = 1px border
|
|
49
|
-
// + 0.75rem radius). top-viewer's host is already overflow:hidden, so the
|
|
50
|
-
// radius clips the canvas corners; border-box keeps the 1px inside the grid
|
|
51
|
-
// area. Done here (not in top-viewer) so the CDE's top-file-viewer, which draws
|
|
52
|
-
// its own frame, never double-borders.
|
|
53
|
-
viewerEl.style.border = "1px solid var(--bim-ui_bg-contrast-20)";
|
|
54
|
-
viewerEl.style.borderRadius = "0.75rem";
|
|
55
|
-
viewerEl.style.boxSizing = "border-box";
|
|
56
|
-
|
|
57
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
-
const app = document.createElement("top-app") as any;
|
|
59
|
-
|
|
60
|
-
app.setup = (waitUntil: (p: Promise<void>, label?: string) => void) => {
|
|
61
|
-
waitUntil(
|
|
62
|
-
(async () => {
|
|
63
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
64
|
-
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
65
|
-
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
66
|
-
classicWorker: true,
|
|
67
|
-
});
|
|
68
|
-
})(),
|
|
69
|
-
"Fragments Core",
|
|
70
|
-
);
|
|
71
|
-
return { components, client };
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Mount minimally first (viewer only) so top-viewer creates the world; the
|
|
75
|
-
// panels + their tools are built once that world exists (below).
|
|
76
|
-
app.elements = { viewer: () => BUI.html`${viewerEl}` };
|
|
77
|
-
app.layouts = {
|
|
78
|
-
Main: {
|
|
79
|
-
label: "Main",
|
|
80
|
-
icon: "solar:3d-square-bold",
|
|
81
|
-
template: `"viewer" 1fr / 1fr`,
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
app.layout = "Main";
|
|
85
|
-
|
|
86
|
-
const container = document.getElementById("that-open-app") ?? document.body;
|
|
87
|
-
container.appendChild(app);
|
|
88
|
-
|
|
89
|
-
// Wait for top-viewer's world to exist before building world-dependent panels.
|
|
90
|
-
// The viewer tool suite + dynamic-anchor pivot dot are now baked into
|
|
91
|
-
// <top-viewer> (setupViewerTools), so the app no longer wires them.
|
|
92
|
-
await firstWorld(components.get(OBC.Worlds));
|
|
93
|
-
|
|
94
|
-
// Platform client + project data for the AppManager-shim consumers
|
|
95
|
-
// (CloudRunner, data-table-panel, app-info-section).
|
|
96
|
-
const projectId: string | undefined = client?.context?.projectId;
|
|
97
|
-
let projectData;
|
|
98
|
-
try {
|
|
99
|
-
if (projectId) projectData = await client.getProjectData(projectId);
|
|
100
|
-
} catch {
|
|
101
|
-
/* dev/no-project → consumers degrade gracefully */
|
|
102
|
-
}
|
|
103
|
-
setAppContext(client, projectData);
|
|
104
|
-
|
|
105
|
-
// Pluggable loaders for <top-models-list>. The built-in ships the lightweight
|
|
106
|
-
// defaults (.frag load, IFC→fragments convert); heavy/app-specific loaders are
|
|
107
|
-
// registered here so they stay OUT of the built-in's bundle. The reality-capture
|
|
108
|
-
// .3tz viewer pulls Spark + 3d-tiles-renderer, so it's lazy-imported app-side
|
|
109
|
-
// and plugged in via the loader registry. Alignment persists through the panel's
|
|
110
|
-
// app-data via the loader context (getAlignment/setAlignment).
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
-
let rcViewer: any = null;
|
|
113
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
-
const modelLoaders: Record<string, any> = {
|
|
115
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
-
"3tz": async (fileId: string, ctx: any) => {
|
|
117
|
-
if (!rcViewer) {
|
|
118
|
-
const { realityCaptureViewer } = await import(
|
|
119
|
-
"./setups/reality-capture-viewer"
|
|
120
|
-
);
|
|
121
|
-
rcViewer = realityCaptureViewer(ctx.components, client);
|
|
122
|
-
}
|
|
123
|
-
const saved = ctx.getAlignment(fileId) as number[] | undefined;
|
|
124
|
-
await rcViewer.loadIntoWorld(fileId, {
|
|
125
|
-
keepPostproduction: true,
|
|
126
|
-
transform: saved ? new THREE.Matrix4().fromArray(saved) : undefined,
|
|
127
|
-
onTransformChange: (m: THREE.Matrix4) =>
|
|
128
|
-
ctx.setAlignment(fileId, m.toArray()),
|
|
129
|
-
});
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// Re-dock: the stable viewer + the panels, under the bim-viewer's named layouts
|
|
134
|
-
// with the activity-bar sidebar (Explorer · Assets · Data · Settings). All panels
|
|
135
|
-
// are now built-ins that self-wire from top-app's componentsContext/clientContext
|
|
136
|
-
// — no `components` plumbing in the app.
|
|
137
|
-
app.elements = {
|
|
138
|
-
viewer: () => BUI.html`${viewerEl}`,
|
|
139
|
-
tree: () => BUI.html`<top-model-tree></top-model-tree>`,
|
|
140
|
-
properties: () => BUI.html`<top-properties-panel></top-properties-panel>`,
|
|
141
|
-
files: () =>
|
|
142
|
-
BUI.html`<top-models-list .loaders=${modelLoaders}></top-models-list>`,
|
|
143
|
-
dataTable: () => BUI.html`<top-data-table-panel></top-data-table-panel>`,
|
|
144
|
-
objects: () => BUI.html`<top-objects-panel></top-objects-panel>`,
|
|
145
|
-
settings: () => BUI.html`<top-settings-panel></top-settings-panel>`,
|
|
146
|
-
};
|
|
147
|
-
// No `label` → the sidebar renders icon-only activity-bar buttons (matching
|
|
148
|
-
// the pre-A2 look), background only on the active one.
|
|
149
|
-
app.layouts = {
|
|
150
|
-
Explorer: {
|
|
151
|
-
icon: "mdi:file-tree",
|
|
152
|
-
template: `"tree viewer" 1fr "properties viewer" 1fr / 22rem 1fr`,
|
|
153
|
-
},
|
|
154
|
-
Assets: {
|
|
155
|
-
icon: "mdi:folder-multiple-outline",
|
|
156
|
-
template: `"files viewer" 1fr "objects viewer" 1fr / 22rem 1fr`,
|
|
157
|
-
},
|
|
158
|
-
Data: {
|
|
159
|
-
icon: "mdi:table",
|
|
160
|
-
template: `"dataTable viewer" 1fr / 22rem 1fr`,
|
|
161
|
-
},
|
|
162
|
-
Settings: {
|
|
163
|
-
icon: "mdi:cog",
|
|
164
|
-
template: `"settings viewer" 1fr / 22rem 1fr`,
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
app.layout = "Explorer";
|
|
168
|
-
app.sidebar = true;
|
|
169
|
-
|
|
170
|
-
// ── Viewer toolbar — now a built-in slotted INTO <top-viewer> (it consumes the
|
|
171
|
-
// world + components contexts top-viewer provides). The rich bottom toolbar +
|
|
172
|
-
// active-tool HUD + clip/measure/walkthrough tools all live in
|
|
173
|
-
// <top-viewer-toolbar>; the side panels share the SAME tool singletons via
|
|
174
|
-
// components.get (OBC.Component-wrapped).
|
|
175
|
-
viewerEl.appendChild(document.createElement("top-viewer-toolbar"));
|
|
176
|
-
|
|
177
|
-
// Navigation gizmo is now baked into <top-viewer> (setupViewerTools), so the
|
|
178
|
-
// app no longer mounts it. (Cascade: the rest of the overlay tools follow.)
|
|
179
|
-
|
|
180
|
-
// No auto-load: the viewer opens empty. Users add models from the Assets
|
|
181
|
-
// (files) panel — top-models-list loads the .frag they pick into the world.
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Resolves with the first world once it exists (top-viewer creates it async). */
|
|
185
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
|
-
function firstWorld(worlds: any): Promise<any> {
|
|
187
|
-
const existing = [...worlds.list.values()][0];
|
|
188
|
-
if (existing) return Promise.resolve(existing);
|
|
189
|
-
return new Promise((resolve) => {
|
|
190
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
-
const handler = ({ value }: any) => {
|
|
192
|
-
worlds.list.onItemSet.remove(handler);
|
|
193
|
-
resolve(value);
|
|
194
|
-
};
|
|
195
|
-
worlds.list.onItemSet.add(handler);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
main().catch(console.error);
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import * as OBC from "@thatopen/components";
|
|
3
|
+
import * as OBF from "@thatopen/components-front";
|
|
4
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
5
|
+
// Inlines the fragments worker so it runs inside the platform's sandboxed iframe.
|
|
6
|
+
import "@thatopen/fragments/inline";
|
|
7
|
+
import * as BUI from "@thatopen/ui";
|
|
8
|
+
import * as MARKERJS from "@markerjs/markerjs3";
|
|
9
|
+
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
10
|
+
import { setAppContext } from "./app";
|
|
11
|
+
|
|
12
|
+
// ─── A2 migration — PHASES 1+2: boot on UIManager + re-dock panels ───────────
|
|
13
|
+
// Juan consolidated the old AppManager (layout) + ViewportsManager (viewport)
|
|
14
|
+
// built-ins into the single UIManager built-in, which ships `top-app` (shell +
|
|
15
|
+
// layout) and `top-viewer` (deferred-PEN viewport). This boots the bim-viewer on
|
|
16
|
+
// that model.
|
|
17
|
+
// Phase 1 — top-app shell hosting one top-viewer + auto-load. [done]
|
|
18
|
+
// Phase 2 — re-dock the side panels (tree/properties/files/data/objects/
|
|
19
|
+
// settings) into top-app's layouts + sidebar. [this file]
|
|
20
|
+
// Phase 3 — the viewer-overlay tools (fps/HUD/gizmo/bottom toolbar/measure +
|
|
21
|
+
// clip handles) that mount over the canvas. [next]
|
|
22
|
+
// The pre-A2 rich main is preserved at `main.rich.ts.bak`.
|
|
23
|
+
async function main() {
|
|
24
|
+
const client = PlatformClient.fromPlatformContext();
|
|
25
|
+
|
|
26
|
+
// Brand accent (purple) — drives layout-selector active state, dividers, etc.
|
|
27
|
+
document.documentElement.style.setProperty("--bim-ui_accent-base", "#6528d7");
|
|
28
|
+
|
|
29
|
+
// The dev `thatopen serve` wrapper HTML doesn't zero the UA body margin (8px),
|
|
30
|
+
// which insets the whole app inside the platform iframe. Kill it here so it's
|
|
31
|
+
// fixed in both dev and production regardless of the host page.
|
|
32
|
+
document.body.style.margin = "0";
|
|
33
|
+
|
|
34
|
+
// UIManager must be in the setup call: it registers the platform web
|
|
35
|
+
// components (top-app, top-viewer, top-viewer-tools, …) before the DOM renders.
|
|
36
|
+
const { components } = await client.setup<OBC.Components>(
|
|
37
|
+
{ OBC, OBF, BUI, THREE, FRAGS, MARKERJS },
|
|
38
|
+
{ uuid: UIManager.uuid },
|
|
39
|
+
);
|
|
40
|
+
components.get(UIManager).init();
|
|
41
|
+
|
|
42
|
+
// One STABLE top-viewer node, returned by reference so re-rendering top-app
|
|
43
|
+
// (when we add the panels below) reuses it instead of disposing/recreating
|
|
44
|
+
// its world. No <top-viewer-tools>: the bim-viewer mounts its own tabbed
|
|
45
|
+
// visibility/inspect toolbar (see below), so the platform default would just
|
|
46
|
+
// duplicate it.
|
|
47
|
+
const viewerEl = document.createElement("top-viewer");
|
|
48
|
+
// Frame the viewport to match the side panels (BUI bim-panel host = 1px border
|
|
49
|
+
// + 0.75rem radius). top-viewer's host is already overflow:hidden, so the
|
|
50
|
+
// radius clips the canvas corners; border-box keeps the 1px inside the grid
|
|
51
|
+
// area. Done here (not in top-viewer) so the CDE's top-file-viewer, which draws
|
|
52
|
+
// its own frame, never double-borders.
|
|
53
|
+
viewerEl.style.border = "1px solid var(--bim-ui_bg-contrast-20)";
|
|
54
|
+
viewerEl.style.borderRadius = "0.75rem";
|
|
55
|
+
viewerEl.style.boxSizing = "border-box";
|
|
56
|
+
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
const app = document.createElement("top-app") as any;
|
|
59
|
+
|
|
60
|
+
app.setup = (waitUntil: (p: Promise<void>, label?: string) => void) => {
|
|
61
|
+
waitUntil(
|
|
62
|
+
(async () => {
|
|
63
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
64
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
65
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
66
|
+
classicWorker: true,
|
|
67
|
+
});
|
|
68
|
+
})(),
|
|
69
|
+
"Fragments Core",
|
|
70
|
+
);
|
|
71
|
+
return { components, client };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Mount minimally first (viewer only) so top-viewer creates the world; the
|
|
75
|
+
// panels + their tools are built once that world exists (below).
|
|
76
|
+
app.elements = { viewer: () => BUI.html`${viewerEl}` };
|
|
77
|
+
app.layouts = {
|
|
78
|
+
Main: {
|
|
79
|
+
label: "Main",
|
|
80
|
+
icon: "solar:3d-square-bold",
|
|
81
|
+
template: `"viewer" 1fr / 1fr`,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
app.layout = "Main";
|
|
85
|
+
|
|
86
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
87
|
+
container.appendChild(app);
|
|
88
|
+
|
|
89
|
+
// Wait for top-viewer's world to exist before building world-dependent panels.
|
|
90
|
+
// The viewer tool suite + dynamic-anchor pivot dot are now baked into
|
|
91
|
+
// <top-viewer> (setupViewerTools), so the app no longer wires them.
|
|
92
|
+
await firstWorld(components.get(OBC.Worlds));
|
|
93
|
+
|
|
94
|
+
// Platform client + project data for the AppManager-shim consumers
|
|
95
|
+
// (CloudRunner, data-table-panel, app-info-section).
|
|
96
|
+
const projectId: string | undefined = client?.context?.projectId;
|
|
97
|
+
let projectData;
|
|
98
|
+
try {
|
|
99
|
+
if (projectId) projectData = await client.getProjectData(projectId);
|
|
100
|
+
} catch {
|
|
101
|
+
/* dev/no-project → consumers degrade gracefully */
|
|
102
|
+
}
|
|
103
|
+
setAppContext(client, projectData);
|
|
104
|
+
|
|
105
|
+
// Pluggable loaders for <top-models-list>. The built-in ships the lightweight
|
|
106
|
+
// defaults (.frag load, IFC→fragments convert); heavy/app-specific loaders are
|
|
107
|
+
// registered here so they stay OUT of the built-in's bundle. The reality-capture
|
|
108
|
+
// .3tz viewer pulls Spark + 3d-tiles-renderer, so it's lazy-imported app-side
|
|
109
|
+
// and plugged in via the loader registry. Alignment persists through the panel's
|
|
110
|
+
// app-data via the loader context (getAlignment/setAlignment).
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
+
let rcViewer: any = null;
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
const modelLoaders: Record<string, any> = {
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
"3tz": async (fileId: string, ctx: any) => {
|
|
117
|
+
if (!rcViewer) {
|
|
118
|
+
const { realityCaptureViewer } = await import(
|
|
119
|
+
"./setups/reality-capture-viewer"
|
|
120
|
+
);
|
|
121
|
+
rcViewer = realityCaptureViewer(ctx.components, client);
|
|
122
|
+
}
|
|
123
|
+
const saved = ctx.getAlignment(fileId) as number[] | undefined;
|
|
124
|
+
await rcViewer.loadIntoWorld(fileId, {
|
|
125
|
+
keepPostproduction: true,
|
|
126
|
+
transform: saved ? new THREE.Matrix4().fromArray(saved) : undefined,
|
|
127
|
+
onTransformChange: (m: THREE.Matrix4) =>
|
|
128
|
+
ctx.setAlignment(fileId, m.toArray()),
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Re-dock: the stable viewer + the panels, under the bim-viewer's named layouts
|
|
134
|
+
// with the activity-bar sidebar (Explorer · Assets · Data · Settings). All panels
|
|
135
|
+
// are now built-ins that self-wire from top-app's componentsContext/clientContext
|
|
136
|
+
// — no `components` plumbing in the app.
|
|
137
|
+
app.elements = {
|
|
138
|
+
viewer: () => BUI.html`${viewerEl}`,
|
|
139
|
+
tree: () => BUI.html`<top-model-tree></top-model-tree>`,
|
|
140
|
+
properties: () => BUI.html`<top-properties-panel></top-properties-panel>`,
|
|
141
|
+
files: () =>
|
|
142
|
+
BUI.html`<top-models-list .loaders=${modelLoaders}></top-models-list>`,
|
|
143
|
+
dataTable: () => BUI.html`<top-data-table-panel></top-data-table-panel>`,
|
|
144
|
+
objects: () => BUI.html`<top-objects-panel></top-objects-panel>`,
|
|
145
|
+
settings: () => BUI.html`<top-settings-panel></top-settings-panel>`,
|
|
146
|
+
};
|
|
147
|
+
// No `label` → the sidebar renders icon-only activity-bar buttons (matching
|
|
148
|
+
// the pre-A2 look), background only on the active one.
|
|
149
|
+
app.layouts = {
|
|
150
|
+
Explorer: {
|
|
151
|
+
icon: "mdi:file-tree",
|
|
152
|
+
template: `"tree viewer" 1fr "properties viewer" 1fr / 22rem 1fr`,
|
|
153
|
+
},
|
|
154
|
+
Assets: {
|
|
155
|
+
icon: "mdi:folder-multiple-outline",
|
|
156
|
+
template: `"files viewer" 1fr "objects viewer" 1fr / 22rem 1fr`,
|
|
157
|
+
},
|
|
158
|
+
Data: {
|
|
159
|
+
icon: "mdi:table",
|
|
160
|
+
template: `"dataTable viewer" 1fr / 22rem 1fr`,
|
|
161
|
+
},
|
|
162
|
+
Settings: {
|
|
163
|
+
icon: "mdi:cog",
|
|
164
|
+
template: `"settings viewer" 1fr / 22rem 1fr`,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
app.layout = "Explorer";
|
|
168
|
+
app.sidebar = true;
|
|
169
|
+
|
|
170
|
+
// ── Viewer toolbar — now a built-in slotted INTO <top-viewer> (it consumes the
|
|
171
|
+
// world + components contexts top-viewer provides). The rich bottom toolbar +
|
|
172
|
+
// active-tool HUD + clip/measure/walkthrough tools all live in
|
|
173
|
+
// <top-viewer-toolbar>; the side panels share the SAME tool singletons via
|
|
174
|
+
// components.get (OBC.Component-wrapped).
|
|
175
|
+
viewerEl.appendChild(document.createElement("top-viewer-toolbar"));
|
|
176
|
+
|
|
177
|
+
// Navigation gizmo is now baked into <top-viewer> (setupViewerTools), so the
|
|
178
|
+
// app no longer mounts it. (Cascade: the rest of the overlay tools follow.)
|
|
179
|
+
|
|
180
|
+
// No auto-load: the viewer opens empty. Users add models from the Assets
|
|
181
|
+
// (files) panel — top-models-list loads the .frag they pick into the world.
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Resolves with the first world once it exists (top-viewer creates it async). */
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
|
+
function firstWorld(worlds: any): Promise<any> {
|
|
187
|
+
const existing = [...worlds.list.values()][0];
|
|
188
|
+
if (existing) return Promise.resolve(existing);
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
+
const handler = ({ value }: any) => {
|
|
192
|
+
worlds.list.onItemSet.remove(handler);
|
|
193
|
+
resolve(value);
|
|
194
|
+
};
|
|
195
|
+
worlds.list.onItemSet.add(handler);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main().catch(console.error);
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
// 3DTilesRendererJS plugin: serve a STANDARD loose 3D Tiles dataset whose tiles
|
|
2
|
-
// are stored as platform HIDDEN files, streamed one-file-per-tile.
|
|
3
|
-
//
|
|
4
|
-
// Seam: the renderer's `fetchData(url, options)` hook (intercepts BOTH the root
|
|
5
|
-
// tileset and every tile content fetch). Point the TilesRenderer at
|
|
6
|
-
// `<baseUrl>/tileset.json`; relative content URIs then resolve to
|
|
7
|
-
// `<baseUrl>/<relUri>` (e.g. "mem://t/tiles/n12_d3.spz"), which we strip back to
|
|
8
|
-
// the dataset-relative path (`relUri`) and look up in the `hiddenFiles` map to
|
|
9
|
-
// find the platform hidden-file id to download.
|
|
10
|
-
//
|
|
11
|
-
// SHARED CONTRACT (matches the converters): the main visible file is a standard
|
|
12
|
-
// `tileset.json`; each tile `content.uri` is a relative path under "tiles/".
|
|
13
|
-
// tileset.json carries a top-level `hiddenFiles: { "<content.uri>": "<id>" }`
|
|
14
|
-
// map. We fetch a tile blob with `client.downloadHiddenFile(id)` -> Response.
|
|
15
|
-
//
|
|
16
|
-
// Per-tile streaming = one `downloadHiddenFile` per visible tile; the controller's
|
|
17
|
-
// Q1 LRU/count-budget governor bounds how many are resident/loading at once.
|
|
18
|
-
|
|
19
|
-
// Structural type for the platform client: download a normal file by id (the
|
|
20
|
-
// tileset main file) and a hidden file by id (each tile). PlatformClient /
|
|
21
|
-
// EngineServicesClient satisfy this.
|
|
22
|
-
export interface HiddenTilesClient {
|
|
23
|
-
downloadFile(fileId: string, params?: any): Promise<Response>;
|
|
24
|
-
downloadHiddenFile(hiddenId: string): Promise<Response>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface HiddenTilesOptions {
|
|
28
|
-
baseUrl: string; // synthetic base the TilesRenderer is pointed at, e.g. "mem://t"
|
|
29
|
-
tilesetBytes: Uint8Array; // the already-downloaded tileset.json bytes
|
|
30
|
-
hiddenFiles: Record<string, string>; // content.uri -> hidden file id
|
|
31
|
-
client: HiddenTilesClient;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class HiddenTilesPlugin {
|
|
35
|
-
name = "HIDDEN_TILES_PLUGIN";
|
|
36
|
-
private baseUrl: string;
|
|
37
|
-
private tilesetBytes: Uint8Array;
|
|
38
|
-
private hiddenFiles: Record<string, string>;
|
|
39
|
-
private client: HiddenTilesClient;
|
|
40
|
-
|
|
41
|
-
constructor({ baseUrl, tilesetBytes, hiddenFiles, client }: HiddenTilesOptions) {
|
|
42
|
-
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
43
|
-
this.tilesetBytes = tilesetBytes;
|
|
44
|
-
this.hiddenFiles = hiddenFiles;
|
|
45
|
-
this.client = client;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// dataset-relative path for URLs under our synthetic base, else null.
|
|
49
|
-
private rel(url: string): string | null {
|
|
50
|
-
const prefix = this.baseUrl + "/";
|
|
51
|
-
return url.startsWith(prefix) ? url.slice(prefix.length) : null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// The renderer fetch hook. Return a Response, or null to let the default fetch
|
|
55
|
-
// run (URLs that aren't under our base).
|
|
56
|
-
async fetchData(url: string, _options: any): Promise<Response | null> {
|
|
57
|
-
const rel = this.rel(url);
|
|
58
|
-
if (rel === null) return null;
|
|
59
|
-
|
|
60
|
-
// Root tileset request: serve the bytes we already downloaded.
|
|
61
|
-
if (rel === "tileset.json") {
|
|
62
|
-
// copy out into a standalone ArrayBuffer for the Response
|
|
63
|
-
const b = this.tilesetBytes;
|
|
64
|
-
const ab = (b.byteOffset === 0 && b.byteLength === b.buffer.byteLength
|
|
65
|
-
? b.buffer
|
|
66
|
-
: b.slice().buffer) as ArrayBuffer;
|
|
67
|
-
return new Response(ab, {
|
|
68
|
-
status: 200,
|
|
69
|
-
headers: { "Content-Type": "application/json" },
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Content (tile) request: strip query/fragment, look up the hidden id.
|
|
74
|
-
const relUri = decodeURIComponent(rel.split("?")[0].split("#")[0]);
|
|
75
|
-
const hiddenId = this.hiddenFiles[relUri];
|
|
76
|
-
if (!hiddenId) {
|
|
77
|
-
// not a known tile -> 404 so the renderer treats it as a failed fetch.
|
|
78
|
-
return new Response(null, { status: 404, statusText: "Not Found" });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const res = await this.client.downloadHiddenFile(hiddenId);
|
|
82
|
-
const buf = await res.arrayBuffer();
|
|
83
|
-
return new Response(buf, {
|
|
84
|
-
status: 200,
|
|
85
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
1
|
+
// 3DTilesRendererJS plugin: serve a STANDARD loose 3D Tiles dataset whose tiles
|
|
2
|
+
// are stored as platform HIDDEN files, streamed one-file-per-tile.
|
|
3
|
+
//
|
|
4
|
+
// Seam: the renderer's `fetchData(url, options)` hook (intercepts BOTH the root
|
|
5
|
+
// tileset and every tile content fetch). Point the TilesRenderer at
|
|
6
|
+
// `<baseUrl>/tileset.json`; relative content URIs then resolve to
|
|
7
|
+
// `<baseUrl>/<relUri>` (e.g. "mem://t/tiles/n12_d3.spz"), which we strip back to
|
|
8
|
+
// the dataset-relative path (`relUri`) and look up in the `hiddenFiles` map to
|
|
9
|
+
// find the platform hidden-file id to download.
|
|
10
|
+
//
|
|
11
|
+
// SHARED CONTRACT (matches the converters): the main visible file is a standard
|
|
12
|
+
// `tileset.json`; each tile `content.uri` is a relative path under "tiles/".
|
|
13
|
+
// tileset.json carries a top-level `hiddenFiles: { "<content.uri>": "<id>" }`
|
|
14
|
+
// map. We fetch a tile blob with `client.downloadHiddenFile(id)` -> Response.
|
|
15
|
+
//
|
|
16
|
+
// Per-tile streaming = one `downloadHiddenFile` per visible tile; the controller's
|
|
17
|
+
// Q1 LRU/count-budget governor bounds how many are resident/loading at once.
|
|
18
|
+
|
|
19
|
+
// Structural type for the platform client: download a normal file by id (the
|
|
20
|
+
// tileset main file) and a hidden file by id (each tile). PlatformClient /
|
|
21
|
+
// EngineServicesClient satisfy this.
|
|
22
|
+
export interface HiddenTilesClient {
|
|
23
|
+
downloadFile(fileId: string, params?: any): Promise<Response>;
|
|
24
|
+
downloadHiddenFile(hiddenId: string): Promise<Response>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HiddenTilesOptions {
|
|
28
|
+
baseUrl: string; // synthetic base the TilesRenderer is pointed at, e.g. "mem://t"
|
|
29
|
+
tilesetBytes: Uint8Array; // the already-downloaded tileset.json bytes
|
|
30
|
+
hiddenFiles: Record<string, string>; // content.uri -> hidden file id
|
|
31
|
+
client: HiddenTilesClient;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class HiddenTilesPlugin {
|
|
35
|
+
name = "HIDDEN_TILES_PLUGIN";
|
|
36
|
+
private baseUrl: string;
|
|
37
|
+
private tilesetBytes: Uint8Array;
|
|
38
|
+
private hiddenFiles: Record<string, string>;
|
|
39
|
+
private client: HiddenTilesClient;
|
|
40
|
+
|
|
41
|
+
constructor({ baseUrl, tilesetBytes, hiddenFiles, client }: HiddenTilesOptions) {
|
|
42
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
43
|
+
this.tilesetBytes = tilesetBytes;
|
|
44
|
+
this.hiddenFiles = hiddenFiles;
|
|
45
|
+
this.client = client;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// dataset-relative path for URLs under our synthetic base, else null.
|
|
49
|
+
private rel(url: string): string | null {
|
|
50
|
+
const prefix = this.baseUrl + "/";
|
|
51
|
+
return url.startsWith(prefix) ? url.slice(prefix.length) : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The renderer fetch hook. Return a Response, or null to let the default fetch
|
|
55
|
+
// run (URLs that aren't under our base).
|
|
56
|
+
async fetchData(url: string, _options: any): Promise<Response | null> {
|
|
57
|
+
const rel = this.rel(url);
|
|
58
|
+
if (rel === null) return null;
|
|
59
|
+
|
|
60
|
+
// Root tileset request: serve the bytes we already downloaded.
|
|
61
|
+
if (rel === "tileset.json") {
|
|
62
|
+
// copy out into a standalone ArrayBuffer for the Response
|
|
63
|
+
const b = this.tilesetBytes;
|
|
64
|
+
const ab = (b.byteOffset === 0 && b.byteLength === b.buffer.byteLength
|
|
65
|
+
? b.buffer
|
|
66
|
+
: b.slice().buffer) as ArrayBuffer;
|
|
67
|
+
return new Response(ab, {
|
|
68
|
+
status: 200,
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Content (tile) request: strip query/fragment, look up the hidden id.
|
|
74
|
+
const relUri = decodeURIComponent(rel.split("?")[0].split("#")[0]);
|
|
75
|
+
const hiddenId = this.hiddenFiles[relUri];
|
|
76
|
+
if (!hiddenId) {
|
|
77
|
+
// not a known tile -> 404 so the renderer treats it as a failed fetch.
|
|
78
|
+
return new Response(null, { status: 404, statusText: "Not Found" });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const res = await this.client.downloadHiddenFile(hiddenId);
|
|
82
|
+
const buf = await res.arrayBuffer();
|
|
83
|
+
return new Response(buf, {
|
|
84
|
+
status: 200,
|
|
85
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|