@thatopen/services 0.2.0 → 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 -59
- package/dist/built-in/index.d.ts +486 -563
- package/dist/cli.js +10 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +3 -5
- 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 -5
- 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
|
@@ -0,0 +1,98 @@
|
|
|
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,3 +1,33 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
1
|
+
export * from "./ui-manager";
|
|
2
|
+
export * from "./cloud-runner";
|
|
3
|
+
export * from "./properties-panel";
|
|
4
|
+
export * from "./fps-indicator";
|
|
5
|
+
export * from "./active-tool-hud";
|
|
6
|
+
export * from "./files-panel";
|
|
7
|
+
export * from "./model-tree";
|
|
8
|
+
export * from "./right-sidebar";
|
|
9
|
+
export * from "./card-header";
|
|
10
|
+
export * from "./toolbar";
|
|
11
|
+
export * from "./visibility-toolbar";
|
|
12
|
+
export * from "./hider";
|
|
13
|
+
export * from "./tool-mode";
|
|
14
|
+
export * from "./clipper";
|
|
15
|
+
export * from "./measurements";
|
|
16
|
+
export * from "./styles";
|
|
17
|
+
export * from "./helper-panel";
|
|
18
|
+
export * from "./styles-panel";
|
|
19
|
+
export * from "./graphics-panel";
|
|
20
|
+
export * from "./commands-panel";
|
|
21
|
+
export * from "./clipper-tool";
|
|
22
|
+
export * from "./clipper-panel";
|
|
23
|
+
export * from "./plans-panel";
|
|
24
|
+
export * from "./navigation-gizmo";
|
|
25
|
+
export * from "./exploded-view";
|
|
26
|
+
export * from "./measurement-tool";
|
|
27
|
+
export * from "./measurement-panel";
|
|
28
|
+
export * from "./data-table-panel";
|
|
29
|
+
export * from "./walkthrough";
|
|
30
|
+
export * from "./tool-mode-manager";
|
|
31
|
+
// reality-capture-viewer is intentionally NOT re-exported here: it spins up
|
|
32
|
+
// decode workers at module load, so it must be LAZY-imported (dynamic import in
|
|
33
|
+
// files-panel's .3tz "View" handler), never pulled onto the app boot path.
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as OBC from "@thatopen/components";
|
|
2
|
+
import * as BUI from "@thatopen/ui";
|
|
3
|
+
import type {
|
|
4
|
+
MeasurementTool,
|
|
5
|
+
MeasureMode,
|
|
6
|
+
SnapKind,
|
|
7
|
+
} from "./measurement-tool";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MEASUREMENT panel — a docked side-panel (a layout alongside
|
|
11
|
+
* Explorer/Files/Graphics/Clipping) for length / area / angle measurements.
|
|
12
|
+
*
|
|
13
|
+
* Sections:
|
|
14
|
+
* - Measure — exclusive mode buttons (Off / Length / Area / Angle); pick one,
|
|
15
|
+
* then double-click surfaces in the viewport (Area: Enter to close).
|
|
16
|
+
* - Snapping — vertices / edges / faces toggles (precise picks).
|
|
17
|
+
* - Measurements — show/hide all, clear all, and a list of every measurement
|
|
18
|
+
* (type + value + units) with delete-per-row.
|
|
19
|
+
*
|
|
20
|
+
* Vanilla BUI, app chrome (native panel header, muted bands, 1px contrast-20
|
|
21
|
+
* hairlines, #3C3C41 scrollbar, library toggle switches + buttons). Factory
|
|
22
|
+
* returns the element WITHOUT self-mounting. (A simple row list is used for the
|
|
23
|
+
* measurements, matching files-panel / clipper-panel, so each row can carry a
|
|
24
|
+
* delete button.)
|
|
25
|
+
*/
|
|
26
|
+
const MODES: { mode: MeasureMode; label: string; icon: string }[] = [
|
|
27
|
+
{ mode: "none", label: "Off", icon: "mdi:cursor-default" },
|
|
28
|
+
{ mode: "length", label: "Length", icon: "mdi:ruler" },
|
|
29
|
+
{ mode: "area", label: "Area", icon: "mdi:vector-square" },
|
|
30
|
+
{ mode: "angle", label: "Angle", icon: "mdi:angle-acute" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const SNAPS: { kind: SnapKind; label: string }[] = [
|
|
34
|
+
{ kind: "point", label: "Vertices" },
|
|
35
|
+
{ kind: "line", label: "Edges" },
|
|
36
|
+
{ kind: "face", label: "Faces" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const measurementPanel = (
|
|
40
|
+
_components: OBC.Components,
|
|
41
|
+
tool: MeasurementTool,
|
|
42
|
+
) => {
|
|
43
|
+
const [panel, update] = BUI.Component.create<BUI.Panel, { tick: number }>(
|
|
44
|
+
(state) => {
|
|
45
|
+
const refresh = () => update({ tick: state.tick + 1 });
|
|
46
|
+
|
|
47
|
+
const mode = tool.getMode();
|
|
48
|
+
const rows = tool.rows();
|
|
49
|
+
const snaps = tool.getSnaps();
|
|
50
|
+
const visible = tool.isVisible();
|
|
51
|
+
|
|
52
|
+
const modeButtons = MODES.map(
|
|
53
|
+
(m) => BUI.html`
|
|
54
|
+
<bim-button
|
|
55
|
+
class="m-mode"
|
|
56
|
+
label=${m.label}
|
|
57
|
+
icon=${m.icon}
|
|
58
|
+
?active=${mode === m.mode}
|
|
59
|
+
@click=${() => {
|
|
60
|
+
tool.setMode(m.mode);
|
|
61
|
+
refresh();
|
|
62
|
+
}}
|
|
63
|
+
></bim-button>
|
|
64
|
+
`,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const snapRows = SNAPS.map(
|
|
68
|
+
(s) => BUI.html`
|
|
69
|
+
<div class="m-row">
|
|
70
|
+
<span class="m-k">${s.label}</span>
|
|
71
|
+
<span class="m-v">
|
|
72
|
+
<bim-checkbox class="m-toggle" toggle ?checked=${snaps[s.kind]}
|
|
73
|
+
@change=${(e: Event) => {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
tool.setSnap(s.kind, !!(e.target as any).checked);
|
|
76
|
+
refresh();
|
|
77
|
+
}}></bim-checkbox>
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const measurementRows =
|
|
84
|
+
rows.length === 0
|
|
85
|
+
? BUI.html`<div class="m-empty">No measurements. Pick a mode above, then double-click surfaces.</div>`
|
|
86
|
+
: rows.map(
|
|
87
|
+
(r) => BUI.html`
|
|
88
|
+
<div class="m-row">
|
|
89
|
+
<span class="m-k m-type">${r.type}</span>
|
|
90
|
+
<span class="m-v">
|
|
91
|
+
<span class="m-val">${r.text}</span>
|
|
92
|
+
<bim-button class="m-del" icon="mdi:trash-can-outline"
|
|
93
|
+
@click=${() => {
|
|
94
|
+
r.remove();
|
|
95
|
+
refresh();
|
|
96
|
+
}}></bim-button>
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return BUI.html`
|
|
103
|
+
<bim-panel label="Measure" icon="mdi:ruler"
|
|
104
|
+
style="width: 100%; height: 100%; pointer-events: auto;">
|
|
105
|
+
<style>
|
|
106
|
+
.m-vp::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; }
|
|
107
|
+
.m-vp::-webkit-scrollbar-thumb { border-radius: 0.25rem; background-color: var(--bim-scrollbar--c, #3C3C41); }
|
|
108
|
+
.m-vp::-webkit-scrollbar-track { background-color: var(--bim-scrollbar--bgc, var(--bim-ui_bg-contrast-10)); }
|
|
109
|
+
|
|
110
|
+
.m-band { box-sizing: border-box; padding: 0.4rem 0.4rem 0.4rem 1.1rem;
|
|
111
|
+
display: flex; align-items: center; gap: 0.4rem;
|
|
112
|
+
font-size: 0.76rem; font-weight: 500; letter-spacing: 0.01em;
|
|
113
|
+
color: var(--bim-ui_bg-contrast-70, #a7a7ab);
|
|
114
|
+
background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
115
|
+
border-top: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
116
|
+
border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
|
|
117
|
+
.m-band-ico { flex: 0 0 auto; font-size: 0.9rem; color: var(--bim-ui_bg-contrast-70, #a7a7ab); }
|
|
118
|
+
.m-band:first-child { border-top: none; }
|
|
119
|
+
|
|
120
|
+
.m-modes { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem;
|
|
121
|
+
padding: 0.5rem 0.4rem 0.5rem 1.1rem; }
|
|
122
|
+
.m-modes bim-button { width: 100%; }
|
|
123
|
+
|
|
124
|
+
.m-row { box-sizing: border-box; min-height: 32px; display: flex;
|
|
125
|
+
align-items: center; justify-content: space-between; gap: 0.6rem;
|
|
126
|
+
padding: 0.35rem 0.4rem 0.35rem 1.1rem; font-size: 0.78rem;
|
|
127
|
+
color: var(--bim-ui_bg-contrast-100, #e3e3e3);
|
|
128
|
+
border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
|
|
129
|
+
.m-k { opacity: 0.6; flex: 0 1 auto; min-width: 0; white-space: nowrap;
|
|
130
|
+
overflow: hidden; text-overflow: ellipsis; }
|
|
131
|
+
.m-type { opacity: 0.85; }
|
|
132
|
+
.m-v { flex: 1 1 auto; min-width: 0; display: flex; align-items: center;
|
|
133
|
+
justify-content: flex-end; gap: 0.5rem; }
|
|
134
|
+
.m-val { font-variant-numeric: tabular-nums; }
|
|
135
|
+
.m-row bim-checkbox.m-toggle { flex: 0 0 2rem; width: 2rem; }
|
|
136
|
+
.m-row bim-button.m-del { flex: 0 0 auto; --bim-button--bgc: transparent; }
|
|
137
|
+
|
|
138
|
+
.m-empty { padding: 0.6rem 0.4rem 0.6rem 1.1rem; opacity: 0.6;
|
|
139
|
+
font-size: 0.78rem; line-height: 1.3; white-space: normal; }
|
|
140
|
+
|
|
141
|
+
.m-actions { display: flex; flex-direction: column; gap: 0.4rem;
|
|
142
|
+
padding: 0.5rem 0.4rem 0.5rem 1.1rem; }
|
|
143
|
+
.m-actions bim-button { width: 100%; }
|
|
144
|
+
</style>
|
|
145
|
+
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
|
146
|
+
<div style="flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0 0.4rem 0.4rem 1.1rem;">
|
|
147
|
+
<div class="m-vp" style="flex: 1 1 auto; min-height: 0; overflow-y: auto;
|
|
148
|
+
margin-left: -1.1rem; margin-right: -0.4rem;">
|
|
149
|
+
|
|
150
|
+
<div class="m-modes">${modeButtons}</div>
|
|
151
|
+
|
|
152
|
+
<div class="m-band"><bim-icon class="m-band-ico" icon="mdi:magnet"></bim-icon>Snapping</div>
|
|
153
|
+
${snapRows}
|
|
154
|
+
|
|
155
|
+
<div class="m-band"><bim-icon class="m-band-ico" icon="mdi:format-list-bulleted"></bim-icon>Measurements</div>
|
|
156
|
+
<div class="m-row">
|
|
157
|
+
<span class="m-k">Show all</span>
|
|
158
|
+
<span class="m-v">
|
|
159
|
+
<bim-checkbox class="m-toggle" toggle ?checked=${visible}
|
|
160
|
+
@change=${(e: Event) => {
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
162
|
+
tool.setVisible(!!(e.target as any).checked);
|
|
163
|
+
refresh();
|
|
164
|
+
}}></bim-checkbox>
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="m-actions">
|
|
168
|
+
<bim-button label="Clear all" icon="mdi:trash-can-outline" ?disabled=${rows.length === 0}
|
|
169
|
+
@click=${() => {
|
|
170
|
+
tool.clearAll();
|
|
171
|
+
refresh();
|
|
172
|
+
}}></bim-button>
|
|
173
|
+
</div>
|
|
174
|
+
${measurementRows}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</bim-panel>
|
|
179
|
+
`;
|
|
180
|
+
},
|
|
181
|
+
{ tick: 0 },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
tool.onChanged.add(() => update({ tick: 0 }));
|
|
185
|
+
tool.onModeChanged.add(() => update({ tick: 0 }));
|
|
186
|
+
|
|
187
|
+
return panel;
|
|
188
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as BUI from "@thatopen/ui";
|
|
2
|
+
import type { MeasurementTool } from "./measurement-tool";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MEASUREMENT settings section for the merged Settings layout (UI-reorg). Color,
|
|
6
|
+
* per-type units, rounding, snap toggles, and global visibility — all driven by
|
|
7
|
+
* W1's measurementTool settings API (getMeasurementSettings + setters), re-read
|
|
8
|
+
* on `onChanged`. (Line thickness lands later with the LineSegments2 conversion.)
|
|
9
|
+
* Returns its `bim-panel` WITHOUT self-mounting.
|
|
10
|
+
*
|
|
11
|
+
* @param tool the measurement tool (worker 1)
|
|
12
|
+
* @returns the `bim-panel` element
|
|
13
|
+
*/
|
|
14
|
+
export const measurementSettingsPanel = (tool: MeasurementTool) => {
|
|
15
|
+
const dropdown = (
|
|
16
|
+
opts: string[],
|
|
17
|
+
selected: string,
|
|
18
|
+
onPick: (v: string) => void,
|
|
19
|
+
) => BUI.html`
|
|
20
|
+
<bim-dropdown
|
|
21
|
+
style="flex: 1 1 auto; min-width: 0;"
|
|
22
|
+
@change=${(e: Event) => {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
const v = (e.target as any).value?.[0];
|
|
25
|
+
if (v != null) onPick(String(v));
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
${opts.map((o) => BUI.html`<bim-option .value=${o} label=${o} ?checked=${o === selected}></bim-option>`)}
|
|
29
|
+
</bim-dropdown>`;
|
|
30
|
+
|
|
31
|
+
const [panel, update] = BUI.Component.create<BUI.Panel, { tick: number }>(
|
|
32
|
+
// NOTE: the callback MUST take a state param (arity >= 1) — an arity-0
|
|
33
|
+
// callback makes Component.create return a single element instead of
|
|
34
|
+
// [el, update], and the destructure throws "object is not iterable".
|
|
35
|
+
(_s) => {
|
|
36
|
+
// Fail-safe: a settings sub-panel must NEVER crash main(). If the tool's
|
|
37
|
+
// settings API is unavailable/throws, render a placeholder instead.
|
|
38
|
+
let s: ReturnType<MeasurementTool["getMeasurementSettings"]> | null = null;
|
|
39
|
+
try {
|
|
40
|
+
s = tool.getMeasurementSettings?.() ?? null;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn("[measurement-settings] getMeasurementSettings failed", error);
|
|
43
|
+
}
|
|
44
|
+
if (!s || !s.unitOptions || !s.units || !s.snaps) {
|
|
45
|
+
return BUI.html`
|
|
46
|
+
<bim-panel label="Measurement" icon="mdi:ruler" style="width: 100%; height: 100%; pointer-events: auto;">
|
|
47
|
+
<div style="padding: 0.9rem 1.1rem; font-size: 0.78rem; color: var(--bim-ui_bg-contrast-70, #a7a7ab);">
|
|
48
|
+
Measurement settings unavailable.
|
|
49
|
+
</div>
|
|
50
|
+
</bim-panel>`;
|
|
51
|
+
}
|
|
52
|
+
const unitRow = (label: string, type: "length" | "area" | "angle") => BUI.html`
|
|
53
|
+
<div class="ms-row">
|
|
54
|
+
<span class="ms-lbl">${label}</span>
|
|
55
|
+
${dropdown(s.unitOptions[type], s.units[type], (v) => tool.setUnits(type, v))}
|
|
56
|
+
</div>`;
|
|
57
|
+
return BUI.html`
|
|
58
|
+
<bim-panel
|
|
59
|
+
label="Measurement"
|
|
60
|
+
icon="mdi:ruler"
|
|
61
|
+
style="width: 100%; height: 100%; pointer-events: auto;"
|
|
62
|
+
>
|
|
63
|
+
<style>
|
|
64
|
+
.ms-body { display: flex; flex-direction: column; gap: 0.45rem;
|
|
65
|
+
padding: 0.6rem 0.75rem; overflow-y: auto; height: 100%; }
|
|
66
|
+
.ms-row { display: flex; align-items: center; gap: 0.5rem; }
|
|
67
|
+
.ms-toggle { cursor: pointer; justify-content: space-between; }
|
|
68
|
+
.ms-lbl { font-size: 0.72rem; white-space: nowrap; flex: 0 0 5.5rem;
|
|
69
|
+
color: var(--bim-ui_bg-contrast-80, #c9c9c9); }
|
|
70
|
+
.ms-snaps { display: flex; gap: 0.9rem; flex-wrap: wrap; }
|
|
71
|
+
.ms-snap { display: inline-flex; align-items: center; gap: 0.3rem;
|
|
72
|
+
cursor: pointer; font-size: 0.72rem;
|
|
73
|
+
color: var(--bim-ui_bg-contrast-80, #c9c9c9); }
|
|
74
|
+
</style>
|
|
75
|
+
<div class="ms-body">
|
|
76
|
+
<div class="ms-row">
|
|
77
|
+
<span class="ms-lbl">Color</span>
|
|
78
|
+
<bim-color-input
|
|
79
|
+
color=${s.color}
|
|
80
|
+
@input=${(e: Event) => {
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
const t = e.target as any;
|
|
83
|
+
tool.setColor(String(t.color ?? t.value));
|
|
84
|
+
}}
|
|
85
|
+
></bim-color-input>
|
|
86
|
+
</div>
|
|
87
|
+
${unitRow("Length", "length")}
|
|
88
|
+
${unitRow("Area", "area")}
|
|
89
|
+
${unitRow("Angle", "angle")}
|
|
90
|
+
<div class="ms-row">
|
|
91
|
+
<span class="ms-lbl">Decimals</span>
|
|
92
|
+
<bim-number-input
|
|
93
|
+
slider
|
|
94
|
+
value=${s.rounding}
|
|
95
|
+
min="0"
|
|
96
|
+
max="6"
|
|
97
|
+
step="1"
|
|
98
|
+
style="flex: 1 1 auto;"
|
|
99
|
+
@change=${(e: Event) => {
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
tool.setRounding(Number((e.target as any).value));
|
|
102
|
+
}}
|
|
103
|
+
></bim-number-input>
|
|
104
|
+
</div>
|
|
105
|
+
${
|
|
106
|
+
typeof s.thickness === "number" && typeof tool.setThickness === "function"
|
|
107
|
+
? BUI.html`
|
|
108
|
+
<div class="ms-row">
|
|
109
|
+
<span class="ms-lbl">Thickness</span>
|
|
110
|
+
<bim-number-input
|
|
111
|
+
slider
|
|
112
|
+
value=${s.thickness}
|
|
113
|
+
min="1"
|
|
114
|
+
max="10"
|
|
115
|
+
step="1"
|
|
116
|
+
style="flex: 1 1 auto;"
|
|
117
|
+
@change=${(e: Event) => {
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
+
tool.setThickness(Number((e.target as any).value));
|
|
120
|
+
}}
|
|
121
|
+
></bim-number-input>
|
|
122
|
+
</div>`
|
|
123
|
+
: BUI.html``
|
|
124
|
+
}
|
|
125
|
+
<div class="ms-row">
|
|
126
|
+
<span class="ms-lbl">Snap</span>
|
|
127
|
+
<div class="ms-snaps">
|
|
128
|
+
${Object.keys(s.snaps).map(
|
|
129
|
+
(kind) => BUI.html`<label class="ms-snap">
|
|
130
|
+
<bim-checkbox
|
|
131
|
+
toggle
|
|
132
|
+
?checked=${s.snaps[kind as keyof typeof s.snaps]}
|
|
133
|
+
@change=${(e: Event) => {
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
135
|
+
tool.setSnap(kind as never, !!(e.target as any).checked);
|
|
136
|
+
}}
|
|
137
|
+
></bim-checkbox>${kind}
|
|
138
|
+
</label>`,
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<label class="ms-row ms-toggle">
|
|
143
|
+
<span class="ms-lbl">Show measurements</span>
|
|
144
|
+
<bim-checkbox
|
|
145
|
+
toggle
|
|
146
|
+
?checked=${s.visible}
|
|
147
|
+
@change=${(e: Event) => {
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
tool.setVisible(!!(e.target as any).checked);
|
|
150
|
+
}}
|
|
151
|
+
></bim-checkbox>
|
|
152
|
+
</label>
|
|
153
|
+
</div>
|
|
154
|
+
</bim-panel>`;
|
|
155
|
+
},
|
|
156
|
+
{ tick: 0 },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
tool.onChanged.add(() => update({ tick: 0 }));
|
|
160
|
+
return panel;
|
|
161
|
+
};
|