@thatopen/services 0.3.0 → 0.3.2
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 +3 -0
- package/dist/built-in/index.d.ts +22 -2
- package/dist/cli.js +3 -3
- package/docs/ai-quickstart.md +86 -0
- package/docs/cli/create.md +4 -3
- package/package.json +3 -3
- package/src/built-in/index.ts +669 -649
- package/src/cli/templates/app/package.json +31 -31
- 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/main.ts +223 -340
- package/src/cli/templates/app/src/setups/camera-tools.ts +63 -63
- package/src/cli/templates/app/src/setups/clipper-tool.ts +20 -1
- package/src/cli/templates/app/src/setups/clipper.ts +75 -75
- package/src/cli/templates/app/src/setups/index.ts +23 -33
- package/src/cli/templates/app/src/setups/inspection.ts +136 -136
- package/src/cli/templates/app/src/setups/measurement-tool.ts +19 -1
- package/src/cli/templates/app/src/setups/measurements.ts +133 -133
- 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/setups/right-sidebar.ts +178 -178
- package/src/cli/templates/app/src/setups/styles-panel.ts +110 -110
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +15 -9
- package/src/cli/templates/app/src/setups/tool-mode.ts +23 -23
- package/src/cli/templates/app/src/setups/active-tool-hud.ts +0 -69
- package/src/cli/templates/app/src/setups/clipper-panel.ts +0 -167
- package/src/cli/templates/app/src/setups/commands-panel.ts +0 -239
- package/src/cli/templates/app/src/setups/data-table-panel.ts +0 -2224
- package/src/cli/templates/app/src/setups/file-format-icons.ts +0 -95
- package/src/cli/templates/app/src/setups/files-panel.ts +0 -1503
- package/src/cli/templates/app/src/setups/fps-indicator.ts +0 -63
- package/src/cli/templates/app/src/setups/graphics-panel.ts +0 -490
- package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +0 -161
- package/src/cli/templates/app/src/setups/model-tree.ts +0 -797
- package/src/cli/templates/app/src/setups/objects-panel.ts +0 -127
- package/src/cli/templates/app/src/setups/properties-panel.ts +0 -1048
- package/src/cli/templates/app/src/setups/settings-panel.ts +0 -94
- package/src/cli/templates/app/src/setups/visibility-toolbar.ts +0 -498
- package/src/cli/templates/app/src/setups/walkthrough.ts +0 -528
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import * as BUI from "@thatopen/ui";
|
|
2
|
-
import { cardHeader } from "./card-header";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Merged SETTINGS panel (UI-reorg) — ONE scrolling bim-panel with a collapsible
|
|
6
|
-
* section per source panel (Graphics / Clip styling / Measurement / Commands).
|
|
7
|
-
* Reuses each panel's existing element verbatim.
|
|
8
|
-
*
|
|
9
|
-
* Sections use a hand-rolled light-DOM header (NOT bim-panel-section, whose
|
|
10
|
-
* header/content padding is hardcoded in shadow DOM and can't be flattened) so
|
|
11
|
-
* the section title row + its divider span the FULL panel width with no inset,
|
|
12
|
-
* and the section content sits flush. The nested bim-panel's own chrome is
|
|
13
|
-
* removed (transparent bg / no border / no radius) and its height:100% / inner
|
|
14
|
-
* scroll neutralised so it flows and the Settings panel does the single scroll —
|
|
15
|
-
* all scoped to light-DOM descendants, so bim-* widget shadow internals (sliders,
|
|
16
|
-
* dropdowns, color inputs) are untouched. Returns the `bim-panel` (no self-mount).
|
|
17
|
-
*
|
|
18
|
-
* @param sections ordered settings sections (label + icon + the panel element)
|
|
19
|
-
* @returns the merged Settings `bim-panel`
|
|
20
|
-
*/
|
|
21
|
-
export const settingsPanel = (
|
|
22
|
-
sections: { label: string; icon: string; el: HTMLElement }[],
|
|
23
|
-
) => {
|
|
24
|
-
for (const s of sections) {
|
|
25
|
-
s.el.setAttribute("header-hidden", ""); // our section header replaces it
|
|
26
|
-
s.el.style.height = "auto";
|
|
27
|
-
s.el.style.width = "100%";
|
|
28
|
-
}
|
|
29
|
-
const collapsed = new Set<string>(); // section labels currently collapsed
|
|
30
|
-
|
|
31
|
-
const [panel, update] = BUI.Component.create<BUI.Panel, { tick: number }>(
|
|
32
|
-
(_s) => BUI.html`
|
|
33
|
-
<bim-panel
|
|
34
|
-
label="Settings"
|
|
35
|
-
icon="mdi:cog"
|
|
36
|
-
header-hidden
|
|
37
|
-
style="width: 100%; height: 100%; pointer-events: auto;"
|
|
38
|
-
>
|
|
39
|
-
<style>
|
|
40
|
-
.set-scroll { display: flex; flex-direction: column; height: 100%; overflow-y: auto; }
|
|
41
|
-
.set-scroll::-webkit-scrollbar { width: 0.4rem; }
|
|
42
|
-
.set-scroll::-webkit-scrollbar-thumb { border-radius: 0.25rem;
|
|
43
|
-
background-color: var(--bim-scrollbar--c, #3C3C41); }
|
|
44
|
-
/* Full-width section header: title row + divider span the whole panel,
|
|
45
|
-
no side inset/box. */
|
|
46
|
-
.set-hd { display: flex; align-items: center; gap: 0.5rem; cursor: pointer;
|
|
47
|
-
margin-top: 1.2rem;
|
|
48
|
-
padding: 0.45rem 0.75rem; font-weight: 600;
|
|
49
|
-
font-size: var(--bim-ui_size-lg, 0.8125rem);
|
|
50
|
-
color: var(--bim-ui_bg-contrast-100, #f1f2f4);
|
|
51
|
-
/* contrast-40 (#323237) so the divider is VISIBLE on the #262629
|
|
52
|
-
surface — contrast-20 now equals the surface (palette pin). */
|
|
53
|
-
border-bottom: 1px solid var(--bim-ui_bg-contrast-40, rgba(255,255,255,0.12)); }
|
|
54
|
-
.set-hd:hover { background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.04)); }
|
|
55
|
-
/* Section icon matches the title font color (not a dimmer grey). */
|
|
56
|
-
.set-hd-ico { flex: 0 0 auto; color: inherit; }
|
|
57
|
-
.set-hd-t { flex: 1 1 auto; }
|
|
58
|
-
.set-hd-x { flex: 0 0 auto; color: var(--bim-ui_bg-contrast-80, #adadad); }
|
|
59
|
-
/* Flatten the nested panel: no "panel inside a panel", content flush to
|
|
60
|
-
the edges (its own bands full-bleed). Light-DOM only — widget shadow
|
|
61
|
-
internals untouched. */
|
|
62
|
-
.set-sec bim-panel {
|
|
63
|
-
height: auto !important;
|
|
64
|
-
width: 100%;
|
|
65
|
-
--bim-panel--bg: transparent;
|
|
66
|
-
--bim-panel--border: none;
|
|
67
|
-
border-radius: 0 !important;
|
|
68
|
-
box-shadow: none !important;
|
|
69
|
-
}
|
|
70
|
-
.set-sec bim-panel * { height: auto !important; max-height: none !important; }
|
|
71
|
-
</style>
|
|
72
|
-
<div class="set-scroll">
|
|
73
|
-
${cardHeader("mdi:cog", "Settings", "0.75rem")}
|
|
74
|
-
${sections.map((s, i) => {
|
|
75
|
-
const open = !collapsed.has(s.label);
|
|
76
|
-
return BUI.html`
|
|
77
|
-
<div class="set-hd" style=${i === 0 ? "margin-top: 0;" : ""} @click=${() => {
|
|
78
|
-
if (collapsed.has(s.label)) collapsed.delete(s.label);
|
|
79
|
-
else collapsed.add(s.label);
|
|
80
|
-
update({ tick: 0 });
|
|
81
|
-
}}>
|
|
82
|
-
<bim-icon class="set-hd-ico" icon=${s.icon}></bim-icon>
|
|
83
|
-
<span class="set-hd-t">${s.label}</span>
|
|
84
|
-
<bim-icon class="set-hd-x" icon=${open ? "mdi:chevron-up" : "mdi:chevron-down"}></bim-icon>
|
|
85
|
-
</div>
|
|
86
|
-
<div class="set-sec" style=${open ? "" : "display: none;"}>${s.el}</div>`;
|
|
87
|
-
})}
|
|
88
|
-
</div>
|
|
89
|
-
</bim-panel>`,
|
|
90
|
-
{ tick: 0 },
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
return panel;
|
|
94
|
-
};
|
|
@@ -1,498 +0,0 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import * as OBC from "@thatopen/components";
|
|
3
|
-
import * as OBF from "@thatopen/components-front";
|
|
4
|
-
import * as BUI from "@thatopen/ui";
|
|
5
|
-
import type { WalkthroughController } from "./walkthrough";
|
|
6
|
-
import { toolModeManager } from "./tool-mode-manager";
|
|
7
|
-
import type { InspectionAction } from "./inspection";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Floating "visibility" toolbar — bottom-center over the viewport.
|
|
11
|
-
*
|
|
12
|
-
* A self-mounting `bim-toolbar` (matching the look of `toolbar.ts`: dark
|
|
13
|
-
* `bg-base`, rounded, subtle shadow, icon-only `bim-button`s with standalone
|
|
14
|
-
* `<bim-tooltip>` hover tooltips) placed inside a floating `bim-grid` overlay so
|
|
15
|
-
* empty areas click through to the 3D scene.
|
|
16
|
-
*
|
|
17
|
-
* ── MODE TOGGLE ────────────────────────────────────────────────────────────
|
|
18
|
-
* One labeled toggle button flips the *target* every action operates on:
|
|
19
|
-
* - "Selected" → the current Highlighter select set (`selection.select`).
|
|
20
|
-
* If nothing is selected the target is EMPTY (actions no-op).
|
|
21
|
-
* - "Unselected" → everything that is NOT selected. If nothing is selected the
|
|
22
|
-
* target is the WHOLE model.
|
|
23
|
-
*
|
|
24
|
-
* ── ACTIONS (each reversible via Show / Reset) ─────────────────────────────
|
|
25
|
-
* - HIDE → hide the target set.
|
|
26
|
-
* - SHOW → un-hide the target set.
|
|
27
|
-
* - ISOLATE → show only the target set, hide everything else.
|
|
28
|
-
* - GHOST → render the target set semi-transparent; opaque rest.
|
|
29
|
-
* - RESET → restore full visibility + opacity (clears hide AND ghost).
|
|
30
|
-
*
|
|
31
|
-
* Combos give full control, e.g.:
|
|
32
|
-
* - Unselected + Hide = isolate the selection.
|
|
33
|
-
* - Selected + Hide = hide the selection.
|
|
34
|
-
* - Show / Reset = restore.
|
|
35
|
-
*
|
|
36
|
-
* ── VISIBILITY API ─────────────────────────────────────────────────────────
|
|
37
|
-
* Visibility (hide/show/isolate) goes through `OBC.Hider` (`set` / `isolate`),
|
|
38
|
-
* which drives the fragments' per-item visibility flag and triggers the core
|
|
39
|
-
* redraw itself — NOT the Highlighter recolor-on-hover path (honoring the perf
|
|
40
|
-
* rule). Ghosting uses a dedicated transparent Highlighter style ("ghost") —
|
|
41
|
-
* the idiomatic flat x-ray the library's Hoverer example calls "ghost mode".
|
|
42
|
-
* It renders a uniform faint translucent gray shell as a flat overlay (NOT
|
|
43
|
-
* through the deferred pen-shadows emitter, which looked muddy), is fully
|
|
44
|
-
* reversible via `highlighter.clear("ghost")`, and coexists with the "select"
|
|
45
|
-
* style so selection still works while items are ghosted.
|
|
46
|
-
*
|
|
47
|
-
* ── MOUNTING ───────────────────────────────────────────────────────────────
|
|
48
|
-
* `visibilityToolbar(components)` builds the floating overlay AND appends it to
|
|
49
|
-
* the viewport itself (it locates the viewport like the other setups do), then
|
|
50
|
-
* returns the `bim-toolbar` element. To wire it from main.ts, add this single
|
|
51
|
-
* line after the viewport + Highlighter exist (e.g. next to the `toolbar(...)`
|
|
52
|
-
* call):
|
|
53
|
-
*
|
|
54
|
-
* visibilityToolbar(components);
|
|
55
|
-
*
|
|
56
|
-
* @param components engine components
|
|
57
|
-
* @param container optional viewport element to overlay; if omitted, the first
|
|
58
|
-
* world's renderer container (the viewport) is used.
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
|
-
type TargetMode = "selected" | "unselected";
|
|
62
|
-
|
|
63
|
-
// Minimal toggle-controller shape (exploded view, etc.) — a single toolbar
|
|
64
|
-
// button can drive any controller exposing this surface.
|
|
65
|
-
type ToggleCtrl = {
|
|
66
|
-
toggle(): void;
|
|
67
|
-
isActive(): boolean;
|
|
68
|
-
onChange(cb: (active: boolean) => void): () => void;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const isModelIdMapEmpty = (map?: OBC.ModelIdMap) =>
|
|
72
|
-
!map || !Object.values(map).some((set) => set.size > 0);
|
|
73
|
-
|
|
74
|
-
export const visibilityToolbar = (
|
|
75
|
-
components: OBC.Components,
|
|
76
|
-
container?: HTMLElement,
|
|
77
|
-
walk?: WalkthroughController,
|
|
78
|
-
explode?: ToggleCtrl,
|
|
79
|
-
inspection?: InspectionAction[],
|
|
80
|
-
) => {
|
|
81
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
82
|
-
const hider = components.get(OBC.Hider);
|
|
83
|
-
const highlighter = components.get(OBF.Highlighter);
|
|
84
|
-
|
|
85
|
-
// ── Ghost / x-ray (scalable per-element GPU state texture) ──────────────
|
|
86
|
-
// The legacy approach recolored fragments per item via a Highlighter "ghost"
|
|
87
|
-
// style (highlightByID) — O(elements) CPU churn that breaks batching. Ghost
|
|
88
|
-
// now lives on the GPU: `model.setGhostItems(localIds)` flips a per-element
|
|
89
|
-
// state texture sampled in the SHELL shader (faint screen-door + desaturate),
|
|
90
|
-
// so it scales to millions of elements with no per-item CPU work. See
|
|
91
|
-
// engine_fragments-beta ghost-emission.ts / material-manager.ts.
|
|
92
|
-
let mode: TargetMode = "selected";
|
|
93
|
-
|
|
94
|
-
// ── Target-set resolution ──────────────────────────────────────────────
|
|
95
|
-
const selection = (): OBC.ModelIdMap | undefined => highlighter.selection.select;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* The ModelIdMap the current mode targets.
|
|
99
|
-
* - selected: the select set (empty → empty map).
|
|
100
|
-
* - unselected: every loaded item minus the select set (nothing selected →
|
|
101
|
-
* the whole model).
|
|
102
|
-
* Built async because the "unselected" set is derived from each model's full
|
|
103
|
-
* item list (via getItemsByVisibility(true|false) union = all items).
|
|
104
|
-
*/
|
|
105
|
-
const targetMap = async (): Promise<OBC.ModelIdMap> => {
|
|
106
|
-
const sel = selection();
|
|
107
|
-
if (mode === "selected") {
|
|
108
|
-
// Clone defensively (Hider/iteration shouldn't mutate the live set).
|
|
109
|
-
const out: OBC.ModelIdMap = {};
|
|
110
|
-
if (sel) {
|
|
111
|
-
for (const [modelId, set] of Object.entries(sel)) {
|
|
112
|
-
if (set.size > 0) out[modelId] = new Set(set);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return out;
|
|
116
|
-
}
|
|
117
|
-
// unselected = all items in every model, minus the selected ones.
|
|
118
|
-
const out: OBC.ModelIdMap = {};
|
|
119
|
-
for (const [modelId, model] of fragments.list) {
|
|
120
|
-
// Union of visible + hidden = every item the model knows about.
|
|
121
|
-
const [vis, hid] = await Promise.all([
|
|
122
|
-
model.getItemsByVisibility(true),
|
|
123
|
-
model.getItemsByVisibility(false),
|
|
124
|
-
]);
|
|
125
|
-
const all = new Set<number>([...vis, ...hid]);
|
|
126
|
-
const selSet = sel?.[modelId];
|
|
127
|
-
if (selSet) for (const id of selSet) all.delete(id);
|
|
128
|
-
if (all.size > 0) out[modelId] = all;
|
|
129
|
-
}
|
|
130
|
-
return out;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// ── Ghost helpers (scalable GPU hash table) ─────────────────────────────
|
|
134
|
-
// Always hash the SMALL key set (the selection) + an invert flag, so the
|
|
135
|
-
// table stays tiny whether we ghost the selection or everything-but it:
|
|
136
|
-
// - "selected" → ghost the selection (invert=false).
|
|
137
|
-
// - "unselected" → ghost everything EXCEPT the selection (invert=true),
|
|
138
|
-
// applied to every loaded model (a model with no selection → empty key set
|
|
139
|
-
// + invert=true → fully ghosted).
|
|
140
|
-
const applyGhost = async () => {
|
|
141
|
-
const sel = selection() ?? {};
|
|
142
|
-
const invert = mode === "unselected";
|
|
143
|
-
const tasks: Promise<unknown>[] = [];
|
|
144
|
-
for (const model of fragments.list.values()) {
|
|
145
|
-
const set = sel[model.modelId];
|
|
146
|
-
const ids = set ? [...set] : [];
|
|
147
|
-
if (!invert && ids.length === 0) continue; // ghost-selected with nothing selected
|
|
148
|
-
// setGhostItems is async (localId→itemId conversion) — update after all.
|
|
149
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
-
tasks.push((model as any).setGhostItems(ids, invert));
|
|
151
|
-
}
|
|
152
|
-
await Promise.all(tasks);
|
|
153
|
-
await fragments.core.update(true);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
/** Clear the ghost overlay on every loaded model. */
|
|
157
|
-
const clearGhost = async () => {
|
|
158
|
-
for (const model of fragments.list.values()) {
|
|
159
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
160
|
-
(model as any).clearGhost?.();
|
|
161
|
-
}
|
|
162
|
-
await fragments.core.update(true);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Un-ghost the given items in place (without disturbing the rest of the ghost
|
|
167
|
-
* state). Making items visible should also clear their ghost, so a shown
|
|
168
|
-
* element never lingers as a ghost.
|
|
169
|
-
*/
|
|
170
|
-
const unghost = async (map: OBC.ModelIdMap) => {
|
|
171
|
-
const tasks: Promise<unknown>[] = [];
|
|
172
|
-
for (const model of fragments.list.values()) {
|
|
173
|
-
const set = map[model.modelId];
|
|
174
|
-
if (!set || set.size === 0) continue;
|
|
175
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
-
tasks.push((model as any).unsetGhostItems?.([...set]));
|
|
177
|
-
}
|
|
178
|
-
if (!tasks.length) return;
|
|
179
|
-
await Promise.all(tasks);
|
|
180
|
-
await fragments.core.update(true);
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// ── Actions ─────────────────────────────────────────────────────────────
|
|
184
|
-
const doHide = async () => {
|
|
185
|
-
const map = await targetMap();
|
|
186
|
-
if (isModelIdMapEmpty(map)) return; // nothing to hide (e.g. Selected w/ no selection)
|
|
187
|
-
await hider.set(false, map);
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const doShow = async () => {
|
|
191
|
-
const map = await targetMap();
|
|
192
|
-
// For "unselected" w/ no selection this is the whole model → show all.
|
|
193
|
-
if (isModelIdMapEmpty(map)) {
|
|
194
|
-
await hider.set(true);
|
|
195
|
-
await clearGhost(); // all visible → nothing should stay ghosted
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
await hider.set(true, map);
|
|
199
|
-
await unghost(map); // shown items un-ghost
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const doIsolate = async () => {
|
|
203
|
-
const map = await targetMap();
|
|
204
|
-
if (isModelIdMapEmpty(map)) return; // isolating an empty set would blank the model
|
|
205
|
-
await hider.isolate(map);
|
|
206
|
-
await unghost(map); // the isolated (visible) items un-ghost
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const doGhost = async () => {
|
|
210
|
-
await applyGhost();
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
// Frame the camera on the target set's merged bounding box (same fit the tree's
|
|
214
|
-
// Focus button uses). Targets selected or unselected per the mode toggle.
|
|
215
|
-
const doFocus = async () => {
|
|
216
|
-
const map = await targetMap();
|
|
217
|
-
if (isModelIdMapEmpty(map)) return;
|
|
218
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
219
|
-
const world = [...components.get(OBC.Worlds).list.values()][0] as any;
|
|
220
|
-
const controls = world?.camera?.controls;
|
|
221
|
-
if (!controls?.fitToSphere) return;
|
|
222
|
-
try {
|
|
223
|
-
const boxes = (await fragments.getBBoxes(map)) as THREE.Box3[];
|
|
224
|
-
const box = new THREE.Box3();
|
|
225
|
-
for (const b of boxes) box.union(b);
|
|
226
|
-
if (box.isEmpty()) return;
|
|
227
|
-
const sphere = box.getBoundingSphere(new THREE.Sphere());
|
|
228
|
-
await controls.fitToSphere(sphere, true); // animated, preserves view dir
|
|
229
|
-
} catch (error) {
|
|
230
|
-
console.warn("[visibility-toolbar] focus failed", error);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// Reset: restore full visibility AND clear every ghost override.
|
|
235
|
-
const doReset = async () => {
|
|
236
|
-
await hider.set(true);
|
|
237
|
-
await clearGhost();
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
// ── Toolbar UI ──────────────────────────────────────────────────────────
|
|
241
|
-
type Action = { label: string; icon: string; run: () => void | Promise<void> };
|
|
242
|
-
const actions: Action[] = [
|
|
243
|
-
{ label: "Focus", icon: "mdi:image-filter-center-focus", run: doFocus },
|
|
244
|
-
{ label: "Hide", icon: "mdi:eye-off-outline", run: doHide },
|
|
245
|
-
{ label: "Show", icon: "mdi:eye-outline", run: doShow },
|
|
246
|
-
{ label: "Isolate", icon: "mdi:select-search", run: doIsolate },
|
|
247
|
-
{ label: "Ghost", icon: "mdi:ghost-outline", run: doGhost },
|
|
248
|
-
];
|
|
249
|
-
|
|
250
|
-
let busy = false;
|
|
251
|
-
const onAction = async (run: () => void | Promise<void>) => {
|
|
252
|
-
if (busy) return;
|
|
253
|
-
busy = true;
|
|
254
|
-
try {
|
|
255
|
-
await run();
|
|
256
|
-
} catch (error) {
|
|
257
|
-
console.warn("[visibility-toolbar] action failed", error);
|
|
258
|
-
} finally {
|
|
259
|
-
busy = false;
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const toggleMode = () => {
|
|
264
|
-
mode = mode === "selected" ? "unselected" : "selected";
|
|
265
|
-
barUpdate({ tick: ++tick });
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
// ── Camera projection (perspective ⇄ orthographic) ──────────────────────
|
|
269
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
270
|
-
const cameraProjection = (): any =>
|
|
271
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
272
|
-
([...components.get(OBC.Worlds).list.values()][0] as any)?.camera?.projection;
|
|
273
|
-
const isOrtho = () => cameraProjection()?.current === "Orthographic";
|
|
274
|
-
|
|
275
|
-
// AO now reconstructs correctly under orthographic projection (fixed at source
|
|
276
|
-
// in the beta postproduction shaders via the `uOrtho` branch), so we no longer
|
|
277
|
-
// force it off in ortho — the user's AO setting is honored in both projections.
|
|
278
|
-
const toggleProjection = async () => {
|
|
279
|
-
const proj = cameraProjection();
|
|
280
|
-
if (!proj?.set) return;
|
|
281
|
-
const goingOrtho = !isOrtho();
|
|
282
|
-
await proj.set(goingOrtho ? "Orthographic" : "Perspective");
|
|
283
|
-
barUpdate({ tick: ++tick });
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
let tick = 0;
|
|
287
|
-
|
|
288
|
-
const [bar, barUpdate] = BUI.Component.create<HTMLElement, { tick: number }>(
|
|
289
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
290
|
-
(_state) => {
|
|
291
|
-
const modeLabel = mode === "selected" ? "Selected" : "Unselected";
|
|
292
|
-
const modeIcon =
|
|
293
|
-
mode === "selected" ? "mdi:cursor-default-click-outline" : "mdi:select-remove";
|
|
294
|
-
const modeTip =
|
|
295
|
-
mode === "selected"
|
|
296
|
-
? "Target: SELECTED elements — click to switch to Unselected"
|
|
297
|
-
: "Target: UNSELECTED elements (rest of model) — click to switch to Selected";
|
|
298
|
-
return BUI.html`
|
|
299
|
-
<bim-toolbar
|
|
300
|
-
style="
|
|
301
|
-
pointer-events: auto;
|
|
302
|
-
overflow: visible;
|
|
303
|
-
padding: 0.15rem;
|
|
304
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
305
|
-
"
|
|
306
|
-
>
|
|
307
|
-
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
308
|
-
<bim-button
|
|
309
|
-
label=${modeLabel}
|
|
310
|
-
icon=${modeIcon}
|
|
311
|
-
@click=${toggleMode}
|
|
312
|
-
style="height: 1.9rem;"
|
|
313
|
-
><bim-tooltip placement="top">${modeTip}</bim-tooltip></bim-button>
|
|
314
|
-
</bim-toolbar-section>
|
|
315
|
-
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
316
|
-
${actions.map(
|
|
317
|
-
(a) => BUI.html`
|
|
318
|
-
<bim-button
|
|
319
|
-
icon=${a.icon}
|
|
320
|
-
@click=${() => onAction(a.run)}
|
|
321
|
-
style="width: 1.9rem; min-width: 1.9rem; height: 1.9rem;"
|
|
322
|
-
><bim-tooltip placement="top">${`${a.label} ${modeLabel.toLowerCase()}`}</bim-tooltip></bim-button>
|
|
323
|
-
`,
|
|
324
|
-
)}
|
|
325
|
-
</bim-toolbar-section>
|
|
326
|
-
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
327
|
-
<bim-button
|
|
328
|
-
icon="mdi:restore"
|
|
329
|
-
@click=${() => onAction(doReset)}
|
|
330
|
-
style="width: 1.9rem; min-width: 1.9rem; height: 1.9rem;"
|
|
331
|
-
><bim-tooltip placement="top">Reset: show all + clear ghost</bim-tooltip></bim-button>
|
|
332
|
-
</bim-toolbar-section>
|
|
333
|
-
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
334
|
-
<bim-button
|
|
335
|
-
icon=${isOrtho() ? "mdi:perspective-less" : "mdi:perspective-more"}
|
|
336
|
-
@click=${toggleProjection}
|
|
337
|
-
style="width: 1.9rem; min-width: 1.9rem; height: 1.9rem;"
|
|
338
|
-
><bim-tooltip placement="top">${
|
|
339
|
-
isOrtho()
|
|
340
|
-
? "Orthographic view — switch to Perspective"
|
|
341
|
-
: "Perspective view — switch to Orthographic"
|
|
342
|
-
}</bim-tooltip></bim-button>
|
|
343
|
-
</bim-toolbar-section>
|
|
344
|
-
${walk
|
|
345
|
-
? BUI.html`<bim-toolbar-section label-hidden style="background: transparent;">
|
|
346
|
-
<bim-button
|
|
347
|
-
icon="mdi:walk"
|
|
348
|
-
?active=${walk.isActive()}
|
|
349
|
-
@click=${() => walk.toggle()}
|
|
350
|
-
style="width: 1.9rem; min-width: 1.9rem; height: 1.9rem;"
|
|
351
|
-
><bim-tooltip placement="top">${
|
|
352
|
-
walk.isActive()
|
|
353
|
-
? "Exit walkthrough"
|
|
354
|
-
: "Walkthrough — first-person navigation"
|
|
355
|
-
}</bim-tooltip></bim-button>
|
|
356
|
-
</bim-toolbar-section>`
|
|
357
|
-
: BUI.html``}
|
|
358
|
-
${explode
|
|
359
|
-
? BUI.html`<bim-toolbar-section label-hidden style="background: transparent;">
|
|
360
|
-
<bim-button
|
|
361
|
-
icon="mdi:arrow-expand-vertical"
|
|
362
|
-
?active=${explode.isActive()}
|
|
363
|
-
@click=${() => explode.toggle()}
|
|
364
|
-
style="width: 1.9rem; min-width: 1.9rem; height: 1.9rem;"
|
|
365
|
-
><bim-tooltip placement="top">${
|
|
366
|
-
explode.isActive()
|
|
367
|
-
? "Collapse — un-explode"
|
|
368
|
-
: "Exploded view"
|
|
369
|
-
}</bim-tooltip></bim-button>
|
|
370
|
-
</bim-toolbar-section>`
|
|
371
|
-
: BUI.html``}
|
|
372
|
-
</bim-toolbar>
|
|
373
|
-
`;
|
|
374
|
-
},
|
|
375
|
-
{ tick: 0 },
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
// Keep the walkthrough button's active state in sync (incl. auto-exit on
|
|
379
|
-
// model unload) without polling.
|
|
380
|
-
if (walk) walk.onChange(() => barUpdate({ tick: ++tick }));
|
|
381
|
-
if (explode) explode.onChange(() => barUpdate({ tick: ++tick }));
|
|
382
|
-
|
|
383
|
-
// ── Inspection tab (Select default · Clip · Measure length/area/angle) ──────
|
|
384
|
-
// A second toolbar holding the inspection tools, routed through W1's
|
|
385
|
-
// toolModeManager (exclusive). Built only when `inspection` actions are passed;
|
|
386
|
-
// the bottom bar then becomes a bim-tabs with View (visibility) + Inspect tabs.
|
|
387
|
-
const manager = toolModeManager(components);
|
|
388
|
-
let itick = 0;
|
|
389
|
-
const [inspectionBar, inspUpdate] = BUI.Component.create<BUI.Toolbar, { tick: number }>(
|
|
390
|
-
// Arity >= 1 (state param) required, else create returns a single element and
|
|
391
|
-
// the [inspectionBar, inspUpdate] destructure throws "object is not iterable".
|
|
392
|
-
(_s) => {
|
|
393
|
-
const selActive = manager.getActiveId() === "select";
|
|
394
|
-
const actionButton = (a: InspectionAction) => BUI.html`
|
|
395
|
-
<bim-button
|
|
396
|
-
icon=${a.icon}
|
|
397
|
-
?active=${a.isActive()}
|
|
398
|
-
@click=${() => a.activate()}
|
|
399
|
-
style="width: 1.9rem; min-width: 1.9rem; height: 1.9rem;"
|
|
400
|
-
><bim-tooltip placement="top">${a.label}</bim-tooltip></bim-button>`;
|
|
401
|
-
// Split the actions into a Clip section + a Measure section so the toolbar's
|
|
402
|
-
// section divider draws a line between the cut button and the measure tools.
|
|
403
|
-
const acts = inspection ?? [];
|
|
404
|
-
const clipActions = acts.filter((a) => a.id.startsWith("clip"));
|
|
405
|
-
const measureActions = acts.filter((a) => a.id.startsWith("measure"));
|
|
406
|
-
return BUI.html`
|
|
407
|
-
<bim-toolbar
|
|
408
|
-
style="pointer-events: auto; overflow: visible; padding: 0.15rem; box-shadow: 0 2px 8px rgba(0,0,0,0.25);"
|
|
409
|
-
>
|
|
410
|
-
<bim-toolbar-section label-hidden style="background: transparent;">
|
|
411
|
-
<bim-button
|
|
412
|
-
icon="mdi:cursor-default"
|
|
413
|
-
?active=${selActive}
|
|
414
|
-
@click=${() => manager.selectMode()}
|
|
415
|
-
style="width: 1.9rem; min-width: 1.9rem; height: 1.9rem;"
|
|
416
|
-
><bim-tooltip placement="top">Select</bim-tooltip></bim-button>
|
|
417
|
-
</bim-toolbar-section>
|
|
418
|
-
${clipActions.length
|
|
419
|
-
? BUI.html`<bim-toolbar-section label-hidden style="background: transparent;">
|
|
420
|
-
${clipActions.map(actionButton)}
|
|
421
|
-
</bim-toolbar-section>`
|
|
422
|
-
: BUI.html``}
|
|
423
|
-
${measureActions.length
|
|
424
|
-
? BUI.html`<bim-toolbar-section label-hidden style="background: transparent;">
|
|
425
|
-
${measureActions.map(actionButton)}
|
|
426
|
-
</bim-toolbar-section>`
|
|
427
|
-
: BUI.html``}
|
|
428
|
-
</bim-toolbar>`;
|
|
429
|
-
},
|
|
430
|
-
{ tick: 0 },
|
|
431
|
-
);
|
|
432
|
-
// Refresh ALL button active-states (Select + tool buttons) when the active
|
|
433
|
-
// tool changes (W1 fires onActiveChanged through the manager).
|
|
434
|
-
manager.onActiveChanged.add(() => inspUpdate({ tick: ++itick }));
|
|
435
|
-
|
|
436
|
-
// The element docked in the floating grid: a tabbed bar when inspection actions
|
|
437
|
-
// are supplied (View = visibility, Inspect = tools), else just the bar.
|
|
438
|
-
const dock: HTMLElement = inspection
|
|
439
|
-
? (BUI.Component.create(
|
|
440
|
-
() => BUI.html`
|
|
441
|
-
<bim-tabs floating style="pointer-events: auto;">
|
|
442
|
-
<bim-tab name="view" label="View" icon="mdi:eye-outline">${bar}</bim-tab>
|
|
443
|
-
<bim-tab name="inspect" label="Inspect" icon="mdi:cursor-default-click">${inspectionBar}</bim-tab>
|
|
444
|
-
</bim-tabs>`,
|
|
445
|
-
) as unknown as HTMLElement)
|
|
446
|
-
: (bar as unknown as HTMLElement);
|
|
447
|
-
|
|
448
|
-
// ── Floating grid overlay: bar docked bottom-center, empty areas click through ──
|
|
449
|
-
const grid = BUI.Component.create(() => {
|
|
450
|
-
const onCreated = (element?: Element) => {
|
|
451
|
-
if (!element) return;
|
|
452
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
453
|
-
const g = element as any;
|
|
454
|
-
g.elements = { bar: dock };
|
|
455
|
-
g.layouts = {
|
|
456
|
-
main: {
|
|
457
|
-
template: `
|
|
458
|
-
"fillL fillC fillR" 1fr
|
|
459
|
-
"restL bar restR" auto
|
|
460
|
-
/ 1fr auto 1fr
|
|
461
|
-
`,
|
|
462
|
-
},
|
|
463
|
-
};
|
|
464
|
-
g.layout = "main";
|
|
465
|
-
};
|
|
466
|
-
return BUI.html`
|
|
467
|
-
<bim-grid style="padding: 1rem;" ${BUI.ref(onCreated)} floating></bim-grid>
|
|
468
|
-
`;
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// Resolve the viewport to overlay (the renderer container of the first world).
|
|
472
|
-
const resolveContainer = (): HTMLElement | undefined => {
|
|
473
|
-
if (container) return container;
|
|
474
|
-
const world = [...components.get(OBC.Worlds).list.values()][0] as
|
|
475
|
-
| { renderer?: { three?: { domElement?: HTMLElement } } }
|
|
476
|
-
| undefined;
|
|
477
|
-
const canvas = world?.renderer?.three?.domElement;
|
|
478
|
-
// Overlay the canvas's parent (the viewport element), so the floating grid
|
|
479
|
-
// sits on top of the 3D scene.
|
|
480
|
-
return (canvas?.parentElement as HTMLElement | undefined) ?? undefined;
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
const host = resolveContainer();
|
|
484
|
-
if (host) {
|
|
485
|
-
host.append(grid);
|
|
486
|
-
} else {
|
|
487
|
-
console.warn(
|
|
488
|
-
"[visibility-toolbar] no viewport found to overlay; append the returned element manually",
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Refresh the tooltips/state when the selection changes (so the Selected-mode
|
|
493
|
-
// action tooltips reflect whether there's anything to act on).
|
|
494
|
-
highlighter.events.select.onHighlight.add(() => barUpdate({ tick: ++tick }));
|
|
495
|
-
highlighter.events.select.onClear.add(() => barUpdate({ tick: ++tick }));
|
|
496
|
-
|
|
497
|
-
return bar;
|
|
498
|
-
};
|