@thatopen/services 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +59 -46
  2. package/dist/built-in/index.d.ts +486 -563
  3. package/dist/cli/lib/npmrc.d.ts +28 -0
  4. package/dist/cli/lib/npmrc.test.d.ts +1 -0
  5. package/dist/cli.js +91 -7
  6. package/dist/core/client.d.ts +10 -0
  7. package/dist/index.cjs.js +2 -2
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.es.js +42 -32
  10. package/dist/types/npm.d.ts +14 -0
  11. package/dist/vite-env.d.ts +1 -1
  12. package/docs/builtin/paths.json +74 -70
  13. package/docs/cli/create.md +28 -28
  14. package/docs/cli/local-server.md +38 -38
  15. package/docs/cli/login.md +32 -32
  16. package/docs/cli/publish.md +31 -31
  17. package/docs/cli/run.md +26 -26
  18. package/docs/cli/serve.md +43 -43
  19. package/docs/cli/swap.md +30 -30
  20. package/package.json +100 -100
  21. package/resources/AGENTS.md +40 -40
  22. package/src/built-in/CDEManager/example.ts +214 -214
  23. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  24. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  25. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  26. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  27. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  28. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  29. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  30. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  31. package/src/built-in/UIManager/src/app/example.ts +99 -99
  32. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
  33. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
  34. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
  35. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
  36. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
  37. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
  38. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
  39. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
  40. package/src/built-in/index.ts +649 -755
  41. package/src/cli/templates/app/README.md +241 -241
  42. package/src/cli/templates/app/index.html +35 -27
  43. package/src/cli/templates/app/package.json +31 -27
  44. package/src/cli/templates/app/src/app.ts +37 -0
  45. package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
  46. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
  47. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  48. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  49. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  50. package/src/cli/templates/app/src/globals.ts +1 -0
  51. package/src/cli/templates/app/src/main.ts +340 -56
  52. package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
  53. package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
  54. package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
  55. package/src/cli/templates/app/src/setups/card-header.ts +35 -0
  56. package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
  57. package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
  58. package/src/cli/templates/app/src/setups/clipper.ts +75 -0
  59. package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
  60. package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
  61. package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
  62. package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
  63. package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
  64. package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
  65. package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
  66. package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
  67. package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
  68. package/src/cli/templates/app/src/setups/hider.ts +98 -0
  69. package/src/cli/templates/app/src/setups/index.ts +33 -3
  70. package/src/cli/templates/app/src/setups/inspection.ts +136 -0
  71. package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
  72. package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
  73. package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
  74. package/src/cli/templates/app/src/setups/measurements.ts +133 -0
  75. package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
  76. package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
  77. package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
  78. package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
  79. package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
  80. package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
  81. package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
  82. package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
  83. package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
  84. package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
  85. package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
  86. package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
  87. package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
  88. package/src/cli/templates/app/src/setups/styles.ts +375 -0
  89. package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
  90. package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
  91. package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
  92. package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
  93. package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
  94. package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
  95. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
  96. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
  97. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
  98. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
  99. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
  100. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
  101. package/src/cli/templates/app/src/ui-components/index.ts +2 -1
  102. package/src/cli/templates/app/tsconfig.json +16 -17
  103. package/src/cli/templates/app/vite.config.js +45 -42
  104. package/src/cli/templates/cloud-component/README.md +205 -205
  105. package/src/cli/templates/cloud-component/_thatopen +5 -5
  106. package/src/cli/templates/cloud-component/declarations.json +4 -4
  107. package/src/cli/templates/cloud-component/package.json +21 -21
  108. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  109. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  110. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  111. package/src/cli/templates/shared/AGENTS.md +8 -8
  112. package/src/cli/templates/shared/CLAUDE.md +1 -1
  113. package/src/cli/templates/shared/_gitignore +5 -4
  114. package/src/built-in/AppManager/example.ts +0 -86
  115. package/src/built-in/FileList/example.ts +0 -107
  116. package/src/built-in/TabbedNavigation/example.ts +0 -115
  117. package/src/built-in/ViewportsManager/example.ts +0 -106
  118. package/src/cli/templates/app/src/setups/fragments.ts +0 -18
  119. package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
  120. 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 "./cloud-runner";
2
- export * from "./fragments";
3
- export * from "./ifc-loader";
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
+ };