@thatopen/services 0.3.11 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/revit.d.ts +9 -0
- package/dist/cli/lib/revit-addin.d.ts +20 -0
- package/dist/cli.js +128 -0
- package/dist/cloud/LasToPotree.d.ts +59 -0
- package/dist/cloud/SplatToFq.d.ts +38 -0
- package/dist/cloud/index.d.ts +2 -0
- package/docs/builtin/paths.json +86 -86
- package/docs/cloud/GaussianSplatConverter.md +43 -0
- package/docs/cloud/PointCloudConverter.md +53 -0
- package/docs/revit-collab-quickstart.md +205 -0
- package/package.json +95 -95
- package/src/built-in/ClashesManager/example.ts +283 -283
- package/src/built-in/UIManager/src/clashes-panel/example.ts +160 -160
- package/src/built-in/UIManager/src/models-list/example.ts +122 -118
- package/src/cli/templates/app/package.json +33 -33
- package/src/cli/templates/app/src/setups/camera-tools.ts +0 -63
- package/src/cli/templates/app/src/setups/camera-views.ts +0 -201
- package/src/cli/templates/app/src/setups/card-header.ts +0 -35
- package/src/cli/templates/app/src/setups/clipper-tool.ts +0 -773
- package/src/cli/templates/app/src/setups/clipper.ts +0 -75
- package/src/cli/templates/app/src/setups/cloud-runner.ts +0 -13
- package/src/cli/templates/app/src/setups/exploded-view.ts +0 -246
- package/src/cli/templates/app/src/setups/helper-panel.ts +0 -88
- package/src/cli/templates/app/src/setups/hider.ts +0 -98
- package/src/cli/templates/app/src/setups/index.ts +0 -23
- package/src/cli/templates/app/src/setups/inspection.ts +0 -136
- package/src/cli/templates/app/src/setups/measurement-panel.ts +0 -188
- package/src/cli/templates/app/src/setups/measurement-tool.ts +0 -588
- package/src/cli/templates/app/src/setups/measurements.ts +0 -133
- package/src/cli/templates/app/src/setups/navigation-gizmo.ts +0 -316
- package/src/cli/templates/app/src/setups/plans-panel.ts +0 -619
- package/src/cli/templates/app/src/setups/right-sidebar.ts +0 -178
- package/src/cli/templates/app/src/setups/styles-panel.ts +0 -110
- package/src/cli/templates/app/src/setups/styles.ts +0 -375
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +0 -156
- package/src/cli/templates/app/src/setups/tool-mode.ts +0 -23
- package/src/cli/templates/app/src/setups/toolbar.ts +0 -229
- package/src/cli/templates/app/src/setups/ui-manager.ts +0 -27
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import * as OBC from "@thatopen/components";
|
|
2
|
-
import * as OBF from "@thatopen/components-front";
|
|
3
|
-
import type { ModeTool } from "./tool-mode";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Section / clipping-planes tool. Wraps `OBC.Clipper` as a toolbar mode tool:
|
|
7
|
-
*
|
|
8
|
-
* - double-click on the model → create a clip plane on the surface under the
|
|
9
|
-
* pointer (raycast; the plane's normal is the hit face normal),
|
|
10
|
-
* - drag the plane's arrow gizmo to move it along its normal (handled by the
|
|
11
|
-
* Clipper's built-in TransformControls while the mode is active),
|
|
12
|
-
* - Delete / Backspace → delete the plane under the cursor,
|
|
13
|
-
* - Shift + Delete / Shift + Backspace → clear ALL section planes.
|
|
14
|
-
*
|
|
15
|
-
* Clipping is applied through `renderer.three.clippingPlanes` with
|
|
16
|
-
* `localClippingEnabled = true` (set by the SimpleRenderer), so the cut is
|
|
17
|
-
* honored during the deferred pipeline's single capture render — no
|
|
18
|
-
* deferred-lib change is needed. (Worth an explicit live check that the section
|
|
19
|
-
* actually cuts geometry in deferred mode.)
|
|
20
|
-
*
|
|
21
|
-
* Leaving the mode keeps the existing section planes cutting the model (so a
|
|
22
|
-
* section persists while navigating); it only stops creating/dragging new ones.
|
|
23
|
-
*/
|
|
24
|
-
export const clipper = (
|
|
25
|
-
components: OBC.Components,
|
|
26
|
-
world: OBC.World,
|
|
27
|
-
): ModeTool => {
|
|
28
|
-
const clipperComp = components.get(OBC.Clipper);
|
|
29
|
-
|
|
30
|
-
const canvas = (world.renderer as OBF.PostproductionRenderer | null)?.three
|
|
31
|
-
.domElement;
|
|
32
|
-
if (!canvas) {
|
|
33
|
-
throw new Error("clipper setup: world has no renderer with a canvas");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const onDblClick = () => {
|
|
37
|
-
void clipperComp.create(world);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
41
|
-
if (event.code !== "Delete" && event.code !== "Backspace") return;
|
|
42
|
-
if (event.shiftKey) {
|
|
43
|
-
clipperComp.deleteAll();
|
|
44
|
-
} else {
|
|
45
|
-
// No-op when nothing is hovered (the Clipper raycasts for a plane).
|
|
46
|
-
void clipperComp.delete(world);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
let active = false;
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
label: "Section",
|
|
54
|
-
icon: "mdi:scissors-cutting",
|
|
55
|
-
activate() {
|
|
56
|
-
if (active) return;
|
|
57
|
-
active = true;
|
|
58
|
-
clipperComp.enabled = true; // allow create + drag-to-move
|
|
59
|
-
clipperComp.visible = true;
|
|
60
|
-
canvas.addEventListener("dblclick", onDblClick);
|
|
61
|
-
window.addEventListener("keydown", onKeyDown);
|
|
62
|
-
canvas.style.cursor = "crosshair";
|
|
63
|
-
},
|
|
64
|
-
deactivate() {
|
|
65
|
-
if (!active) return;
|
|
66
|
-
active = false;
|
|
67
|
-
// Stop creating / dragging, but keep the planes in the renderer's
|
|
68
|
-
// clippingPlanes so the section stays cut while navigating.
|
|
69
|
-
clipperComp.enabled = false;
|
|
70
|
-
canvas.removeEventListener("dblclick", onDblClick);
|
|
71
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
72
|
-
canvas.style.cursor = "";
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import * as OBC from "@thatopen/components";
|
|
2
|
-
import { CloudRunner } from "../bim-components";
|
|
3
|
-
import { getUIManager } from "./ui-manager";
|
|
4
|
-
|
|
5
|
-
export const cloudRunner = (components: OBC.Components) => {
|
|
6
|
-
const runner = components.get(CloudRunner);
|
|
7
|
-
const uis = getUIManager(components);
|
|
8
|
-
|
|
9
|
-
// Re-render all appInfoSection instances whenever execution state changes.
|
|
10
|
-
runner.onExecutionUpdated.add(() => {
|
|
11
|
-
uis.custom.get("cloudRunnerSection").updateInstances();
|
|
12
|
-
});
|
|
13
|
-
};
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import * as OBC from "@thatopen/components";
|
|
3
|
-
import * as FRAGS from "@thatopen/fragments";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* EXPLODED VIEW — a HEADLESS controller (no UI). The toolbar wires a single
|
|
7
|
-
* `mdi:arrow-expand-vertical` button to `toggle()` / `isActive()` / `onChange()`.
|
|
8
|
-
*
|
|
9
|
-
* ── Technique ──────────────────────────────────────────────────────────────
|
|
10
|
-
* Fragments has no runtime "translate these items" call; the supported path is
|
|
11
|
-
* the Edit API's GLOBAL TRANSFORMS (each item placement is a global transform
|
|
12
|
-
* with a `position`). So per model we:
|
|
13
|
-
* 1. Group element localIds by IFCBUILDINGSTOREY (from the spatial structure)
|
|
14
|
-
* and order storeys by elevation (min-Y of each group's bbox).
|
|
15
|
-
* 2. Map items → their global-transform ids; a transform shared across MORE
|
|
16
|
-
* than one storey is skipped (it can't be moved per-storey cleanly).
|
|
17
|
-
* 3. SNAPSHOT each transform's original position ONCE.
|
|
18
|
-
* 4. To explode at magnitude m: set each transform's Y to
|
|
19
|
-
* `originalY + storeyIndex × step × m` — ALWAYS absolute from the snapshot,
|
|
20
|
-
* never accumulated, so repeated explode/reset never drifts. m = 0 restores
|
|
21
|
-
* exactly.
|
|
22
|
-
*
|
|
23
|
-
* Edits are applied in the background (the Edit API can't do 60/s). Visibility &
|
|
24
|
-
* selection are untouched (only transforms change).
|
|
25
|
-
*
|
|
26
|
-
* const explode = explodedView(components);
|
|
27
|
-
* // toolbar: button @click=explode.toggle(); active styling = explode.isActive()
|
|
28
|
-
*
|
|
29
|
-
* @param components engine components
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
const STOREY = "IFCBUILDINGSTOREY";
|
|
33
|
-
const DEFAULT_MAGNITUDE = 1.0;
|
|
34
|
-
|
|
35
|
-
export interface ExplodedViewController {
|
|
36
|
-
/** Off→on (applies the default magnitude) / on→off (restores exactly). */
|
|
37
|
-
toggle(): void;
|
|
38
|
-
/** Whether the model is currently exploded (magnitude > 0). */
|
|
39
|
-
isActive(): boolean;
|
|
40
|
-
/** Subscribe to active changes (toggle / auto-reset). Returns an unsubscribe. */
|
|
41
|
-
onChange(cb: (active: boolean) => void): () => void;
|
|
42
|
-
/** Graded control (0→1). Kept for a future popover slider; 0 restores exactly. */
|
|
43
|
-
setMagnitude(v: number): void;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface ModelExplode {
|
|
47
|
-
// global-transform localId → its storey index (0 = lowest). Only single-storey
|
|
48
|
-
// transforms are included.
|
|
49
|
-
gtToStorey: Map<number, number>;
|
|
50
|
-
// global-transform localId → original position [x,y,z] (the snapshot).
|
|
51
|
-
snapshot: Map<number, [number, number, number]>;
|
|
52
|
-
// current global-transform objects (other fields preserved on UPDATE).
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
-
gts: Map<number, any>;
|
|
55
|
-
step: number; // vertical units per storey index at magnitude 1
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const explodedView = (
|
|
59
|
-
components: OBC.Components,
|
|
60
|
-
): ExplodedViewController => {
|
|
61
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
62
|
-
// The Edit API; opt into the in-place incremental edit path (cheaper than a
|
|
63
|
-
// per-edit delta-model rebuild). Harmless if already enabled.
|
|
64
|
-
try {
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
-
(fragments.core as any).settings.incrementalEdit = true;
|
|
67
|
-
} catch {
|
|
68
|
-
/* setting may not exist on older cores — edits still work, just slower */
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let magnitude = 0;
|
|
72
|
-
let active = false;
|
|
73
|
-
let busy = false;
|
|
74
|
-
const prepared = new Map<string, ModelExplode>();
|
|
75
|
-
const listeners = new Set<(active: boolean) => void>();
|
|
76
|
-
|
|
77
|
-
const setActive = (a: boolean) => {
|
|
78
|
-
if (a === active) return;
|
|
79
|
-
active = a;
|
|
80
|
-
for (const cb of listeners) cb(a);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Collect, per storey node, all descendant element localIds.
|
|
84
|
-
const collectStoreys = (root: FRAGS.SpatialTreeItem) => {
|
|
85
|
-
const storeys: number[][] = [];
|
|
86
|
-
const descendants = (node: FRAGS.SpatialTreeItem, acc: number[]) => {
|
|
87
|
-
if (node.localId !== null && node.localId !== undefined && node.localId >= 0) {
|
|
88
|
-
acc.push(node.localId);
|
|
89
|
-
}
|
|
90
|
-
node.children?.forEach((c) => descendants(c, acc));
|
|
91
|
-
};
|
|
92
|
-
const walk = (node: FRAGS.SpatialTreeItem) => {
|
|
93
|
-
if ((node.category ?? "").toUpperCase() === STOREY) {
|
|
94
|
-
const ids: number[] = [];
|
|
95
|
-
descendants(node, ids);
|
|
96
|
-
if (ids.length) storeys.push(ids);
|
|
97
|
-
} else {
|
|
98
|
-
node.children?.forEach(walk);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
walk(root);
|
|
102
|
-
return storeys;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Build the explode plan for one model (snapshot + storey→transform map).
|
|
106
|
-
const prepareModel = async (
|
|
107
|
-
model: FRAGS.FragmentsModel,
|
|
108
|
-
): Promise<ModelExplode | null> => {
|
|
109
|
-
const structure = await model.getSpatialStructure();
|
|
110
|
-
if (!structure) return null;
|
|
111
|
-
let storeys = collectStoreys(structure);
|
|
112
|
-
if (storeys.length < 2) return null; // nothing to separate
|
|
113
|
-
|
|
114
|
-
// Order storeys by elevation (min-Y of the group's merged bbox).
|
|
115
|
-
const withY = await Promise.all(
|
|
116
|
-
storeys.map(async (ids) => {
|
|
117
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
118
|
-
try {
|
|
119
|
-
const boxes = (await fragments.getBBoxes({
|
|
120
|
-
[model.modelId]: new Set(ids),
|
|
121
|
-
})) as THREE.Box3[];
|
|
122
|
-
for (const b of boxes) if (!b.isEmpty()) minY = Math.min(minY, b.min.y);
|
|
123
|
-
} catch {
|
|
124
|
-
/* leave at +Inf → sorts last; harmless */
|
|
125
|
-
}
|
|
126
|
-
return { ids, minY };
|
|
127
|
-
}),
|
|
128
|
-
);
|
|
129
|
-
withY.sort((a, b) => a.minY - b.minY);
|
|
130
|
-
storeys = withY.map((s) => s.ids);
|
|
131
|
-
|
|
132
|
-
// Map global-transform ids → storey index; mark cross-storey ones for skip.
|
|
133
|
-
const gtToStorey = new Map<number, number>();
|
|
134
|
-
const skip = new Set<number>();
|
|
135
|
-
for (let i = 0; i < storeys.length; i += 1) {
|
|
136
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
-
const gtIds: Iterable<number> = await (model as any).getGlobalTranformsIdsOfItems(
|
|
138
|
-
storeys[i],
|
|
139
|
-
);
|
|
140
|
-
for (const gt of gtIds) {
|
|
141
|
-
if (skip.has(gt)) continue;
|
|
142
|
-
if (gtToStorey.has(gt) && gtToStorey.get(gt) !== i) {
|
|
143
|
-
gtToStorey.delete(gt); // shared across storeys → can't move cleanly
|
|
144
|
-
skip.add(gt);
|
|
145
|
-
} else {
|
|
146
|
-
gtToStorey.set(gt, i);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (gtToStorey.size === 0) return null;
|
|
151
|
-
|
|
152
|
-
// Snapshot original positions.
|
|
153
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
154
|
-
const gts: Map<number, any> = await model.getGlobalTransforms();
|
|
155
|
-
const snapshot = new Map<number, [number, number, number]>();
|
|
156
|
-
for (const [id, gt] of gts) {
|
|
157
|
-
if (!gtToStorey.has(id)) continue;
|
|
158
|
-
const p = gt.position as number[];
|
|
159
|
-
snapshot.set(id, [p[0], p[1], p[2]]);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Vertical step per storey index: a fraction of the model's height so the
|
|
163
|
-
// explode reads clearly regardless of model scale.
|
|
164
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
-
const box = (model as any).box as THREE.Box3 | undefined;
|
|
166
|
-
const height = box && !box.isEmpty() ? box.max.y - box.min.y : 10;
|
|
167
|
-
const step = (height * 0.45) / Math.max(1, storeys.length - 1);
|
|
168
|
-
|
|
169
|
-
return { gtToStorey, snapshot, gts, step };
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const rebuild = async () => {
|
|
173
|
-
prepared.clear();
|
|
174
|
-
for (const model of fragments.list.values()) {
|
|
175
|
-
try {
|
|
176
|
-
const plan = await prepareModel(model);
|
|
177
|
-
if (plan) prepared.set(model.modelId, plan);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.warn("[exploded-view] prepare failed", model.modelId, error);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// Re-apply the current magnitude to freshly-loaded models.
|
|
183
|
-
if (magnitude > 0) void apply();
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// Apply the current magnitude across all prepared models. Always sets the
|
|
187
|
-
// absolute position from the snapshot (no accumulation → no drift).
|
|
188
|
-
const apply = async () => {
|
|
189
|
-
if (busy) return;
|
|
190
|
-
busy = true;
|
|
191
|
-
try {
|
|
192
|
-
for (const [modelId, plan] of prepared) {
|
|
193
|
-
const model = fragments.list.get(modelId);
|
|
194
|
-
if (!model) continue;
|
|
195
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
196
|
-
const requests: any[] = [];
|
|
197
|
-
for (const [id, orig] of plan.snapshot) {
|
|
198
|
-
const i = plan.gtToStorey.get(id) ?? 0;
|
|
199
|
-
const gt = plan.gts.get(id);
|
|
200
|
-
if (!gt) continue;
|
|
201
|
-
requests.push({
|
|
202
|
-
type: FRAGS.EditRequestType.UPDATE_GLOBAL_TRANSFORM,
|
|
203
|
-
localId: id,
|
|
204
|
-
data: {
|
|
205
|
-
...gt,
|
|
206
|
-
position: [orig[0], orig[1] + i * plan.step * magnitude, orig[2]],
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
if (requests.length === 0) continue;
|
|
211
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
-
await (fragments.core as any).editor.edit(modelId, requests);
|
|
213
|
-
}
|
|
214
|
-
await fragments.core.update(true);
|
|
215
|
-
} catch (error) {
|
|
216
|
-
console.warn("[exploded-view] apply failed", error);
|
|
217
|
-
} finally {
|
|
218
|
-
busy = false;
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const setMagnitude = (v: number) => {
|
|
223
|
-
magnitude = Math.max(0, Math.min(1, v));
|
|
224
|
-
void apply();
|
|
225
|
-
setActive(magnitude > 0);
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// Recompute the plan whenever the loaded models change.
|
|
229
|
-
fragments.core.onModelLoaded.add(() => void rebuild());
|
|
230
|
-
fragments.list.onItemDeleted.add(() => void rebuild());
|
|
231
|
-
void rebuild();
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
toggle() {
|
|
235
|
-
setMagnitude(active ? 0 : DEFAULT_MAGNITUDE);
|
|
236
|
-
},
|
|
237
|
-
isActive() {
|
|
238
|
-
return active;
|
|
239
|
-
},
|
|
240
|
-
onChange(cb) {
|
|
241
|
-
listeners.add(cb);
|
|
242
|
-
return () => listeners.delete(cb);
|
|
243
|
-
},
|
|
244
|
-
setMagnitude,
|
|
245
|
-
};
|
|
246
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import * as BUI from "@thatopen/ui";
|
|
2
|
-
import { cardHeader } from "./card-header";
|
|
3
|
-
import { toolPlaceholderUri } from "../assets/tool-placeholder";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A contextual helper card — ALWAYS present in the left stack (see left-sidebar).
|
|
7
|
-
* It renders the options of whichever panel-tool is active (e.g. Styles); when
|
|
8
|
-
* no tool is active it shows an empty-state illustration + hint. Mirrors the
|
|
9
|
-
* standardized card chrome: dark `bg-base`, `cardHeader` (with divider) fixed,
|
|
10
|
-
* inset `.helper-scroll` `overflow-y:auto` (same scrollbar as the others).
|
|
11
|
-
*
|
|
12
|
-
* Returns the panel ELEMENT (mounted by the left stack) plus a controller:
|
|
13
|
-
* show({ title, icon, render }) — show a tool's content (+ retitle the header)
|
|
14
|
-
* clear() — revert to the empty state
|
|
15
|
-
* refresh() — re-run the current `render`
|
|
16
|
-
*/
|
|
17
|
-
export interface HelperContent {
|
|
18
|
-
title: string;
|
|
19
|
-
icon: string;
|
|
20
|
-
render: () => unknown;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface HelperPanelController {
|
|
24
|
-
element: HTMLElement;
|
|
25
|
-
show(content: HelperContent): void;
|
|
26
|
-
clear(): void;
|
|
27
|
-
refresh(): void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const DEFAULT = { title: "Tools", icon: "mdi:tune" };
|
|
31
|
-
|
|
32
|
-
const emptyState = () => BUI.html`
|
|
33
|
-
<div style="
|
|
34
|
-
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
35
|
-
gap: 0.75rem; height: 100%; min-height: 12rem; padding: 1rem; text-align: center;
|
|
36
|
-
">
|
|
37
|
-
<img src=${toolPlaceholderUri} alt="" style="width: 7.5rem; height: auto; opacity: 0.9;" />
|
|
38
|
-
<bim-label style="opacity: 0.55; white-space: normal;">Select a tool to see it here.</bim-label>
|
|
39
|
-
</div>
|
|
40
|
-
`;
|
|
41
|
-
|
|
42
|
-
export const helperPanel = (): HelperPanelController => {
|
|
43
|
-
let renderContent: (() => unknown) | null = null;
|
|
44
|
-
|
|
45
|
-
const [panel, update] = BUI.Component.create<BUI.Panel, { title: string; icon: string }>(
|
|
46
|
-
(state) => BUI.html`
|
|
47
|
-
<bim-panel
|
|
48
|
-
label=${state.title}
|
|
49
|
-
icon=${state.icon}
|
|
50
|
-
style="width: 100%; height: 100%; pointer-events: auto;"
|
|
51
|
-
>
|
|
52
|
-
<style>
|
|
53
|
-
.helper-scroll::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; }
|
|
54
|
-
.helper-scroll::-webkit-scrollbar-thumb { border-radius: 0.25rem; background-color: var(--bim-scrollbar--c, #3C3C41); }
|
|
55
|
-
.helper-scroll::-webkit-scrollbar-track { background-color: var(--bim-scrollbar--bgc, var(--bim-ui_bg-contrast-10)); }
|
|
56
|
-
</style>
|
|
57
|
-
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
58
|
-
<div style="flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0.4rem 0.4rem 0.4rem 1.1rem;">
|
|
59
|
-
<!-- Scroll container: scrollbar sits inset from the card (left
|
|
60
|
-
wrapper pad). Inner content has right padding so it clears the
|
|
61
|
-
scrollbar (gap on both sides), mirroring the tree. -->
|
|
62
|
-
<div class="helper-scroll" style="flex: 1 1 auto; min-height: 0; overflow-y: auto;">
|
|
63
|
-
<div style="padding-right: 0.6rem;">
|
|
64
|
-
${renderContent ? renderContent() : emptyState()}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</bim-panel>
|
|
70
|
-
`,
|
|
71
|
-
{ ...DEFAULT },
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
element: panel,
|
|
76
|
-
show({ title, icon, render }) {
|
|
77
|
-
renderContent = render;
|
|
78
|
-
update({ title, icon });
|
|
79
|
-
},
|
|
80
|
-
clear() {
|
|
81
|
-
renderContent = null;
|
|
82
|
-
update({ ...DEFAULT });
|
|
83
|
-
},
|
|
84
|
-
refresh() {
|
|
85
|
-
update({}); // re-runs the template → re-calls renderContent()
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import * as OBC from "@thatopen/components";
|
|
2
|
-
import * as OBF from "@thatopen/components-front";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Visibility actions driven by the CURRENT selection (the Highlighter "select"
|
|
6
|
-
* set that the tree/viewport populate). Returns a controller for the toolbar to
|
|
7
|
-
* wire to buttons — it does not render any UI itself.
|
|
8
|
-
*
|
|
9
|
-
* Suggested icons (ACTION buttons):
|
|
10
|
-
* - hideSelected → "mdi:eye-off-outline"
|
|
11
|
-
* - isolateSelected → "mdi:select-search" (alt: "mdi:eye")
|
|
12
|
-
* - showAll → "mdi:eye-outline" (alt: "mdi:restore")
|
|
13
|
-
*/
|
|
14
|
-
export interface HiderController {
|
|
15
|
-
/** Hide the currently selected items. No-op if nothing is selected. */
|
|
16
|
-
hideSelected(): void;
|
|
17
|
-
/** Hide everything except the current selection. No-op if nothing is selected. */
|
|
18
|
-
isolateSelected(): void;
|
|
19
|
-
/** Reset: make every item visible again. */
|
|
20
|
-
showAll(): void;
|
|
21
|
-
/**
|
|
22
|
-
* Ghost (x-ray) the current selection via the scalable per-element GPU state
|
|
23
|
-
* texture (`model.setGhostItems`) — NO per-item recolor, so it scales to
|
|
24
|
-
* millions of elements. No-op if nothing is selected.
|
|
25
|
-
*/
|
|
26
|
-
ghostSelected(): void;
|
|
27
|
-
/** Clear the ghost (x-ray) overlay on all loaded models. */
|
|
28
|
-
clearGhost(): void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const hider = (components: OBC.Components): HiderController => {
|
|
32
|
-
const hiderComp = components.get(OBC.Hider);
|
|
33
|
-
const highlighter = components.get(OBF.Highlighter);
|
|
34
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
35
|
-
|
|
36
|
-
const currentSelection = (): OBC.ModelIdMap | undefined =>
|
|
37
|
-
highlighter.selection.select;
|
|
38
|
-
const hasItems = (map?: OBC.ModelIdMap) =>
|
|
39
|
-
!!map && Object.values(map).some((set) => set.size > 0);
|
|
40
|
-
|
|
41
|
-
// Making items visible must also un-ghost them — a shown element should never
|
|
42
|
-
// linger as a ghost. unsetGhostItems is async (localId→itemId worker
|
|
43
|
-
// conversion), so collect per-model tasks and update once they all land.
|
|
44
|
-
const unghost = (map: OBC.ModelIdMap) => {
|
|
45
|
-
const tasks: Promise<unknown>[] = [];
|
|
46
|
-
for (const [modelId, set] of Object.entries(map)) {
|
|
47
|
-
const model = fragments.list.get(modelId);
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
-
if (model && set.size > 0) tasks.push((model as any).unsetGhostItems?.([...set]));
|
|
50
|
-
}
|
|
51
|
-
if (tasks.length) void Promise.all(tasks).then(() => fragments.core.update(true));
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const clearGhostAll = () => {
|
|
55
|
-
for (const model of fragments.list.values()) {
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
-
(model as any).clearGhost?.();
|
|
58
|
-
}
|
|
59
|
-
void fragments.core.update(true);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
hideSelected() {
|
|
64
|
-
const sel = currentSelection();
|
|
65
|
-
if (!sel || !hasItems(sel)) return;
|
|
66
|
-
void hiderComp.set(false, sel);
|
|
67
|
-
},
|
|
68
|
-
isolateSelected() {
|
|
69
|
-
const sel = currentSelection();
|
|
70
|
-
if (!sel || !hasItems(sel)) return;
|
|
71
|
-
void hiderComp.isolate(sel);
|
|
72
|
-
// The selection is the only thing visible now → un-ghost it so an isolated
|
|
73
|
-
// element is never left ghosted.
|
|
74
|
-
unghost(sel);
|
|
75
|
-
},
|
|
76
|
-
showAll() {
|
|
77
|
-
void hiderComp.set(true);
|
|
78
|
-
// Everything is visible again → nothing should stay ghosted.
|
|
79
|
-
clearGhostAll();
|
|
80
|
-
},
|
|
81
|
-
ghostSelected() {
|
|
82
|
-
const sel = currentSelection();
|
|
83
|
-
if (!sel || !hasItems(sel)) return;
|
|
84
|
-
const tasks: Promise<unknown>[] = [];
|
|
85
|
-
for (const [modelId, set] of Object.entries(sel)) {
|
|
86
|
-
const model = fragments.list.get(modelId);
|
|
87
|
-
// setGhostItems is async (localId→itemId worker conversion) — update
|
|
88
|
-
// only after every model's ghost state is written.
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
-
if (model && set.size > 0) tasks.push((model as any).setGhostItems([...set], false));
|
|
91
|
-
}
|
|
92
|
-
void Promise.all(tasks).then(() => fragments.core.update(true));
|
|
93
|
-
},
|
|
94
|
-
clearGhost() {
|
|
95
|
-
clearGhostAll();
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export * from "./ui-manager";
|
|
2
|
-
export * from "./cloud-runner";
|
|
3
|
-
export * from "./right-sidebar";
|
|
4
|
-
export * from "./card-header";
|
|
5
|
-
export * from "./toolbar";
|
|
6
|
-
export * from "./hider";
|
|
7
|
-
export * from "./tool-mode";
|
|
8
|
-
export * from "./clipper";
|
|
9
|
-
export * from "./measurements";
|
|
10
|
-
export * from "./styles";
|
|
11
|
-
export * from "./helper-panel";
|
|
12
|
-
export * from "./styles-panel";
|
|
13
|
-
export * from "./clipper-tool";
|
|
14
|
-
export * from "./plans-panel";
|
|
15
|
-
export * from "./navigation-gizmo";
|
|
16
|
-
export * from "./exploded-view";
|
|
17
|
-
export * from "./measurement-tool";
|
|
18
|
-
export * from "./measurement-panel";
|
|
19
|
-
export * from "./tool-mode-manager";
|
|
20
|
-
// reality-capture-viewer is intentionally NOT re-exported here: it spins up
|
|
21
|
-
// decode workers at module load, so it must be LAZY-imported (dynamic import in
|
|
22
|
-
// main.ts's app-registered .3tz loader for <top-models-list>), never pulled onto
|
|
23
|
-
// the app boot path.
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import * as OBC from "@thatopen/components";
|
|
2
|
-
import type { ClipperTool } from "./clipper-tool";
|
|
3
|
-
import type { MeasurementTool } from "./measurement-tool";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Unified "Objects" outliner API (W2's new panel). Enumerates every created clip
|
|
7
|
-
* plane and every measurement as a flat list of {@link InstanceRow}s with
|
|
8
|
-
* per-instance HIDE / DISABLE / DELETE, so W2 can render rows without knowing
|
|
9
|
-
* about the individual Clipping / Measurement tools.
|
|
10
|
-
*
|
|
11
|
-
* Shapes are owned here and produced by each tool's `instances()`; this module
|
|
12
|
-
* just merges them and fans the two tools' change events into one.
|
|
13
|
-
*/
|
|
14
|
-
export type InstanceKind = "clip" | "measurement";
|
|
15
|
-
|
|
16
|
-
export interface InstanceRow {
|
|
17
|
-
/** Stable id (plane id for clips; per-measurement id) — use as the row key. */
|
|
18
|
-
id: string;
|
|
19
|
-
kind: InstanceKind;
|
|
20
|
-
/** "Clip plane" | "Length" | "Area" | "Angle". */
|
|
21
|
-
type: string;
|
|
22
|
-
/** User-facing row label (includes the measured value for measurements). */
|
|
23
|
-
label: string;
|
|
24
|
-
/** Currently shown in the scene. */
|
|
25
|
-
visible: boolean;
|
|
26
|
-
/** Clip planes only: whether the plane is actively cutting. `undefined` for measurements. */
|
|
27
|
-
enabled?: boolean;
|
|
28
|
-
/** Show/hide this instance without deleting it. */
|
|
29
|
-
setVisible(on: boolean): void;
|
|
30
|
-
/** Clip planes only: toggle the cut on/off (present only when `enabled` is defined). */
|
|
31
|
-
setEnabled?(on: boolean): void;
|
|
32
|
-
/** Permanently remove this instance. */
|
|
33
|
-
remove(): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface InspectionInstances {
|
|
37
|
-
/** Snapshot of all clip planes + measurements, in that order. */
|
|
38
|
-
list(): InstanceRow[];
|
|
39
|
-
/** Fires whenever the set or state of instances changes (re-query `list()`). */
|
|
40
|
-
readonly onChanged: OBC.Event<void>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* A toolbar action W2 wires into the new "Inspection" tab. Each `activate()`
|
|
45
|
-
* routes through the toolModeManager (exclusive — entering one exits any other
|
|
46
|
-
* tool). W2 drives the Select button + active highlighting via
|
|
47
|
-
* `toolModeManager.selectMode()` / `getActiveId()` / `onActiveChanged`; each
|
|
48
|
-
* action's own `isActive()` is provided for convenience.
|
|
49
|
-
*/
|
|
50
|
-
export interface InspectionAction {
|
|
51
|
-
id: string;
|
|
52
|
-
label: string;
|
|
53
|
-
/** mdi icon name (e.g. "mdi:ruler"). */
|
|
54
|
-
icon: string;
|
|
55
|
-
activate(): void;
|
|
56
|
-
isActive(): boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Inspection toolbar actions: clip plane + the measurement types/modes. Order is
|
|
61
|
-
* the toolbar order. measure-edge/face reuse Length/Area with a forced sub-mode;
|
|
62
|
-
* measure-length/area are the plain "free" modes (isActive distinguishes them by
|
|
63
|
-
* sub-mode). Volume is a distinct measurer.
|
|
64
|
-
*/
|
|
65
|
-
export const inspectionActions = (
|
|
66
|
-
clipperTool: ClipperTool,
|
|
67
|
-
measurementTool: MeasurementTool,
|
|
68
|
-
): InspectionAction[] => {
|
|
69
|
-
const m = measurementTool;
|
|
70
|
-
const isMeasure = (mode: string, sub: string) =>
|
|
71
|
-
m.getMode() === mode && m.getSubMode() === sub;
|
|
72
|
-
return [
|
|
73
|
-
{
|
|
74
|
-
id: "clip-plane",
|
|
75
|
-
label: "Clip plane",
|
|
76
|
-
icon: "mdi:scissors-cutting",
|
|
77
|
-
activate: () => clipperTool.setPlacing(true),
|
|
78
|
-
isActive: () => clipperTool.isPlacing(),
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: "measure-length",
|
|
82
|
-
label: "Length",
|
|
83
|
-
icon: "mdi:ruler",
|
|
84
|
-
activate: () => m.setMode("length", "free"),
|
|
85
|
-
isActive: () => isMeasure("length", "free"),
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: "measure-area",
|
|
89
|
-
label: "Area",
|
|
90
|
-
icon: "mdi:vector-polygon",
|
|
91
|
-
activate: () => m.setMode("area", "free"),
|
|
92
|
-
isActive: () => isMeasure("area", "free"),
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: "measure-angle",
|
|
96
|
-
label: "Angle",
|
|
97
|
-
icon: "mdi:angle-acute",
|
|
98
|
-
activate: () => m.setMode("angle"),
|
|
99
|
-
isActive: () => m.getMode() === "angle",
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
id: "measure-edge",
|
|
103
|
-
label: "Edge",
|
|
104
|
-
icon: "mdi:vector-line",
|
|
105
|
-
activate: () => m.setMode("length", "edge"),
|
|
106
|
-
isActive: () => isMeasure("length", "edge"),
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
id: "measure-face",
|
|
110
|
-
label: "Face",
|
|
111
|
-
icon: "mdi:vector-square",
|
|
112
|
-
activate: () => m.setMode("area", "face"),
|
|
113
|
-
isActive: () => isMeasure("area", "face"),
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
id: "volume",
|
|
117
|
-
label: "Volume",
|
|
118
|
-
icon: "mdi:cube-outline",
|
|
119
|
-
activate: () => m.setMode("volume"),
|
|
120
|
-
isActive: () => m.getMode() === "volume",
|
|
121
|
-
},
|
|
122
|
-
];
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
export const inspectionInstances = (
|
|
126
|
-
clipperTool: ClipperTool,
|
|
127
|
-
measurementTool: MeasurementTool,
|
|
128
|
-
): InspectionInstances => {
|
|
129
|
-
const onChanged = new OBC.Event<void>();
|
|
130
|
-
clipperTool.onChanged.add(() => onChanged.trigger());
|
|
131
|
-
measurementTool.onChanged.add(() => onChanged.trigger());
|
|
132
|
-
return {
|
|
133
|
-
list: () => [...clipperTool.instances(), ...measurementTool.instances()],
|
|
134
|
-
onChanged,
|
|
135
|
-
};
|
|
136
|
-
};
|