@thatopen/services 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/built-in/index.d.ts +22 -2
- package/dist/cli.js +3 -3
- package/docs/ai-quickstart.md +86 -0
- package/docs/builtin/paths.json +74 -74
- package/docs/cli/create.md +4 -3
- package/package.json +1 -1
- package/src/built-in/index.ts +22 -2
- package/src/cli/templates/app/src/app.ts +37 -37
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -50
- package/src/cli/templates/app/src/main.ts +223 -340
- package/src/cli/templates/app/src/setups/camera-tools.ts +63 -63
- package/src/cli/templates/app/src/setups/clipper-tool.ts +20 -1
- package/src/cli/templates/app/src/setups/clipper.ts +75 -75
- package/src/cli/templates/app/src/setups/index.ts +23 -33
- package/src/cli/templates/app/src/setups/inspection.ts +136 -136
- package/src/cli/templates/app/src/setups/measurement-tool.ts +19 -1
- package/src/cli/templates/app/src/setups/measurements.ts +133 -133
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -88
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -166
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -31
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -51
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -1383
- package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -178
- package/src/cli/templates/app/src/setups/styles-panel.ts +110 -110
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +15 -9
- package/src/cli/templates/app/src/setups/tool-mode.ts +23 -23
- package/src/cli/templates/app/src/setups/active-tool-hud.ts +0 -69
- package/src/cli/templates/app/src/setups/clipper-panel.ts +0 -167
- package/src/cli/templates/app/src/setups/commands-panel.ts +0 -239
- package/src/cli/templates/app/src/setups/data-table-panel.ts +0 -2224
- package/src/cli/templates/app/src/setups/file-format-icons.ts +0 -95
- package/src/cli/templates/app/src/setups/files-panel.ts +0 -1503
- package/src/cli/templates/app/src/setups/fps-indicator.ts +0 -63
- package/src/cli/templates/app/src/setups/graphics-panel.ts +0 -490
- package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +0 -161
- package/src/cli/templates/app/src/setups/model-tree.ts +0 -797
- package/src/cli/templates/app/src/setups/objects-panel.ts +0 -127
- package/src/cli/templates/app/src/setups/properties-panel.ts +0 -1048
- package/src/cli/templates/app/src/setups/settings-panel.ts +0 -94
- package/src/cli/templates/app/src/setups/visibility-toolbar.ts +0 -498
- package/src/cli/templates/app/src/setups/walkthrough.ts +0 -528
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import * as OBC from "@thatopen/components";
|
|
2
|
-
import { toolModeManager, type ManagedTool } from "./tool-mode-manager";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Active-tool HUD — a floating text overlay (top-left of the viewport) showing
|
|
6
|
-
* what the user is currently doing, e.g. "Drawing clipping plane" /
|
|
7
|
-
* "Measuring length".
|
|
8
|
-
*
|
|
9
|
-
* Fully DECOUPLED from any specific tool: it only subscribes to the global
|
|
10
|
-
* toolModeManager's `onActiveChanged` and renders the active tool's registered
|
|
11
|
-
* {@link ManagedTool.label} (+ optional icon), hidden when no tool is active. Any
|
|
12
|
-
* future tool that registers a label with the manager shows up here with zero
|
|
13
|
-
* HUD changes.
|
|
14
|
-
*
|
|
15
|
-
* Styled like the other in-viewer overlays (fps counter): panel surface + 1px
|
|
16
|
-
* contrast-20 border + theme text, pointer-events off.
|
|
17
|
-
*/
|
|
18
|
-
export interface ActiveToolHud {
|
|
19
|
-
element: HTMLElement;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const activeToolHud = (
|
|
23
|
-
parent: HTMLElement,
|
|
24
|
-
components: OBC.Components,
|
|
25
|
-
): ActiveToolHud => {
|
|
26
|
-
const manager = toolModeManager(components);
|
|
27
|
-
|
|
28
|
-
const el = document.createElement("div");
|
|
29
|
-
el.style.cssText = `
|
|
30
|
-
position: absolute; top: 0.6rem; left: 0.6rem; z-index: 10;
|
|
31
|
-
display: none; align-items: center; gap: 0.4rem;
|
|
32
|
-
padding: 0.25rem 0.6rem; border-radius: 0.5rem;
|
|
33
|
-
background: var(--bim-ui_bg-contrast-10, #262629);
|
|
34
|
-
border: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
35
|
-
color: var(--bim-ui_bg-contrast-100, #e3e3e3);
|
|
36
|
-
font: 600 0.74rem/1.2 "Plus Jakarta Sans", sans-serif;
|
|
37
|
-
pointer-events: none; user-select: none;
|
|
38
|
-
`;
|
|
39
|
-
const icon = document.createElement("bim-icon");
|
|
40
|
-
icon.style.fontSize = "0.95rem";
|
|
41
|
-
const text = document.createElement("span");
|
|
42
|
-
el.append(icon, text);
|
|
43
|
-
|
|
44
|
-
// The viewer is position:relative, so an absolutely-positioned child overlays
|
|
45
|
-
// the canvas correctly.
|
|
46
|
-
if (!parent.style.position) parent.style.position = "relative";
|
|
47
|
-
parent.append(el);
|
|
48
|
-
|
|
49
|
-
const render = (tool: ManagedTool | null) => {
|
|
50
|
-
if (!tool) {
|
|
51
|
-
el.style.display = "none";
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
text.textContent =
|
|
55
|
-
typeof tool.label === "function" ? tool.label() : tool.label;
|
|
56
|
-
if (tool.icon) {
|
|
57
|
-
icon.setAttribute("icon", tool.icon);
|
|
58
|
-
icon.style.display = "";
|
|
59
|
-
} else {
|
|
60
|
-
icon.style.display = "none";
|
|
61
|
-
}
|
|
62
|
-
el.style.display = "flex";
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
manager.onActiveChanged.add(render);
|
|
66
|
-
render(manager.active);
|
|
67
|
-
|
|
68
|
-
return { element: el };
|
|
69
|
-
};
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import * as OBC from "@thatopen/components";
|
|
2
|
-
import * as BUI from "@thatopen/ui";
|
|
3
|
-
import type { ClipperTool } from "./clipper-tool";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* CLIPPING panel — a docked side-panel (a 4th layout alongside
|
|
7
|
-
* Explorer/Files/Graphics) for section planes + per-category section styling.
|
|
8
|
-
*
|
|
9
|
-
* Sections:
|
|
10
|
-
* - Section planes — master Enabled, Add plane (placing mode: double-click a
|
|
11
|
-
* surface), Clear all.
|
|
12
|
-
* - Active planes — one row per plane (per-plane enable + delete).
|
|
13
|
-
* - Section styling — master fills/edges visibility + a per-IFC-category list,
|
|
14
|
-
* each with a fill colour, an edge colour, and an include toggle.
|
|
15
|
-
*
|
|
16
|
-
* Vanilla BUI, matching graphics-panel.ts / files-panel.ts: `bim-panel` (native
|
|
17
|
-
* header label+icon), muted section bands, 1px contrast-20 hairline rows, the
|
|
18
|
-
* #3C3C41 scrollbar, library `bim-checkbox[toggle]` switches, `bim-button`s and
|
|
19
|
-
* `bim-color-input`s. Factory returns the element WITHOUT self-mounting.
|
|
20
|
-
*/
|
|
21
|
-
export const clipperPanel = (
|
|
22
|
-
_components: OBC.Components,
|
|
23
|
-
tool: ClipperTool,
|
|
24
|
-
) => {
|
|
25
|
-
const [panel, update] = BUI.Component.create<BUI.Panel, { tick: number }>(
|
|
26
|
-
(state) => {
|
|
27
|
-
const refresh = () => update({ tick: state.tick + 1 });
|
|
28
|
-
|
|
29
|
-
const enabled = tool.isEnabled();
|
|
30
|
-
const sectionStyle = tool.getSectionStyle();
|
|
31
|
-
const stylingVisible = tool.isStylingVisible();
|
|
32
|
-
|
|
33
|
-
return BUI.html`
|
|
34
|
-
<bim-panel label="Clipping" icon="mdi:scissors-cutting"
|
|
35
|
-
style="width: 100%; height: 100%; pointer-events: auto;">
|
|
36
|
-
<style>
|
|
37
|
-
.c-vp::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; }
|
|
38
|
-
.c-vp::-webkit-scrollbar-thumb { border-radius: 0.25rem; background-color: var(--bim-scrollbar--c, #3C3C41); }
|
|
39
|
-
.c-vp::-webkit-scrollbar-track { background-color: var(--bim-scrollbar--bgc, var(--bim-ui_bg-contrast-10)); }
|
|
40
|
-
|
|
41
|
-
.c-band { box-sizing: border-box; padding: 0.4rem 0.4rem 0.4rem 1.1rem;
|
|
42
|
-
display: flex; align-items: center; gap: 0.4rem;
|
|
43
|
-
font-size: 0.76rem; font-weight: 500; letter-spacing: 0.01em;
|
|
44
|
-
color: var(--bim-ui_bg-contrast-70, #a7a7ab);
|
|
45
|
-
background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
46
|
-
border-top: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
47
|
-
border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
|
|
48
|
-
.c-band-ico { flex: 0 0 auto; font-size: 0.9rem; color: var(--bim-ui_bg-contrast-70, #a7a7ab); }
|
|
49
|
-
.c-band:first-child { border-top: none; }
|
|
50
|
-
|
|
51
|
-
.c-row { box-sizing: border-box; min-height: 32px; display: flex;
|
|
52
|
-
align-items: center; justify-content: space-between; gap: 0.6rem;
|
|
53
|
-
padding: 0.35rem 0.4rem 0.35rem 1.1rem; font-size: 0.78rem;
|
|
54
|
-
color: var(--bim-ui_bg-contrast-100, #e3e3e3);
|
|
55
|
-
border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
|
|
56
|
-
.c-k { opacity: 0.6; flex: 0 1 auto; min-width: 0; white-space: nowrap;
|
|
57
|
-
overflow: hidden; text-overflow: ellipsis; }
|
|
58
|
-
.c-v { flex: 1 1 auto; min-width: 0; display: flex; align-items: center;
|
|
59
|
-
justify-content: flex-end; gap: 0.5rem; }
|
|
60
|
-
.c-row bim-checkbox.c-toggle, .c-cat bim-checkbox.c-toggle { flex: 0 0 2rem; width: 2rem; }
|
|
61
|
-
.c-row bim-button.c-del { flex: 0 0 auto; --bim-button--bgc: transparent; }
|
|
62
|
-
|
|
63
|
-
.c-empty { padding: 0.6rem 0.4rem 0.6rem 1.1rem; opacity: 0.6;
|
|
64
|
-
font-size: 0.78rem; line-height: 1.3; white-space: normal; }
|
|
65
|
-
|
|
66
|
-
.c-actions { display: flex; flex-direction: column; gap: 0.4rem;
|
|
67
|
-
padding: 0.5rem 0.4rem 0.5rem 1.1rem; }
|
|
68
|
-
.c-actions bim-button { width: 100%; }
|
|
69
|
-
|
|
70
|
-
/* Per-category style row: [toggle] [name grows] [fill][edge swatches] */
|
|
71
|
-
.c-cat { box-sizing: border-box; min-height: 32px; display: flex;
|
|
72
|
-
align-items: center; gap: 0.5rem;
|
|
73
|
-
padding: 0.3rem 0.4rem 0.3rem 1.1rem; font-size: 0.78rem;
|
|
74
|
-
color: var(--bim-ui_bg-contrast-100, #e3e3e3);
|
|
75
|
-
border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
|
|
76
|
-
.c-cat-name { flex: 1 1 auto; min-width: 0; opacity: 0.85;
|
|
77
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
78
|
-
.c-cat-colors { flex: 0 0 auto; display: flex; align-items: center; gap: 0.35rem; }
|
|
79
|
-
.c-cat-colors bim-color-input { flex: 0 0 auto; }
|
|
80
|
-
.c-cat-off { opacity: 0.45; }
|
|
81
|
-
.c-cat-off .c-cat-colors { pointer-events: none; }
|
|
82
|
-
</style>
|
|
83
|
-
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
|
84
|
-
<div style="flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0 0.4rem 0.4rem 1.1rem;">
|
|
85
|
-
<div class="c-vp" style="flex: 1 1 auto; min-height: 0; overflow-y: auto;
|
|
86
|
-
margin-left: -1.1rem; margin-right: -0.4rem;">
|
|
87
|
-
|
|
88
|
-
<div class="c-row">
|
|
89
|
-
<span class="c-k">Enabled</span>
|
|
90
|
-
<span class="c-v">
|
|
91
|
-
<bim-checkbox class="c-toggle" toggle ?checked=${enabled}
|
|
92
|
-
@change=${(e: Event) => {
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
-
tool.setEnabled(!!(e.target as any).checked);
|
|
95
|
-
refresh();
|
|
96
|
-
}}></bim-checkbox>
|
|
97
|
-
</span>
|
|
98
|
-
</div>
|
|
99
|
-
<div class="c-row">
|
|
100
|
-
<span class="c-k">Show fills / edges</span>
|
|
101
|
-
<span class="c-v">
|
|
102
|
-
<bim-checkbox class="c-toggle" toggle ?checked=${stylingVisible}
|
|
103
|
-
@change=${(e: Event) => {
|
|
104
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
-
tool.setStylingVisible(!!(e.target as any).checked);
|
|
106
|
-
refresh();
|
|
107
|
-
}}></bim-checkbox>
|
|
108
|
-
</span>
|
|
109
|
-
</div>
|
|
110
|
-
<div class="c-row">
|
|
111
|
-
<span class="c-k">Fill color</span>
|
|
112
|
-
<span class="c-v">
|
|
113
|
-
<bim-color-input color=${sectionStyle.fill}
|
|
114
|
-
@input=${(e: Event) => {
|
|
115
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
-
const t = e.target as any;
|
|
117
|
-
tool.setSectionFill(String(t.color ?? t.value));
|
|
118
|
-
}}></bim-color-input>
|
|
119
|
-
</span>
|
|
120
|
-
</div>
|
|
121
|
-
<div class="c-row">
|
|
122
|
-
<span class="c-k">Edge color</span>
|
|
123
|
-
<span class="c-v">
|
|
124
|
-
<bim-color-input color=${sectionStyle.line}
|
|
125
|
-
@input=${(e: Event) => {
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
-
const t = e.target as any;
|
|
128
|
-
tool.setSectionLine(String(t.color ?? t.value));
|
|
129
|
-
}}></bim-color-input>
|
|
130
|
-
</span>
|
|
131
|
-
</div>
|
|
132
|
-
<div class="c-row">
|
|
133
|
-
<span class="c-k">Fill opacity</span>
|
|
134
|
-
<span class="c-v">
|
|
135
|
-
<bim-number-input slider value=${sectionStyle.opacity}
|
|
136
|
-
min="0" max="1" step="0.05"
|
|
137
|
-
@change=${(e: Event) => {
|
|
138
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
139
|
-
tool.setSectionOpacity(Number((e.target as any).value));
|
|
140
|
-
}}></bim-number-input>
|
|
141
|
-
</span>
|
|
142
|
-
</div>
|
|
143
|
-
<div class="c-row">
|
|
144
|
-
<span class="c-k">Edge width</span>
|
|
145
|
-
<span class="c-v">
|
|
146
|
-
<bim-number-input slider value=${sectionStyle.width}
|
|
147
|
-
min="1" max="20" step="1"
|
|
148
|
-
@change=${(e: Event) => {
|
|
149
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
-
tool.setSectionWidth(Number((e.target as any).value));
|
|
151
|
-
}}></bim-number-input>
|
|
152
|
-
</span>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
</bim-panel>
|
|
158
|
-
`;
|
|
159
|
-
},
|
|
160
|
-
{ tick: 0 },
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
tool.onChanged.add(() => update({ tick: 0 }));
|
|
164
|
-
tool.onStyleChanged.add(() => update({ tick: 0 }));
|
|
165
|
-
|
|
166
|
-
return panel;
|
|
167
|
-
};
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import * as OBC from "@thatopen/components";
|
|
3
|
-
import * as OBF from "@thatopen/components-front";
|
|
4
|
-
import * as BUI from "@thatopen/ui";
|
|
5
|
-
import { hider } from "./hider";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* COMMANDS — a docked side-panel (alongside Explorer / Files / Graphics) that is
|
|
9
|
-
* both the keyboard-shortcut registry and its palette UI.
|
|
10
|
-
*
|
|
11
|
-
* A single `COMMANDS` table maps each viewer action to a shortcut + an mdi icon
|
|
12
|
-
* and a `run()` wired to the EXISTING components (the `hider` controller for
|
|
13
|
-
* visibility, the Highlighter for the current selection + clear, camera-controls
|
|
14
|
-
* for focus). The panel lists every command with its shortcut (command-palette
|
|
15
|
-
* style, click-to-run), and a global `keydown` handler dispatches the shortcuts —
|
|
16
|
-
* ignored while typing in an input so search fields aren't hijacked.
|
|
17
|
-
*
|
|
18
|
-
* Self-contained: returns the panel element WITHOUT self-mounting (like
|
|
19
|
-
* modelTree / graphicsPanel) and installs its own window keydown listener.
|
|
20
|
-
*
|
|
21
|
-
* @param components engine components
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
interface ViewerCommand {
|
|
25
|
-
id: string;
|
|
26
|
-
label: string;
|
|
27
|
-
icon: string;
|
|
28
|
-
/** Display chips, e.g. ["Shift", "H"]. */
|
|
29
|
-
keys: string[];
|
|
30
|
-
/** Does this keydown event trigger the command? */
|
|
31
|
-
match: (e: KeyboardEvent) => boolean;
|
|
32
|
-
run: () => void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const noMods = (e: KeyboardEvent) =>
|
|
36
|
-
!e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
37
|
-
|
|
38
|
-
// Don't hijack shortcuts while the user is typing in a field.
|
|
39
|
-
const typingInField = (e: KeyboardEvent) => {
|
|
40
|
-
const path = (e.composedPath?.() ?? []) as HTMLElement[];
|
|
41
|
-
const el = (path[0] ?? (e.target as HTMLElement)) || null;
|
|
42
|
-
const active = (document.activeElement as HTMLElement) || null;
|
|
43
|
-
const isField = (n: HTMLElement | null) =>
|
|
44
|
-
!!n &&
|
|
45
|
-
(n.isContentEditable ||
|
|
46
|
-
/^(input|textarea|select)$/i.test(n.tagName) ||
|
|
47
|
-
/^bim-(text|number)-input$/i.test(n.tagName));
|
|
48
|
-
return path.some(isField) || isField(el) || isField(active);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const commandsPanel = (components: OBC.Components) => {
|
|
52
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
53
|
-
const highlighter = components.get(OBF.Highlighter);
|
|
54
|
-
const selectName = highlighter.config.selectName;
|
|
55
|
-
const h = hider(components);
|
|
56
|
-
|
|
57
|
-
const hasSelection = () => {
|
|
58
|
-
const map = highlighter.selection[selectName];
|
|
59
|
-
return !!map && Object.values(map).some((s) => s.size > 0);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const focusSelection = async () => {
|
|
63
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
-
const world = [...components.get(OBC.Worlds).list.values()][0] as any;
|
|
65
|
-
const controls = world?.camera?.controls;
|
|
66
|
-
if (!controls) return;
|
|
67
|
-
const map = highlighter.selection[selectName];
|
|
68
|
-
const hasSel = !!map && Object.values(map).some((s) => s.size > 0);
|
|
69
|
-
try {
|
|
70
|
-
const box = new THREE.Box3();
|
|
71
|
-
if (hasSel) {
|
|
72
|
-
const boxes = (await fragments.getBBoxes(map)) as THREE.Box3[];
|
|
73
|
-
for (const b of boxes) box.union(b);
|
|
74
|
-
} else {
|
|
75
|
-
// Nothing selected → focus = zoom to fit the WHOLE model (union of all
|
|
76
|
-
// loaded models' bounds). This makes a dedicated fit/home button unnecessary.
|
|
77
|
-
for (const model of fragments.list.values()) {
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
-
const b = (model as any).box as THREE.Box3 | undefined;
|
|
80
|
-
if (b && !b.isEmpty()) box.union(b);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (box.isEmpty()) return;
|
|
84
|
-
const sphere = box.getBoundingSphere(new THREE.Sphere());
|
|
85
|
-
await controls.fitToSphere(sphere, true);
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.warn("[commands] focus failed", error);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const clearSelection = () => void highlighter.clear(selectName);
|
|
92
|
-
|
|
93
|
-
const COMMANDS: ViewerCommand[] = [
|
|
94
|
-
{
|
|
95
|
-
id: "focus",
|
|
96
|
-
label: "Focus selection",
|
|
97
|
-
icon: "mdi:image-filter-center-focus",
|
|
98
|
-
keys: ["F"],
|
|
99
|
-
match: (e) => e.key.toLowerCase() === "f" && noMods(e),
|
|
100
|
-
run: () => void focusSelection(),
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: "hide",
|
|
104
|
-
label: "Hide selected",
|
|
105
|
-
icon: "mdi:eye-off-outline",
|
|
106
|
-
keys: ["H"],
|
|
107
|
-
match: (e) => e.key.toLowerCase() === "h" && noMods(e),
|
|
108
|
-
run: () => h.hideSelected(),
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: "isolate",
|
|
112
|
-
label: "Isolate selected",
|
|
113
|
-
icon: "mdi:select-search",
|
|
114
|
-
keys: ["Shift", "H"],
|
|
115
|
-
match: (e) =>
|
|
116
|
-
e.key.toLowerCase() === "h" &&
|
|
117
|
-
e.shiftKey &&
|
|
118
|
-
!e.ctrlKey &&
|
|
119
|
-
!e.metaKey &&
|
|
120
|
-
!e.altKey,
|
|
121
|
-
run: () => h.isolateSelected(),
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
id: "ghost",
|
|
125
|
-
label: "Ghost selected",
|
|
126
|
-
icon: "mdi:ghost-outline",
|
|
127
|
-
keys: ["G"],
|
|
128
|
-
match: (e) => e.key.toLowerCase() === "g" && noMods(e),
|
|
129
|
-
run: () => h.ghostSelected(),
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
id: "showAll",
|
|
133
|
-
label: "Show all",
|
|
134
|
-
icon: "mdi:eye-outline",
|
|
135
|
-
keys: ["Shift", "A"],
|
|
136
|
-
match: (e) =>
|
|
137
|
-
e.key.toLowerCase() === "a" &&
|
|
138
|
-
e.shiftKey &&
|
|
139
|
-
!e.ctrlKey &&
|
|
140
|
-
!e.metaKey &&
|
|
141
|
-
!e.altKey,
|
|
142
|
-
run: () => h.showAll(),
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
id: "clear",
|
|
146
|
-
label: "Clear selection",
|
|
147
|
-
icon: "mdi:close",
|
|
148
|
-
keys: ["Esc"],
|
|
149
|
-
match: (e) => e.key === "Escape" && noMods(e),
|
|
150
|
-
run: () => clearSelection(),
|
|
151
|
-
},
|
|
152
|
-
];
|
|
153
|
-
|
|
154
|
-
// ── Global shortcut dispatch ───────────────────────────────────
|
|
155
|
-
const onKeyDown = (e: KeyboardEvent) => {
|
|
156
|
-
if (typingInField(e)) return;
|
|
157
|
-
for (const cmd of COMMANDS) {
|
|
158
|
-
if (cmd.match(e)) {
|
|
159
|
-
e.preventDefault();
|
|
160
|
-
cmd.run();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
window.addEventListener("keydown", onKeyDown);
|
|
166
|
-
|
|
167
|
-
// ── Panel UI (command-palette list; mirrors graphics-panel chrome) ──
|
|
168
|
-
const [panel, update] = BUI.Component.create<BUI.Panel, { query: string }>(
|
|
169
|
-
(state) => {
|
|
170
|
-
const q = state.query.trim().toLowerCase();
|
|
171
|
-
const rows = COMMANDS.filter((c) => !q || c.label.toLowerCase().includes(q));
|
|
172
|
-
return BUI.html`
|
|
173
|
-
<bim-panel
|
|
174
|
-
label="Commands"
|
|
175
|
-
icon="mdi:keyboard"
|
|
176
|
-
style="width: 100%; height: 100%; pointer-events: auto;"
|
|
177
|
-
>
|
|
178
|
-
<style>
|
|
179
|
-
.cmd-vp::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; }
|
|
180
|
-
.cmd-vp::-webkit-scrollbar-thumb { border-radius: 0.25rem; background-color: var(--bim-scrollbar--c, #3C3C41); }
|
|
181
|
-
.cmd-vp::-webkit-scrollbar-track { background-color: var(--bim-scrollbar--bgc, var(--bim-ui_bg-contrast-10)); }
|
|
182
|
-
.cmd-row { box-sizing: border-box; min-height: 32px; display: flex;
|
|
183
|
-
align-items: center; gap: 0.55rem; padding: 0.35rem 0.4rem 0.35rem 1.1rem;
|
|
184
|
-
font-size: 0.78rem; line-height: 1.3; cursor: pointer;
|
|
185
|
-
color: var(--bim-ui_bg-contrast-100, #e3e3e3);
|
|
186
|
-
border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
|
|
187
|
-
.cmd-row:hover { background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.08)); }
|
|
188
|
-
.cmd-ico { flex: 0 0 auto; color: #99a0ae; font-size: 0.95rem; }
|
|
189
|
-
.cmd-label { flex: 1 1 auto; min-width: 0; overflow: hidden;
|
|
190
|
-
text-overflow: ellipsis; white-space: nowrap; }
|
|
191
|
-
.cmd-keys { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 0.2rem; }
|
|
192
|
-
.cmd-kbd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
193
|
-
font-size: 0.66rem; line-height: 1; padding: 0.12rem 0.35rem;
|
|
194
|
-
border-radius: 0.25rem; color: var(--bim-ui_bg-contrast-80, #c9c9c9);
|
|
195
|
-
background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.08));
|
|
196
|
-
border: 1px solid var(--bim-ui_bg-contrast-40, rgba(255,255,255,0.18)); }
|
|
197
|
-
.cmd-plus { opacity: 0.4; font-size: 0.66rem; }
|
|
198
|
-
</style>
|
|
199
|
-
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
|
200
|
-
<div style="flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0.4rem 0.4rem 0.4rem 1.1rem; gap: 0.25rem;">
|
|
201
|
-
<bim-text-input
|
|
202
|
-
icon="mdi:magnify"
|
|
203
|
-
icon-inside
|
|
204
|
-
placeholder="Search commands…"
|
|
205
|
-
style="flex: 0 0 auto; width: 100%; margin: 0 0 0.25rem -0.35rem;"
|
|
206
|
-
@input=${(e: Event) => {
|
|
207
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
-
update({ query: String((e.target as any).value ?? "") });
|
|
209
|
-
}}
|
|
210
|
-
></bim-text-input>
|
|
211
|
-
<div style="flex: 0 0 auto; border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); margin: 0 -0.4rem 0.1rem -1.1rem;"></div>
|
|
212
|
-
<div class="cmd-vp" style="flex: 1 1 auto; min-height: 0; overflow-y: auto; margin-left: -1.1rem; margin-right: -0.4rem;">
|
|
213
|
-
${rows.length === 0
|
|
214
|
-
? BUI.html`<div style="padding: 0.75rem 1.1rem;"><bim-label style="opacity: 0.6;">No commands.</bim-label></div>`
|
|
215
|
-
: rows.map(
|
|
216
|
-
(c) => BUI.html`
|
|
217
|
-
<div class="cmd-row" title=${c.label} @click=${() => c.run()}>
|
|
218
|
-
<bim-icon class="cmd-ico" icon=${c.icon}></bim-icon>
|
|
219
|
-
<span class="cmd-label">${c.label}</span>
|
|
220
|
-
<span class="cmd-keys">
|
|
221
|
-
${c.keys.map(
|
|
222
|
-
(k, i) => BUI.html`${i > 0
|
|
223
|
-
? BUI.html`<span class="cmd-plus">+</span>`
|
|
224
|
-
: null}<span class="cmd-kbd">${k}</span>`,
|
|
225
|
-
)}
|
|
226
|
-
</span>
|
|
227
|
-
</div>`,
|
|
228
|
-
)}
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
</bim-panel>
|
|
233
|
-
`;
|
|
234
|
-
},
|
|
235
|
-
{ query: "" },
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
return panel;
|
|
239
|
-
};
|