@thatopen/services 0.1.3 → 0.3.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 +59 -46
- package/dist/built-in/index.d.ts +486 -563
- package/dist/cli/lib/npmrc.d.ts +28 -0
- package/dist/cli/lib/npmrc.test.d.ts +1 -0
- package/dist/cli.js +91 -7
- package/dist/core/client.d.ts +10 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +42 -32
- package/dist/types/npm.d.ts +14 -0
- package/dist/vite-env.d.ts +1 -1
- package/docs/builtin/paths.json +74 -70
- package/docs/cli/create.md +28 -28
- 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/package.json +100 -100
- 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/UIManager/src/app/example.ts +99 -99
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
- package/src/built-in/index.ts +649 -755
- package/src/cli/templates/app/README.md +241 -241
- package/src/cli/templates/app/index.html +35 -27
- package/src/cli/templates/app/package.json +31 -27
- package/src/cli/templates/app/src/app.ts +37 -0
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
- 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 -0
- package/src/cli/templates/app/src/main.ts +340 -56
- package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
- package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
- package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
- package/src/cli/templates/app/src/setups/card-header.ts +35 -0
- package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
- package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
- package/src/cli/templates/app/src/setups/clipper.ts +75 -0
- package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
- package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
- package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
- package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
- package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
- package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
- package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
- package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
- package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
- package/src/cli/templates/app/src/setups/hider.ts +98 -0
- package/src/cli/templates/app/src/setups/index.ts +33 -3
- package/src/cli/templates/app/src/setups/inspection.ts +136 -0
- package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
- package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
- package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
- package/src/cli/templates/app/src/setups/measurements.ts +133 -0
- package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
- package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
- package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
- package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
- package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
- package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
- package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
- package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
- package/src/cli/templates/app/src/setups/styles.ts +375 -0
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
- package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
- package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
- package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
- package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
- package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
- package/src/cli/templates/app/src/ui-components/index.ts +2 -1
- package/src/cli/templates/app/tsconfig.json +16 -17
- package/src/cli/templates/app/vite.config.js +45 -42
- 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 -4
- package/src/built-in/AppManager/example.ts +0 -86
- package/src/built-in/FileList/example.ts +0 -107
- package/src/built-in/TabbedNavigation/example.ts +0 -115
- package/src/built-in/ViewportsManager/example.ts +0 -106
- package/src/cli/templates/app/src/setups/fragments.ts +0 -18
- package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
- package/src/cli/templates/app/src/ui-components/AppPanel/index.ts +0 -160
|
@@ -1,56 +1,340 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
import * as
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import "./
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
import {
|
|
12
|
+
propertiesPanel,
|
|
13
|
+
modelTree,
|
|
14
|
+
filesPanel,
|
|
15
|
+
graphicsPanel,
|
|
16
|
+
clipperTool,
|
|
17
|
+
clipperPanel,
|
|
18
|
+
commandsPanel,
|
|
19
|
+
measurementTool,
|
|
20
|
+
dataTablePanel,
|
|
21
|
+
fpsIndicator,
|
|
22
|
+
activeToolHud,
|
|
23
|
+
navigationGizmo,
|
|
24
|
+
visibilityToolbar,
|
|
25
|
+
walkthrough,
|
|
26
|
+
} from "./setups";
|
|
27
|
+
import { objectsPanel } from "./setups/objects-panel";
|
|
28
|
+
import { inspectionInstances, inspectionActions } from "./setups/inspection";
|
|
29
|
+
import { measurementSettingsPanel } from "./setups/measurement-settings-panel";
|
|
30
|
+
import { settingsPanel } from "./setups/settings-panel";
|
|
31
|
+
|
|
32
|
+
// ─── A2 migration — PHASES 1+2: boot on UIManager + re-dock panels ───────────
|
|
33
|
+
// Juan consolidated the old AppManager (layout) + ViewportsManager (viewport)
|
|
34
|
+
// built-ins into the single UIManager built-in, which ships `top-app` (shell +
|
|
35
|
+
// layout) and `top-viewer` (deferred-PEN viewport). This boots the bim-viewer on
|
|
36
|
+
// that model.
|
|
37
|
+
// Phase 1 — top-app shell hosting one top-viewer + auto-load. [done]
|
|
38
|
+
// Phase 2 — re-dock the side panels (tree/properties/files/data/objects/
|
|
39
|
+
// settings) into top-app's layouts + sidebar. [this file]
|
|
40
|
+
// Phase 3 — the viewer-overlay tools (fps/HUD/gizmo/bottom toolbar/measure +
|
|
41
|
+
// clip handles) that mount over the canvas. [next]
|
|
42
|
+
// The pre-A2 rich main is preserved at `main.rich.ts.bak`.
|
|
43
|
+
async function main() {
|
|
44
|
+
const client = PlatformClient.fromPlatformContext();
|
|
45
|
+
|
|
46
|
+
// Brand accent (purple) — drives layout-selector active state, dividers, etc.
|
|
47
|
+
document.documentElement.style.setProperty("--bim-ui_accent-base", "#6528d7");
|
|
48
|
+
|
|
49
|
+
// The dev `thatopen serve` wrapper HTML doesn't zero the UA body margin (8px),
|
|
50
|
+
// which insets the whole app inside the platform iframe. Kill it here so it's
|
|
51
|
+
// fixed in both dev and production regardless of the host page.
|
|
52
|
+
document.body.style.margin = "0";
|
|
53
|
+
|
|
54
|
+
// UIManager must be in the setup call: it registers the platform web
|
|
55
|
+
// components (top-app, top-viewer, top-viewer-tools, …) before the DOM renders.
|
|
56
|
+
const { components } = await client.setup<OBC.Components>(
|
|
57
|
+
{ OBC, OBF, BUI, THREE, FRAGS, MARKERJS },
|
|
58
|
+
{ uuid: UIManager.uuid },
|
|
59
|
+
);
|
|
60
|
+
components.get(UIManager).init();
|
|
61
|
+
|
|
62
|
+
// One STABLE top-viewer node, returned by reference so re-rendering top-app
|
|
63
|
+
// (when we add the panels below) reuses it instead of disposing/recreating
|
|
64
|
+
// its world. No <top-viewer-tools>: the bim-viewer mounts its own tabbed
|
|
65
|
+
// visibility/inspect toolbar (see below), so the platform default would just
|
|
66
|
+
// duplicate it.
|
|
67
|
+
const viewerEl = document.createElement("top-viewer");
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
const app = document.createElement("top-app") as any;
|
|
71
|
+
|
|
72
|
+
app.setup = (waitUntil: (p: Promise<void>, label?: string) => void) => {
|
|
73
|
+
waitUntil(
|
|
74
|
+
(async () => {
|
|
75
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
76
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
77
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
78
|
+
classicWorker: true,
|
|
79
|
+
});
|
|
80
|
+
})(),
|
|
81
|
+
"Fragments Core",
|
|
82
|
+
);
|
|
83
|
+
return { components, client };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Mount minimally first (viewer only) so top-viewer creates the world; the
|
|
87
|
+
// panels + their tools are built once that world exists (below).
|
|
88
|
+
app.elements = { viewer: () => BUI.html`${viewerEl}` };
|
|
89
|
+
app.layouts = {
|
|
90
|
+
Main: {
|
|
91
|
+
label: "Main",
|
|
92
|
+
icon: "solar:3d-square-bold",
|
|
93
|
+
template: `"viewer" 1fr / 1fr`,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
app.layout = "Main";
|
|
97
|
+
|
|
98
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
99
|
+
container.appendChild(app);
|
|
100
|
+
|
|
101
|
+
// Wait for top-viewer's world before building world-dependent tools/panels.
|
|
102
|
+
const world = await firstWorld(components.get(OBC.Worlds));
|
|
103
|
+
// Auto-anchor: the orbit pivot follows the surface picked on left-press
|
|
104
|
+
// (library dynamicAnchor). top-viewer doesn't enable it, so set it here.
|
|
105
|
+
world.dynamicAnchor = true;
|
|
106
|
+
// …and show that pivot as an on-screen dot (projected from 3D each frame).
|
|
107
|
+
setupAnchorDot(world, viewerEl);
|
|
108
|
+
|
|
109
|
+
// Platform client + project data for the AppManager-shim consumers
|
|
110
|
+
// (CloudRunner, data-table-panel, app-info-section).
|
|
111
|
+
const projectId: string | undefined = client?.context?.projectId;
|
|
112
|
+
let projectData;
|
|
113
|
+
try {
|
|
114
|
+
if (projectId) projectData = await client.getProjectData(projectId);
|
|
115
|
+
} catch {
|
|
116
|
+
/* dev/no-project → consumers degrade gracefully */
|
|
117
|
+
}
|
|
118
|
+
setAppContext(client, projectData);
|
|
119
|
+
|
|
120
|
+
// Headless tools — drive the panels; they resolve the world via `components`.
|
|
121
|
+
const clipTool = clipperTool(components);
|
|
122
|
+
const measureTool = measurementTool(components);
|
|
123
|
+
|
|
124
|
+
// Panels.
|
|
125
|
+
const treeEl = modelTree(components);
|
|
126
|
+
const propsEl = propertiesPanel(components);
|
|
127
|
+
const filesEl = filesPanel(components, client) as unknown as HTMLElement;
|
|
128
|
+
const dataTableEl = dataTablePanel(components) as unknown as HTMLElement;
|
|
129
|
+
// FPS counter — mounted into the viewer overlay; the Graphics panel "Show FPS"
|
|
130
|
+
// switch drives it (pass the controller in).
|
|
131
|
+
const fps = fpsIndicator(viewerEl);
|
|
132
|
+
const graphicsEl = graphicsPanel(components, fps) as unknown as HTMLElement;
|
|
133
|
+
const clippingEl = clipperPanel(components, clipTool) as unknown as HTMLElement;
|
|
134
|
+
const commandsEl = commandsPanel(components) as unknown as HTMLElement;
|
|
135
|
+
const measureSettingsEl = measurementSettingsPanel(
|
|
136
|
+
measureTool,
|
|
137
|
+
) as unknown as HTMLElement;
|
|
138
|
+
const inspection = inspectionInstances(clipTool, measureTool);
|
|
139
|
+
const objectsEl = objectsPanel(inspection) as unknown as HTMLElement;
|
|
140
|
+
const settingsEl = settingsPanel([
|
|
141
|
+
{ label: "Graphics", icon: "mdi:tune", el: graphicsEl },
|
|
142
|
+
{ label: "Clip styling", icon: "mdi:scissors-cutting", el: clippingEl },
|
|
143
|
+
{ label: "Measurement", icon: "mdi:ruler", el: measureSettingsEl },
|
|
144
|
+
{ label: "Commands", icon: "mdi:keyboard", el: commandsEl },
|
|
145
|
+
]) as unknown as HTMLElement;
|
|
146
|
+
|
|
147
|
+
for (const el of [
|
|
148
|
+
treeEl,
|
|
149
|
+
propsEl,
|
|
150
|
+
filesEl,
|
|
151
|
+
dataTableEl,
|
|
152
|
+
objectsEl,
|
|
153
|
+
settingsEl,
|
|
154
|
+
] as HTMLElement[]) {
|
|
155
|
+
el.style.width = "100%";
|
|
156
|
+
el.style.height = "100%";
|
|
157
|
+
// top-app's grid areas are overflow:hidden; border-box keeps the panel's own
|
|
158
|
+
// border inside the area so it isn't clipped on the bottom/right edges.
|
|
159
|
+
el.style.boxSizing = "border-box";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Re-dock: same stable viewer + the panels, under the bim-viewer's named
|
|
163
|
+
// layouts with the activity-bar sidebar (Explorer · Assets · Data · Settings).
|
|
164
|
+
const wrap = (el: HTMLElement) => () => BUI.html`${el}`;
|
|
165
|
+
app.elements = {
|
|
166
|
+
viewer: () => BUI.html`${viewerEl}`,
|
|
167
|
+
tree: wrap(treeEl),
|
|
168
|
+
properties: wrap(propsEl),
|
|
169
|
+
files: wrap(filesEl),
|
|
170
|
+
dataTable: wrap(dataTableEl),
|
|
171
|
+
objects: wrap(objectsEl),
|
|
172
|
+
settings: wrap(settingsEl),
|
|
173
|
+
};
|
|
174
|
+
// No `label` → the sidebar renders icon-only activity-bar buttons (matching
|
|
175
|
+
// the pre-A2 look), background only on the active one.
|
|
176
|
+
app.layouts = {
|
|
177
|
+
Explorer: {
|
|
178
|
+
icon: "mdi:file-tree",
|
|
179
|
+
template: `"tree viewer" 1fr "properties viewer" 1fr / 22rem 1fr`,
|
|
180
|
+
},
|
|
181
|
+
Assets: {
|
|
182
|
+
icon: "mdi:folder-multiple-outline",
|
|
183
|
+
template: `"files viewer" 1fr "objects viewer" 1fr / 22rem 1fr`,
|
|
184
|
+
},
|
|
185
|
+
Data: {
|
|
186
|
+
icon: "mdi:table",
|
|
187
|
+
template: `"dataTable viewer" 1fr / 22rem 1fr`,
|
|
188
|
+
},
|
|
189
|
+
Settings: {
|
|
190
|
+
icon: "mdi:cog",
|
|
191
|
+
template: `"settings viewer" 1fr / 22rem 1fr`,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
app.layout = "Explorer";
|
|
195
|
+
app.sidebar = true;
|
|
196
|
+
|
|
197
|
+
// ── Viewer-overlay tools — appended to the top-viewer host, so they slot into
|
|
198
|
+
// top-viewer's pointer-events:none overlay over the canvas (interactive parts
|
|
199
|
+
// opt back into pointer events themselves). ────────────────────────────────
|
|
200
|
+
activeToolHud(viewerEl, components);
|
|
201
|
+
|
|
202
|
+
let walk;
|
|
203
|
+
try {
|
|
204
|
+
walk = walkthrough(components);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.warn("[a2] walkthrough controller failed to init", e);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Floating bottom toolbar (tabbed): "View" = visibility (hide/show/ghost/
|
|
210
|
+
// isolate + projection + walkthrough); "Inspect" = Select / Clip / Measure,
|
|
211
|
+
// routed through the tool-mode manager.
|
|
212
|
+
visibilityToolbar(
|
|
213
|
+
components,
|
|
214
|
+
viewerEl,
|
|
215
|
+
walk,
|
|
216
|
+
undefined,
|
|
217
|
+
inspectionActions(clipTool, measureTool),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Navigation gizmo / view-cube (top-right): live orientation + click-to-orient.
|
|
221
|
+
try {
|
|
222
|
+
navigationGizmo(components, viewerEl);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.warn("[a2] navigationGizmo failed to mount", e);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Auto-load one model (top-viewer's world wires fragments→scene itself).
|
|
228
|
+
void autoLoadFirstModel(components, client);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Resolves with the first world once it exists (top-viewer creates it async). */
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
233
|
+
function firstWorld(worlds: any): Promise<any> {
|
|
234
|
+
const existing = [...worlds.list.values()][0];
|
|
235
|
+
if (existing) return Promise.resolve(existing);
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
+
const handler = ({ value }: any) => {
|
|
239
|
+
worlds.list.onItemSet.remove(handler);
|
|
240
|
+
resolve(value);
|
|
241
|
+
};
|
|
242
|
+
worlds.list.onItemSet.add(handler);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* On-screen pivot dot for the dynamic anchor: an HTML element projected from the
|
|
248
|
+
* 3D anchor point each frame. Appended to the top-viewer host so it slots into
|
|
249
|
+
* the viewer overlay over the canvas. Revealed once a drag starts (so a click-to-
|
|
250
|
+
* select doesn't flash it) and kept glued to the pivot as the camera orbits.
|
|
251
|
+
*/
|
|
252
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
253
|
+
function setupAnchorDot(world: any, element: HTMLElement) {
|
|
254
|
+
const dot = document.createElement("div");
|
|
255
|
+
dot.style.cssText =
|
|
256
|
+
"position:absolute; width:13px; height:13px; border-radius:50%;" +
|
|
257
|
+
"background:#6528d7; transform:translate(-50%,-50%);" +
|
|
258
|
+
"pointer-events:none; display:none; z-index:5;" +
|
|
259
|
+
"box-shadow:0 0 0 2px rgba(255,255,255,0.35);";
|
|
260
|
+
element.appendChild(dot);
|
|
261
|
+
|
|
262
|
+
let anchor: THREE.Vector3 | null = null;
|
|
263
|
+
const place = () => {
|
|
264
|
+
if (!anchor) return;
|
|
265
|
+
const ndc = anchor.clone().project(world.camera.three);
|
|
266
|
+
dot.style.left = `${(ndc.x * 0.5 + 0.5) * element.clientWidth}px`;
|
|
267
|
+
dot.style.top = `${(-ndc.y * 0.5 + 0.5) * element.clientHeight}px`;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
let shown = false;
|
|
271
|
+
world.onDynamicAnchorSet.add((p: THREE.Vector3) => {
|
|
272
|
+
anchor = p.clone();
|
|
273
|
+
shown = false;
|
|
274
|
+
place();
|
|
275
|
+
});
|
|
276
|
+
world.onDynamicAnchorClear.add(() => {
|
|
277
|
+
anchor = null;
|
|
278
|
+
shown = false;
|
|
279
|
+
dot.style.display = "none";
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Reveal only once a real drag starts (DRAG px), so a click-to-select doesn't
|
|
283
|
+
// flash the dot.
|
|
284
|
+
const DRAG = 6;
|
|
285
|
+
let press: { x: number; y: number } | null = null;
|
|
286
|
+
element.addEventListener("pointerdown", (e) => {
|
|
287
|
+
if (e.button === 0) press = { x: e.clientX, y: e.clientY };
|
|
288
|
+
});
|
|
289
|
+
element.addEventListener("pointermove", (e) => {
|
|
290
|
+
if (!press || !anchor || shown) return;
|
|
291
|
+
const dx = e.clientX - press.x;
|
|
292
|
+
const dy = e.clientY - press.y;
|
|
293
|
+
if (dx * dx + dy * dy >= DRAG * DRAG) {
|
|
294
|
+
shown = true;
|
|
295
|
+
place();
|
|
296
|
+
dot.style.display = "block";
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const clearPress = () => {
|
|
300
|
+
press = null;
|
|
301
|
+
};
|
|
302
|
+
element.addEventListener("pointerup", clearPress);
|
|
303
|
+
element.addEventListener("pointercancel", clearPress);
|
|
304
|
+
|
|
305
|
+
// Keep the dot glued to the 3D pivot as the camera orbits around it.
|
|
306
|
+
world.renderer?.onBeforeUpdate.add(place);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
310
|
+
async function autoLoadFirstModel(components: OBC.Components, client: any) {
|
|
311
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
312
|
+
const projectId: string | undefined = client?.context?.projectId;
|
|
313
|
+
if (!projectId) {
|
|
314
|
+
console.warn("[a2] no projectId — skipping auto-load");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
319
|
+
const items = (await client.listFiles({ projectId })) as any[];
|
|
320
|
+
const frags = items.filter((it) =>
|
|
321
|
+
(it.name ?? "").toLowerCase().endsWith(".frag"),
|
|
322
|
+
);
|
|
323
|
+
const frag =
|
|
324
|
+
frags.find((it) => (it.name ?? "").toLowerCase().includes("bloxhub")) ??
|
|
325
|
+
frags[0];
|
|
326
|
+
if (!frag) {
|
|
327
|
+
console.warn("[a2] no .frag in project to auto-load");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const resp = await client.downloadFile(String(frag._id));
|
|
331
|
+
const buffer = await resp.arrayBuffer();
|
|
332
|
+
await fragments.core.load(buffer, { modelId: String(frag._id) });
|
|
333
|
+
await fragments.core.update(true);
|
|
334
|
+
console.log("[a2] auto-loaded model:", frag.name);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.warn("[a2] auto-load failed", error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as OBC from "@thatopen/components";
|
|
2
|
+
import { toolModeManager, type ManagedTool } from "./tool-mode-manager";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Active-tool HUD — a floating text overlay (top-left of the viewport) showing
|
|
6
|
+
* what the user is currently doing, e.g. "Drawing clipping plane" /
|
|
7
|
+
* "Measuring length".
|
|
8
|
+
*
|
|
9
|
+
* Fully DECOUPLED from any specific tool: it only subscribes to the global
|
|
10
|
+
* toolModeManager's `onActiveChanged` and renders the active tool's registered
|
|
11
|
+
* {@link ManagedTool.label} (+ optional icon), hidden when no tool is active. Any
|
|
12
|
+
* future tool that registers a label with the manager shows up here with zero
|
|
13
|
+
* HUD changes.
|
|
14
|
+
*
|
|
15
|
+
* Styled like the other in-viewer overlays (fps counter): panel surface + 1px
|
|
16
|
+
* contrast-20 border + theme text, pointer-events off.
|
|
17
|
+
*/
|
|
18
|
+
export interface ActiveToolHud {
|
|
19
|
+
element: HTMLElement;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const activeToolHud = (
|
|
23
|
+
parent: HTMLElement,
|
|
24
|
+
components: OBC.Components,
|
|
25
|
+
): ActiveToolHud => {
|
|
26
|
+
const manager = toolModeManager(components);
|
|
27
|
+
|
|
28
|
+
const el = document.createElement("div");
|
|
29
|
+
el.style.cssText = `
|
|
30
|
+
position: absolute; top: 0.6rem; left: 0.6rem; z-index: 10;
|
|
31
|
+
display: none; align-items: center; gap: 0.4rem;
|
|
32
|
+
padding: 0.25rem 0.6rem; border-radius: 0.5rem;
|
|
33
|
+
background: var(--bim-ui_bg-contrast-10, #262629);
|
|
34
|
+
border: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
35
|
+
color: var(--bim-ui_bg-contrast-100, #e3e3e3);
|
|
36
|
+
font: 600 0.74rem/1.2 "Plus Jakarta Sans", sans-serif;
|
|
37
|
+
pointer-events: none; user-select: none;
|
|
38
|
+
`;
|
|
39
|
+
const icon = document.createElement("bim-icon");
|
|
40
|
+
icon.style.fontSize = "0.95rem";
|
|
41
|
+
const text = document.createElement("span");
|
|
42
|
+
el.append(icon, text);
|
|
43
|
+
|
|
44
|
+
// The viewer is position:relative, so an absolutely-positioned child overlays
|
|
45
|
+
// the canvas correctly.
|
|
46
|
+
if (!parent.style.position) parent.style.position = "relative";
|
|
47
|
+
parent.append(el);
|
|
48
|
+
|
|
49
|
+
const render = (tool: ManagedTool | null) => {
|
|
50
|
+
if (!tool) {
|
|
51
|
+
el.style.display = "none";
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
text.textContent =
|
|
55
|
+
typeof tool.label === "function" ? tool.label() : tool.label;
|
|
56
|
+
if (tool.icon) {
|
|
57
|
+
icon.setAttribute("icon", tool.icon);
|
|
58
|
+
icon.style.display = "";
|
|
59
|
+
} else {
|
|
60
|
+
icon.style.display = "none";
|
|
61
|
+
}
|
|
62
|
+
el.style.display = "flex";
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
manager.onActiveChanged.add(render);
|
|
66
|
+
render(manager.active);
|
|
67
|
+
|
|
68
|
+
return { element: el };
|
|
69
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import * as OBC from "@thatopen/components";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Camera tools for the viewer toolbar. Returns controllers matching the
|
|
6
|
+
* toolbar's integration contract:
|
|
7
|
+
* - `fitAll` — ACTION: fit the camera to the whole loaded model.
|
|
8
|
+
* - `orthoToggle` — TOGGLE: switch the projection Perspective ⇄ Orthographic.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const firstWorld = (components: OBC.Components): any =>
|
|
13
|
+
[...components.get(OBC.Worlds).list.values()][0];
|
|
14
|
+
|
|
15
|
+
export interface CameraTools {
|
|
16
|
+
fitAll: { label: string; icon: string; run(): Promise<void> };
|
|
17
|
+
orthoToggle: {
|
|
18
|
+
label: string;
|
|
19
|
+
icon: string;
|
|
20
|
+
active(): boolean;
|
|
21
|
+
activate(): Promise<void>;
|
|
22
|
+
deactivate(): Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const cameraTools = (components: OBC.Components): CameraTools => {
|
|
27
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
fitAll: {
|
|
31
|
+
label: "Zoom to fit",
|
|
32
|
+
icon: "mdi:fit-to-page-outline",
|
|
33
|
+
async run() {
|
|
34
|
+
const controls = firstWorld(components)?.camera?.controls;
|
|
35
|
+
if (!controls) return;
|
|
36
|
+
// Union every loaded model's bounding box, then frame its sphere —
|
|
37
|
+
// fitToSphere preserves the current view direction (no rotation snap).
|
|
38
|
+
const box = new THREE.Box3();
|
|
39
|
+
for (const model of fragments.list.values()) {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
const b = (model as any).box as THREE.Box3 | undefined;
|
|
42
|
+
if (b && !b.isEmpty()) box.union(b);
|
|
43
|
+
}
|
|
44
|
+
if (box.isEmpty()) return;
|
|
45
|
+
const sphere = box.getBoundingSphere(new THREE.Sphere());
|
|
46
|
+
await controls.fitToSphere(sphere, true);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
orthoToggle: {
|
|
50
|
+
label: "Orthographic",
|
|
51
|
+
icon: "mdi:angle-right",
|
|
52
|
+
active() {
|
|
53
|
+
return firstWorld(components)?.camera?.projection?.current === "Orthographic";
|
|
54
|
+
},
|
|
55
|
+
async activate() {
|
|
56
|
+
await firstWorld(components)?.camera?.projection?.set?.("Orthographic");
|
|
57
|
+
},
|
|
58
|
+
async deactivate() {
|
|
59
|
+
await firstWorld(components)?.camera?.projection?.set?.("Perspective");
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|