@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,63 @@
1
+ /**
2
+ * Small FPS counter, mounted INSIDE the viewer (top-center overlay). Measures
3
+ * the browser animation-frame rate (a good proxy for overall responsiveness).
4
+ *
5
+ * It is styled like the rest of the app (panel surface + 1px contrast-20 border
6
+ * + theme text), not a raw green-on-black badge, and is TOGGLEABLE — the
7
+ * returned controller's `setVisible` is wired to a "Show FPS" switch in the
8
+ * Graphics panel. The rAF loop keeps running while hidden (cost is negligible);
9
+ * only the element's visibility changes.
10
+ */
11
+ export interface FpsIndicator {
12
+ /** The overlay element (already mounted inside the viewer). */
13
+ element: HTMLElement;
14
+ /** Whether the counter is currently shown. */
15
+ readonly visible: boolean;
16
+ /** Show / hide the counter. */
17
+ setVisible: (v: boolean) => void;
18
+ }
19
+
20
+ export const fpsIndicator = (parent: HTMLElement): FpsIndicator => {
21
+ const el = document.createElement("div");
22
+ el.style.cssText = `
23
+ position: absolute; top: 0.6rem; left: 50%; transform: translateX(-50%); z-index: 10;
24
+ padding: 0.2rem 0.55rem; border-radius: 0.5rem;
25
+ background: var(--bim-ui_bg-contrast-10, #262629);
26
+ border: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
27
+ color: var(--bim-ui_bg-contrast-100, #e3e3e3);
28
+ font: 600 0.72rem/1.2 ui-monospace, monospace;
29
+ pointer-events: none; user-select: none;
30
+ display: none;
31
+ `;
32
+ el.textContent = "-- FPS";
33
+ // The viewer is position:relative (set in viewports-manager for the anchor
34
+ // dot), so an absolutely-positioned child overlays the canvas correctly.
35
+ if (!parent.style.position) parent.style.position = "relative";
36
+ parent.append(el);
37
+
38
+ let visible = false;
39
+ let frames = 0;
40
+ let last = performance.now();
41
+ const tick = (now: number) => {
42
+ frames += 1;
43
+ const dt = now - last;
44
+ if (dt >= 500) {
45
+ el.textContent = `${Math.round((frames * 1000) / dt)} FPS`;
46
+ frames = 0;
47
+ last = now;
48
+ }
49
+ requestAnimationFrame(tick);
50
+ };
51
+ requestAnimationFrame(tick);
52
+
53
+ return {
54
+ element: el,
55
+ get visible() {
56
+ return visible;
57
+ },
58
+ setVisible: (v: boolean) => {
59
+ visible = v;
60
+ el.style.display = v ? "block" : "none";
61
+ },
62
+ };
63
+ };
@@ -0,0 +1,490 @@
1
+ import * as OBC from "@thatopen/components";
2
+ import * as BUI from "@thatopen/ui";
3
+ import { cardHeader } from "./card-header";
4
+ import { styles, StyleSetting } from "./styles";
5
+ import type { FpsIndicator } from "./fps-indicator";
6
+
7
+ /**
8
+ * GRAPHICS panel — a docked side-panel (third layout alongside Explorer/Files)
9
+ * exposing the viewer's rendering/graphics settings.
10
+ *
11
+ * It REUSES the typed `styles(components, world)` DESCRIPTOR (the same one that
12
+ * backs the bottom-left "Styles" helper tool) so every control here drives the
13
+ * live postproduction pipeline directly — postproduction on/off & preset, edges
14
+ * (contour), surface color / AO / tonal shading, scene background & grid, and
15
+ * the quality block (FXAA, render scale, adaptive resolution, target FPS, the
16
+ * half-res selection-outline toggle). No setting is invented; each maps 1:1 to
17
+ * a wired pipeline feature. Mutating a control takes effect on the next frame.
18
+ *
19
+ * The generic `control()` renderer (switch/slider/color/dropdown wired to a
20
+ * setting's get/set) mirrors styles-panel.ts so the two stay consistent.
21
+ *
22
+ * LAYOUT: to keep the panel dense, a feature's dependent controls are STACKED
23
+ * onto a single shared row (see ROW_LAYOUT below) — e.g. "Edge color" + "Edge
24
+ * strength" sit together, and a feature's toggle pairs with its one dependent
25
+ * value ("Grid" + grid color, "Ambient occlusion" + AO strength, …). Booleans
26
+ * render as a compact pill TOGGLE SWITCH (purple when on) instead of a checkbox.
27
+ * Only the PRESENTATION changes here — every control still drives the exact same
28
+ * StyleSetting get/set, so the pipeline wiring is untouched.
29
+ *
30
+ * Returns the panel ELEMENT WITHOUT self-mounting (like modelTree / filesPanel)
31
+ * so main.ts can strip the card chrome and dock it into the grid.
32
+ */
33
+
34
+ // ── Document-level theming for the dropdown POPUP ───────────────────────────
35
+ // The Style-preset `bim-dropdown` renders its open menu as a `bim-context-menu`
36
+ // that BUI MOVES OUT to a top-level `<dialog data-context-dialog>` appended to
37
+ // `document.body` while visible (see ContextMenu.set visible() in
38
+ // node_modules/@thatopen/ui/dist/index.js: `document.body.append(lt.dialog)` /
39
+ // `lt.dialog.append(this)`). Because the popup leaves this panel's shadow scope,
40
+ // the panel's <style> can't reach it — the popup falls back to BUI defaults
41
+ // (`--bim-ui_bg-contrast-50` surface, 1px shadow-radius) and looks out of place.
42
+ //
43
+ // So we theme it from the DOCUMENT instead, scoped to the context dialog, to
44
+ // match the app's other menus: dark panel surface, 1px contrast-40 border, a
45
+ // small radius + subtle shadow, compact ~0.78rem items, and the SELECTED option
46
+ // drawn with the app's purple accent (var(--bim-ui_accent-base)). `bim-option`'s shadow `:host`
47
+ // sets its own padding/radius (specificity 0,1,0), so where we must beat that we
48
+ // use the scoped dialog selector with `!important`; option BACKGROUND/hover/
49
+ // selected are unset by BUI so a plain rule suffices. The checked-label colour
50
+ // derives from `--bim-ui_main-base`, which we pin to the accent here too.
51
+ const APP_ACCENT = "var(--bim-ui_accent-base)";
52
+ const ensureDropdownPopupStyle = () => {
53
+ const id = "graphics-dropdown-popup-style";
54
+ if (document.getElementById(id)) return;
55
+ const el = document.createElement("style");
56
+ el.id = id;
57
+ el.textContent = `
58
+ /* The popup surface (the relocated bim-context-menu inside the dialog). */
59
+ dialog[data-context-dialog] bim-context-menu {
60
+ border: 1px solid var(--bim-ui_bg-contrast-40, rgba(255,255,255,0.18)) !important;
61
+ border-radius: 0.375rem !important;
62
+ box-shadow: 0 6px 18px rgba(0,0,0,0.45) !important;
63
+ padding: 0.25rem !important;
64
+ min-width: 8rem;
65
+ }
66
+ /* Compact option rows; selected/hover backgrounds (BUI leaves these unset). */
67
+ dialog[data-context-dialog] bim-option {
68
+ font-size: 0.78rem;
69
+ border-radius: 0.25rem !important;
70
+ padding: 0.18rem 0.5rem !important;
71
+ }
72
+ dialog[data-context-dialog] bim-option:hover {
73
+ background-color: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.08));
74
+ }
75
+ /* SELECTED option: the app purple accent, consistently. */
76
+ dialog[data-context-dialog] bim-option[checked] {
77
+ background-color: ${APP_ACCENT};
78
+ }
79
+ `;
80
+ document.head.append(el);
81
+ };
82
+
83
+ // ── Toggle SWITCH for a bool setting (library bim-checkbox[toggle]) ──────────
84
+ // The library checkbox has a built-in pill/switch mode (dark ball + outline,
85
+ // purple track when on) — so we use it directly instead of a hand-rolled widget,
86
+ // keeping the toggle styling in ONE place (the library) for the whole platform.
87
+ // It drives the SAME `StyleSetting.set(boolean)`, so behaviour is identical.
88
+ const boolSwitch = (s: StyleSetting & { type: "bool" }, refresh: () => void) => {
89
+ const on = s.get();
90
+ return BUI.html`<bim-checkbox
91
+ class="g-toggle"
92
+ toggle
93
+ ?checked=${on}
94
+ @change=${(e: Event) => {
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ s.set(!!(e.target as any).checked);
97
+ refresh();
98
+ }}
99
+ ></bim-checkbox>`;
100
+ };
101
+
102
+ // Generic control for one StyleSetting (same wiring styles-panel.ts uses).
103
+ //
104
+ // IMPORTANT: the label is rendered by the ROW (left cell, muted) so every
105
+ // control here is created WITHOUT its own `label` attribute — it renders as a
106
+ // bare, compact widget that sits in the row's right cell. Sizing/colour is
107
+ // driven entirely by the `.g-row` CSS (BUI control custom-props) below.
108
+ const control = (s: StyleSetting, refresh: () => void) => {
109
+ switch (s.type) {
110
+ case "bool":
111
+ // Pill toggle switch (replaces the former bim-checkbox), same setter.
112
+ return boolSwitch(s, refresh);
113
+ case "number":
114
+ return BUI.html`<bim-number-input
115
+ slider
116
+ value=${s.get()}
117
+ min=${s.min}
118
+ max=${s.max}
119
+ step=${s.step}
120
+ @change=${(e: Event) => {
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ s.set(Number((e.target as any).value));
123
+ refresh();
124
+ }}
125
+ ></bim-number-input>`;
126
+ case "color":
127
+ return BUI.html`<bim-color-input
128
+ color=${s.get()}
129
+ @input=${(e: Event) => {
130
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
+ const t = e.target as any;
132
+ s.set(String(t.color ?? t.value));
133
+ refresh();
134
+ }}
135
+ ></bim-color-input>`;
136
+ case "enum":
137
+ return BUI.html`<bim-dropdown
138
+ @change=${(e: Event) => {
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ const picked = (e.target as any).value?.[0];
141
+ const match = s.options.find((o) => o.label === picked);
142
+ if (match) {
143
+ s.set(match.value);
144
+ refresh();
145
+ }
146
+ }}
147
+ >
148
+ ${s.options.map(
149
+ (o) => BUI.html`<bim-option label=${o.label} ?checked=${o.value === s.get()}></bim-option>`,
150
+ )}
151
+ </bim-dropdown>`;
152
+ }
153
+ };
154
+
155
+ // A control sized for sharing a row with another control / a toggle. No inline
156
+ // mini-label: the row's left label gives context, and the widget itself is
157
+ // self-evident (a colour swatch, or a slider showing its value), so a mini-label
158
+ // just costs width and risks crowding the toggle. The `g-i-<type>` class drives
159
+ // the compact per-type width.
160
+ const inlineControl = (s: StyleSetting, refresh: () => void) =>
161
+ BUI.html`<span class="g-inline g-i-${s.type}">${control(s, refresh)}</span>`;
162
+
163
+ export const graphicsPanel = (components: OBC.Components, fps?: FpsIndicator) => {
164
+ // Theme the Style-preset dropdown's relocated popup (document-level, since it
165
+ // escapes this panel's shadow to a top-level dialog). Idempotent.
166
+ ensureDropdownPopupStyle();
167
+
168
+ // The postproduction pipeline lives on the first world's renderer. It is
169
+ // allocated lazily (once the viewport has a real size), so resolve the world
170
+ // at render time — same pattern the model-tree uses.
171
+ const getWorld = () =>
172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
173
+ ([...components.get(OBC.Worlds).list.values()][0] as OBC.World) ?? null;
174
+
175
+ // Has the deferred pipeline been configured yet? `styles()` reads
176
+ // `renderer.postproduction.deferred`, which throws until the pipeline is up.
177
+ const ready = (world: OBC.World | null) => {
178
+ if (!world) return false;
179
+ try {
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ const r = world.renderer as any;
182
+ return !!r?.postproduction?.deferred;
183
+ } catch {
184
+ return false;
185
+ }
186
+ };
187
+
188
+ const [panel, update] = BUI.Component.create<BUI.Panel, { tick: number }>(
189
+ (state) => {
190
+ void state.tick; // bump to force a re-read of all controls
191
+
192
+ const world = getWorld();
193
+ let body: unknown;
194
+
195
+ if (!ready(world)) {
196
+ body = BUI.html`<div style="padding: 0.6rem 0.4rem 0.6rem 1.1rem;">
197
+ <bim-label style="opacity: 0.6; white-space: normal; font-size: 0.78rem;">Rendering pipeline not ready yet. Load a model / wait for the viewer to initialize.</bim-label>
198
+ </div>`;
199
+ } else {
200
+ const { settings } = styles(components, world!);
201
+ const byKey = new Map(settings.map((s) => [s.key, s]));
202
+ const refresh = () => update({ tick: state.tick + 1 });
203
+
204
+ // ── Row layout plan ───────────────────────────────────────────────
205
+ // Each section is a band header followed by a list of ROWS. A row is
206
+ // either a single labelled control (the usual label-left / control-
207
+ // right shape) or a PAIRED row that stacks a feature's dependent
208
+ // controls together so the panel reads as a dense grid. Every key here
209
+ // still resolves to its StyleSetting via `byKey`, so wiring is intact.
210
+ type Row =
211
+ // Standard "label on left, one control on right" row.
212
+ | { kind: "single"; key: string; label?: string }
213
+ // Toggle on the left (with the section's row label), its ONE
214
+ // dependent value inline on the right of the same line.
215
+ | { kind: "toggleWith"; toggle: string; label?: string; dep: string; depLabel: string; depAlwaysEnabled?: boolean }
216
+ // Two dependent controls sharing a row under an obvious section,
217
+ // each with a tight inline mini-label (no wide left label cell).
218
+ | { kind: "pair"; label: string; a: string; aLabel: string; b: string; bLabel: string };
219
+
220
+ const sections: { name: string; icon: string; rows: Row[] }[] = [
221
+ {
222
+ name: "Preset",
223
+ icon: "mdi:palette-swatch",
224
+ rows: [
225
+ { kind: "single", key: "preset" },
226
+ { kind: "single", key: "postproductionEnabled" },
227
+ ],
228
+ },
229
+ {
230
+ name: "Edges",
231
+ icon: "mdi:vector-square",
232
+ rows: [
233
+ { kind: "single", key: "edges" },
234
+ // Edge color + edge strength share one row (swatch + strength).
235
+ { kind: "pair", label: "Edge", a: "edgeColor", aLabel: "Color", b: "edgeStrength", bLabel: "Strength" },
236
+ ],
237
+ },
238
+ {
239
+ name: "Shading",
240
+ icon: "mdi:circle-half-full",
241
+ rows: [
242
+ { kind: "single", key: "surfaceColor" },
243
+ // AO toggle + its strength on one line; tonal toggle + its floor.
244
+ { kind: "toggleWith", toggle: "ao", label: "Ambient occlusion", dep: "aoStrength", depLabel: "Strength" },
245
+ { kind: "toggleWith", toggle: "tonalShading", label: "Tonal shading", dep: "tonalFloor", depLabel: "Floor" },
246
+ ],
247
+ },
248
+ {
249
+ name: "Scene",
250
+ icon: "mdi:image-outline",
251
+ rows: [
252
+ // Each scene toggle pairs with its own color on the same line.
253
+ // Background colour is relevant when transparent is OFF (opaque),
254
+ // i.e. the INVERSE of the generic toggle→dep relationship, so keep
255
+ // the picker always interactive instead of dimming it.
256
+ { kind: "toggleWith", toggle: "transparentBackground", label: "Transparent bg", dep: "backgroundColor", depLabel: "Color", depAlwaysEnabled: true },
257
+ { kind: "toggleWith", toggle: "grid", label: "Grid", dep: "gridColor", depLabel: "Color" },
258
+ ],
259
+ },
260
+ {
261
+ name: "Quality",
262
+ icon: "mdi:speedometer",
263
+ rows: [
264
+ { kind: "single", key: "fxaa" },
265
+ // Adaptive resolution toggle + its target FPS on one line.
266
+ { kind: "toggleWith", toggle: "adaptiveResolution", label: "Adaptive resolution", dep: "targetFps", depLabel: "FPS" },
267
+ // Render scale + the high-res selection-outline toggle on one line.
268
+ { kind: "toggleWith", toggle: "highResOutline", label: "Hi-res outline", dep: "renderScale", depLabel: "Scale" },
269
+ ],
270
+ },
271
+ ];
272
+
273
+ // "Show FPS" toggle (Quality section) — a synthetic bool wired straight
274
+ // to the FPS overlay's visibility, so it renders through the same control
275
+ // path as every other switch. Only added when an FPS controller exists.
276
+ if (fps) {
277
+ byKey.set("showFps", {
278
+ key: "showFps",
279
+ label: "Show FPS",
280
+ group: "Quality",
281
+ type: "bool",
282
+ default: false,
283
+ get: () => fps.visible,
284
+ set: (v: boolean) => fps.setVisible(v),
285
+ });
286
+ sections.find((s) => s.name === "Quality")?.rows.push({
287
+ kind: "single",
288
+ key: "showFps",
289
+ });
290
+ }
291
+
292
+ // Render one planned row, resolving each key to its live StyleSetting.
293
+ const renderRow = (row: Row): unknown => {
294
+ if (row.kind === "single") {
295
+ const s = byKey.get(row.key);
296
+ if (!s) return null;
297
+ return BUI.html`<div class="g-row g-${s.type}">
298
+ <span class="g-k">${row.label ?? s.label}</span>
299
+ <span class="g-v">${control(s, refresh)}</span>
300
+ </div>`;
301
+ }
302
+ if (row.kind === "toggleWith") {
303
+ const t = byKey.get(row.toggle);
304
+ const dep = byKey.get(row.dep);
305
+ if (!t || t.type !== "bool" || !dep) return null;
306
+ const off = row.depAlwaysEnabled ? false : !t.get();
307
+ // The dependent control is dimmed/disabled while its toggle is off.
308
+ return BUI.html`<div class="g-row g-toggle-with">
309
+ <span class="g-k">${row.label ?? t.label}</span>
310
+ <span class="g-v g-v-multi">
311
+ <span class="g-dep ${off ? "g-dep-off" : ""}">
312
+ ${inlineControl(dep, refresh)}
313
+ </span>
314
+ ${boolSwitch(t, refresh)}
315
+ </span>
316
+ </div>`;
317
+ }
318
+ // pair: two dependent controls side by side under one section label.
319
+ const a = byKey.get(row.a);
320
+ const b = byKey.get(row.b);
321
+ if (!a || !b) return null;
322
+ return BUI.html`<div class="g-row g-pair">
323
+ <span class="g-k">${row.label}</span>
324
+ <span class="g-v g-v-multi">
325
+ ${inlineControl(a, refresh)}
326
+ ${inlineControl(b, refresh)}
327
+ </span>
328
+ </div>`;
329
+ };
330
+
331
+ body = BUI.html`
332
+ ${sections.map(
333
+ (sec) => BUI.html`
334
+ <div class="g-band"><bim-icon class="g-band-ico" icon=${sec.icon}></bim-icon>${sec.name}</div>
335
+ ${sec.rows.map((row) => renderRow(row))}
336
+ `,
337
+ )}
338
+ `;
339
+ }
340
+
341
+ return BUI.html`
342
+ <bim-panel
343
+ label="Graphics"
344
+ icon="mdi:tune"
345
+ style="width: 100%; height: 100%; pointer-events: auto;"
346
+ >
347
+ <style>
348
+ .prop-vp::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; }
349
+ .prop-vp::-webkit-scrollbar-thumb { border-radius: 0.25rem; background-color: var(--bim-scrollbar--c, #3C3C41); }
350
+ .prop-vp::-webkit-scrollbar-track { background-color: var(--bim-scrollbar--bgc, var(--bim-ui_bg-contrast-10)); }
351
+
352
+ /* ── Full-bleed rows (mirror properties' .p-row / .p-kv) ──────────
353
+ Rows reach both panel edges; text is re-inset via the same 1.1rem
354
+ left / 0.4rem right padding the panel chrome uses. One consistent
355
+ compact scale (~0.78rem) like the properties rows. */
356
+ /* ── Full-bleed rows (mirror properties' .p-row / .p-kv) ──────────
357
+ Rows reach both panel edges; text is re-inset via the same 1.1rem
358
+ left / 0.4rem right padding the panel chrome uses. One consistent
359
+ compact scale (~0.78rem) like the properties rows. */
360
+ .g-row { box-sizing: border-box; min-height: 32px; display: flex;
361
+ align-items: center; justify-content: space-between; gap: 0.6rem;
362
+ padding: 0.35rem 0.4rem 0.35rem 1.1rem; font-size: 0.78rem;
363
+ line-height: 1.3; color: var(--bim-ui_bg-contrast-100, #e3e3e3);
364
+ border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
365
+ /* Muted label on the LEFT (like .p-k, opacity ~0.6). Single line +
366
+ ellipsis so a long label never wraps and overlaps its control. */
367
+ .g-k { opacity: 0.6; flex: 0 1 auto; min-width: 0; max-width: 42%;
368
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
369
+ /* Control on the RIGHT (like .p-v), right-aligned, never crushed. */
370
+ .g-v { flex: 1 1 auto; min-width: 0; display: flex;
371
+ align-items: center; justify-content: flex-end; }
372
+ /* Multi-control right cell: dependent value(s) + toggle on one line,
373
+ tight gap, right-aligned, wrap-free. */
374
+ .g-v-multi { gap: 0.65rem; flex-wrap: nowrap; }
375
+
376
+ /* Inline mini-labelled control (the dependent values that share a
377
+ row). The mini-label is muted + tiny; the control sits right next
378
+ to it with a compact field. */
379
+ .g-inline { display: inline-flex; align-items: center; gap: 0.3rem;
380
+ flex: 0 1 auto; min-width: 0; }
381
+ .g-il { opacity: 0.55; font-size: 0.7rem; letter-spacing: 0.01em;
382
+ white-space: nowrap; }
383
+ /* A dependent control whose owning toggle is OFF: dimmed + inert. */
384
+ .g-dep { display: inline-flex; align-items: center; min-width: 0;
385
+ flex: 0 1 auto; }
386
+ .g-dep-off { opacity: 0.4; pointer-events: none; }
387
+
388
+ /* Section band — EXACTLY the properties panel's .p-psh band: muted,
389
+ full-bleed, a subtle gray BACKGROUND (one step up from the panel)
390
+ with top/bottom hairlines matching the same 1px contrast-20 used
391
+ between rows — never a heavier rule, never the bright/gradient
392
+ panel-title treatment. Subordinate to the "Graphics" card title. */
393
+ .g-band { box-sizing: border-box; padding: 0.4rem 0.4rem 0.4rem 1.1rem;
394
+ display: flex; align-items: center; gap: 0.4rem;
395
+ font-size: 0.76rem; font-weight: 500; letter-spacing: 0.01em;
396
+ color: var(--bim-ui_bg-contrast-70, #a7a7ab);
397
+ background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
398
+ border-top: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
399
+ border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
400
+ /* Per-section icon — muted to match the band text, never the accent. */
401
+ .g-band-ico { flex: 0 0 auto; font-size: 0.9rem;
402
+ color: var(--bim-ui_bg-contrast-70, #a7a7ab); }
403
+ .g-band:first-child { border-top: none; }
404
+
405
+ /* ── Slim, dark-themed controls ──────────────────────────────────
406
+ Override BUI control custom-props so the widgets read as compact
407
+ right-aligned controls (matching the searchbar field look used in
408
+ the tree/properties inputs), not chunky default widgets. */
409
+ .g-v bim-number-input,
410
+ .g-v bim-color-input,
411
+ .g-v bim-dropdown {
412
+ /* Subtle app surface (not a near-black box) so the controls read as
413
+ part of the panel, like the app's other surfaces. */
414
+ font-size: 0.78rem;
415
+ }
416
+ /* Number input: small dark field (not a big solid-purple block),
417
+ right-aligned, fixed compact width. Slim the embedded slider too.
418
+ Narrower (5.5rem) inside a shared/inline cell so two controls fit. */
419
+ .g-number bim-number-input { flex: 0 0 auto; width: 7.5rem; }
420
+ /* SLIDER FILL FLUSH TO BORDER.
421
+ In BUI the slider renders as <bim-input>→.input (the rounded
422
+ field, padded by --bim-input--p) → .slider → .slider-indicator
423
+ (the purple fill: position:absolute; top:0; left:0; height:100%;
424
+ width:value%; background:var(--bim-ui_main-base)). The fill is
425
+ absolutely positioned to the .input CONTENT box, so the field's
426
+ inner padding (--bim-input--p, which the shared rule above sets to
427
+ 0.15rem 0.35rem) shows as an inset band of field background all
428
+ around the fill. Zero the padding for the slider so the fill
429
+ reaches the rounded border edge-to-edge. The value text is a
430
+ separate z-index:1 <bim-label> centered over the fill, so it stays
431
+ readable. (--bim-slider--* are NOT real BUI hooks — the fill color
432
+ is --bim-ui_main-base, already var(--bim-ui_accent-base) globally.) */
433
+ .g-v bim-number-input {
434
+ }
435
+ .g-v bim-number-input::part(input) { text-align: right; }
436
+ .g-inline bim-number-input { flex: 0 1 auto; width: 4.5rem; min-width: 3rem; }
437
+ /* Color input: small swatch + compact hex, not an oversized control. */
438
+ .g-color bim-color-input { flex: 0 0 auto; width: 6.5rem; }
439
+ .g-v bim-color-input { /* swatch box size */ }
440
+ /* Let the colour input take its NATURAL width (swatch + hex), with no
441
+ grow/shrink — so its real rounded box renders at that size instead
442
+ of a forced narrower width whose content overflows rightward into
443
+ the gap / under the toggle. There is room for it in every row. */
444
+ .g-inline bim-color-input { flex: 0 0 auto; }
445
+ /* Dropdown CLOSED control: compact, right-aligned, and it inherits
446
+ the shared field look above (bg-contrast-20 fill, 1px contrast-40
447
+ outline). The OPEN popup is themed document-level (see
448
+ ensureDropdownPopupStyle) because BUI relocates it to a top-level
449
+ dialog outside this shadow scope. */
450
+ .g-enum bim-dropdown { flex: 0 0 auto; min-width: 7rem; max-width: 10rem; }
451
+
452
+ /* ── Library toggle (bim-checkbox[toggle]) sizing ────────────────
453
+ The checkbox host is display:block and would stretch to fill the
454
+ row's right cell (pushing/overlapping the dependent control). Pin
455
+ it to its intrinsic switch width and keep it from growing so it
456
+ sits flush-right without crowding its neighbour. The track/knob/
457
+ on-colour all come from the library (purple track + #3C3C41 ball
458
+ with an outline) — no per-app restyle. */
459
+ .g-row bim-checkbox.g-toggle { flex: 0 0 2rem; width: 2rem; margin-left: 0.35rem; }
460
+ </style>
461
+ <div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
462
+ <div style="flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0 0.4rem 0.4rem 1.1rem;">
463
+ <div class="prop-vp" style="flex: 1 1 auto; min-height: 0; overflow-y: auto;
464
+ margin-left: -1.1rem; margin-right: -0.4rem;">
465
+ ${body}
466
+ </div>
467
+ </div>
468
+ </div>
469
+ </bim-panel>
470
+ `;
471
+ },
472
+ { tick: 0 },
473
+ );
474
+
475
+ // The pipeline configures asynchronously after the first sized frame; the
476
+ // panel may render before it's ready. Poll briefly until ready, then re-render
477
+ // once so the real controls appear (cheap: stops the moment it succeeds).
478
+ let attempts = 0;
479
+ const poll = window.setInterval(() => {
480
+ attempts += 1;
481
+ if (ready(getWorld())) {
482
+ update({ tick: 0 });
483
+ window.clearInterval(poll);
484
+ } else if (attempts > 120) {
485
+ window.clearInterval(poll); // give up after ~60s
486
+ }
487
+ }, 500);
488
+
489
+ return panel;
490
+ };
@@ -0,0 +1,88 @@
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
+ };