@volter/editor-sdk 0.5.57
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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +19 -0
- package/package.json +90 -0
- package/src/account.ts +210 -0
- package/src/chrome.ts +83 -0
- package/src/client.ts +1547 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +985 -0
- package/src/document-probe.ts +237 -0
- package/src/editor-view.ts +220 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1167 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +128 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +14 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +149 -0
- package/src/project-tool-catalog.ts +96 -0
- package/src/selection.tsx +108 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +19 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +333 -0
- package/src/session/discovery.ts +90 -0
- package/src/session/editor-brand.ts +73 -0
- package/src/session/editor-compatibility.ts +248 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +389 -0
- package/src/session/project-module-url.ts +245 -0
- package/src/session/registry-format.ts +203 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +116 -0
- package/src/session/workbench-locator.ts +650 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +97 -0
- package/src/types.ts +1581 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +149 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +24 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +129 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +140 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +245 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2633 -0
- package/src/widgets/z-index.ts +25 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneElement,
|
|
3
|
+
isValidElement,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { createPortal } from 'react-dom';
|
|
12
|
+
|
|
13
|
+
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
14
|
+
|
|
15
|
+
interface TooltipProps {
|
|
16
|
+
text: string;
|
|
17
|
+
/** Explicitly `| undefined`: a hint resolved from the active keymap
|
|
18
|
+
* (`keymap-presets.ts`) is absent when that keymap leaves the action
|
|
19
|
+
* unbound, and the tooltip simply omits the `<kbd>` rather than lie. */
|
|
20
|
+
hotkey?: string | undefined;
|
|
21
|
+
children: ReactElement;
|
|
22
|
+
position?: TooltipPosition;
|
|
23
|
+
delay?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TooltipCoordinates {
|
|
27
|
+
left: number;
|
|
28
|
+
top: number;
|
|
29
|
+
ready: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const VIEWPORT_GUTTER = 8;
|
|
33
|
+
const TOOLTIP_GAP = 6;
|
|
34
|
+
|
|
35
|
+
function opposite(position: TooltipPosition): TooltipPosition {
|
|
36
|
+
if (position === 'top') return 'bottom';
|
|
37
|
+
if (position === 'bottom') return 'top';
|
|
38
|
+
if (position === 'left') return 'right';
|
|
39
|
+
return 'left';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function placementFits(position: TooltipPosition, trigger: DOMRect, tooltip: DOMRect): boolean {
|
|
43
|
+
if (position === 'top') return trigger.top - tooltip.height - TOOLTIP_GAP >= VIEWPORT_GUTTER;
|
|
44
|
+
if (position === 'bottom')
|
|
45
|
+
return trigger.bottom + tooltip.height + TOOLTIP_GAP <= window.innerHeight - VIEWPORT_GUTTER;
|
|
46
|
+
if (position === 'left') return trigger.left - tooltip.width - TOOLTIP_GAP >= VIEWPORT_GUTTER;
|
|
47
|
+
return trigger.right + tooltip.width + TOOLTIP_GAP <= window.innerWidth - VIEWPORT_GUTTER;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function coordinatesFor(
|
|
51
|
+
position: TooltipPosition,
|
|
52
|
+
trigger: DOMRect,
|
|
53
|
+
tooltip: DOMRect,
|
|
54
|
+
): TooltipCoordinates {
|
|
55
|
+
let left = trigger.left + (trigger.width - tooltip.width) / 2;
|
|
56
|
+
let top = trigger.top + (trigger.height - tooltip.height) / 2;
|
|
57
|
+
if (position === 'top') top = trigger.top - tooltip.height - TOOLTIP_GAP;
|
|
58
|
+
else if (position === 'bottom') top = trigger.bottom + TOOLTIP_GAP;
|
|
59
|
+
else if (position === 'left') left = trigger.left - tooltip.width - TOOLTIP_GAP;
|
|
60
|
+
else left = trigger.right + TOOLTIP_GAP;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
left: Math.max(
|
|
64
|
+
VIEWPORT_GUTTER,
|
|
65
|
+
Math.min(left, window.innerWidth - tooltip.width - VIEWPORT_GUTTER),
|
|
66
|
+
),
|
|
67
|
+
top: Math.max(
|
|
68
|
+
VIEWPORT_GUTTER,
|
|
69
|
+
Math.min(top, window.innerHeight - tooltip.height - VIEWPORT_GUTTER),
|
|
70
|
+
),
|
|
71
|
+
ready: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Canonical editor tooltip.
|
|
77
|
+
*
|
|
78
|
+
* The popup is portaled to the editor theme root and positioned against the
|
|
79
|
+
* viewport. That lets it escape dock/panel overflow clipping without leaking
|
|
80
|
+
* outside the theme boundary. Pointer and keyboard focus both expose the same
|
|
81
|
+
* accessible description.
|
|
82
|
+
*/
|
|
83
|
+
export function Tooltip({
|
|
84
|
+
text,
|
|
85
|
+
hotkey,
|
|
86
|
+
children,
|
|
87
|
+
position = 'bottom',
|
|
88
|
+
delay = 400,
|
|
89
|
+
}: TooltipProps) {
|
|
90
|
+
const id = useId();
|
|
91
|
+
const wrapperRef = useRef<HTMLSpanElement>(null);
|
|
92
|
+
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
93
|
+
const timer = useRef<ReturnType<typeof setTimeout>>(null);
|
|
94
|
+
const hovered = useRef(false);
|
|
95
|
+
const focused = useRef(false);
|
|
96
|
+
const [visible, setVisible] = useState(false);
|
|
97
|
+
const [portalRoot, setPortalRoot] = useState<Element | null>(null);
|
|
98
|
+
const [coordinates, setCoordinates] = useState<TooltipCoordinates>({
|
|
99
|
+
left: 0,
|
|
100
|
+
top: 0,
|
|
101
|
+
ready: false,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
useLayoutEffect(() => {
|
|
105
|
+
setPortalRoot(wrapperRef.current?.closest('.vgai-editor-theme') ?? null);
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
useEffect(
|
|
109
|
+
() => () => {
|
|
110
|
+
if (timer.current) clearTimeout(timer.current);
|
|
111
|
+
},
|
|
112
|
+
[],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const hide = () => {
|
|
116
|
+
if (timer.current) clearTimeout(timer.current);
|
|
117
|
+
timer.current = null;
|
|
118
|
+
setVisible(false);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const hideIfInactive = () => {
|
|
122
|
+
if (!hovered.current && !focused.current) hide();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const show = (wait: number) => {
|
|
126
|
+
if (timer.current) clearTimeout(timer.current);
|
|
127
|
+
timer.current = setTimeout(() => {
|
|
128
|
+
setCoordinates((current) => ({ ...current, ready: false }));
|
|
129
|
+
setVisible(true);
|
|
130
|
+
}, wait);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
useLayoutEffect(() => {
|
|
134
|
+
if (!visible) return;
|
|
135
|
+
const update = () => {
|
|
136
|
+
const trigger = wrapperRef.current?.getBoundingClientRect();
|
|
137
|
+
const tooltip = tooltipRef.current?.getBoundingClientRect();
|
|
138
|
+
if (!trigger || !tooltip) return;
|
|
139
|
+
const resolved = placementFits(position, trigger, tooltip) ? position : opposite(position);
|
|
140
|
+
setCoordinates(coordinatesFor(resolved, trigger, tooltip));
|
|
141
|
+
};
|
|
142
|
+
update();
|
|
143
|
+
window.addEventListener('resize', update);
|
|
144
|
+
window.addEventListener('scroll', update, true);
|
|
145
|
+
return () => {
|
|
146
|
+
window.removeEventListener('resize', update);
|
|
147
|
+
window.removeEventListener('scroll', update, true);
|
|
148
|
+
};
|
|
149
|
+
}, [hotkey, position, text, visible]);
|
|
150
|
+
|
|
151
|
+
let child = children;
|
|
152
|
+
if (isValidElement<{ 'aria-describedby'?: string }>(children)) {
|
|
153
|
+
const describedBy = [children.props['aria-describedby'], visible ? id : null]
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.join(' ');
|
|
156
|
+
child = cloneElement(children, describedBy ? { 'aria-describedby': describedBy } : {});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<span
|
|
161
|
+
ref={wrapperRef}
|
|
162
|
+
className="vgai-tooltip-anchor"
|
|
163
|
+
onMouseEnter={() => {
|
|
164
|
+
hovered.current = true;
|
|
165
|
+
show(delay);
|
|
166
|
+
}}
|
|
167
|
+
onMouseLeave={() => {
|
|
168
|
+
hovered.current = false;
|
|
169
|
+
hideIfInactive();
|
|
170
|
+
}}
|
|
171
|
+
onFocusCapture={() => {
|
|
172
|
+
focused.current = true;
|
|
173
|
+
show(0);
|
|
174
|
+
}}
|
|
175
|
+
onBlurCapture={(event) => {
|
|
176
|
+
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
|
177
|
+
focused.current = false;
|
|
178
|
+
hideIfInactive();
|
|
179
|
+
}
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{child}
|
|
183
|
+
{visible &&
|
|
184
|
+
portalRoot &&
|
|
185
|
+
createPortal(
|
|
186
|
+
<div
|
|
187
|
+
ref={tooltipRef}
|
|
188
|
+
id={id}
|
|
189
|
+
role="tooltip"
|
|
190
|
+
className="vgai-tooltip"
|
|
191
|
+
style={{
|
|
192
|
+
left: coordinates.left,
|
|
193
|
+
top: coordinates.top,
|
|
194
|
+
visibility: coordinates.ready ? 'visible' : 'hidden',
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<span>{text}</span>
|
|
198
|
+
{hotkey && <kbd>{hotkey}</kbd>}
|
|
199
|
+
</div>,
|
|
200
|
+
portalRoot,
|
|
201
|
+
)}
|
|
202
|
+
</span>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NumberInput } from './NumberInput';
|
|
2
|
+
|
|
3
|
+
interface Vec3InputProps {
|
|
4
|
+
value: [number | null, number | null, number | null];
|
|
5
|
+
onChange: (value: [number, number, number]) => void;
|
|
6
|
+
/** Per-axis callback for multi-edit. When provided, takes priority over onChange. */
|
|
7
|
+
onAxisChange?: (axis: 0 | 1 | 2, value: number) => void;
|
|
8
|
+
step?: number;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** When set, each axis field gets `data-testid="{prefix}-x|y|z"` for e2e. */
|
|
11
|
+
testIdPrefix?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Vec3Input({
|
|
15
|
+
value,
|
|
16
|
+
onChange,
|
|
17
|
+
onAxisChange,
|
|
18
|
+
step = 0.1,
|
|
19
|
+
disabled = false,
|
|
20
|
+
testIdPrefix,
|
|
21
|
+
}: Vec3InputProps) {
|
|
22
|
+
const tid = (axis: 'x' | 'y' | 'z') => (testIdPrefix ? `${testIdPrefix}-${axis}` : undefined);
|
|
23
|
+
const handleAxis = (axis: 0 | 1 | 2) => (v: number) => {
|
|
24
|
+
if (onAxisChange) {
|
|
25
|
+
onAxisChange(axis, v);
|
|
26
|
+
} else {
|
|
27
|
+
const tuple: [number, number, number] = [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0];
|
|
28
|
+
tuple[axis] = v;
|
|
29
|
+
onChange(tuple);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Axis tint literals are deliberate (not tokenized): they mirror the
|
|
34
|
+
// viewport gizmo's R/G/B axis colors, a cross-tool convention (Unity/
|
|
35
|
+
// Blender) distinct from the semantic danger/success/accent tones.
|
|
36
|
+
return (
|
|
37
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
38
|
+
<NumberInput
|
|
39
|
+
label="X"
|
|
40
|
+
labelColor="#E06178"
|
|
41
|
+
value={value[0]}
|
|
42
|
+
onChange={handleAxis(0)}
|
|
43
|
+
step={step}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
style={{ flex: 1 }}
|
|
46
|
+
testId={tid('x')}
|
|
47
|
+
/>
|
|
48
|
+
<NumberInput
|
|
49
|
+
label="Y"
|
|
50
|
+
labelColor="#C2ED66"
|
|
51
|
+
value={value[1]}
|
|
52
|
+
onChange={handleAxis(1)}
|
|
53
|
+
step={step}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
style={{ flex: 1 }}
|
|
56
|
+
testId={tid('y')}
|
|
57
|
+
/>
|
|
58
|
+
<NumberInput
|
|
59
|
+
label="Z"
|
|
60
|
+
labelColor="#6BABF5"
|
|
61
|
+
value={value[2]}
|
|
62
|
+
onChange={handleAxis(2)}
|
|
63
|
+
step={step}
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
style={{ flex: 1 }}
|
|
66
|
+
testId={tid('z')}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { themeVars } from '../theme';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared semantic-tone triples for dismissible chrome banners
|
|
5
|
+
* (V-16): two sibling banners each hand-rolled an identical-shaped
|
|
6
|
+
* `{bg, border, fg}` triple as its own inline magic values — built from the
|
|
7
|
+
* same lightness formula, but two independent literals with no shared
|
|
8
|
+
* constant. Hoisted here so a future retouch of one can't silently drift
|
|
9
|
+
* from the other (the same class of bug V-4 already demonstrated happened
|
|
10
|
+
* to `PlayBar`'s button styles).
|
|
11
|
+
*
|
|
12
|
+
* These are tinted-panel triples, not `theme.ts`'s flat `danger`/`warn`
|
|
13
|
+
* scalars (`theme.ts` has no "dark tinted banner background" slot) — kept
|
|
14
|
+
* as their own small module rather than added to the shared token file.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface BannerTone {
|
|
18
|
+
bg: string;
|
|
19
|
+
border: string;
|
|
20
|
+
fg: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const TONE_ERROR: BannerTone = {
|
|
24
|
+
bg: themeVars.semantic.dangerFaint,
|
|
25
|
+
border: themeVars.semantic.danger,
|
|
26
|
+
fg: themeVars.semantic.danger,
|
|
27
|
+
};
|
|
28
|
+
export const TONE_WARNING: BannerTone = {
|
|
29
|
+
bg: themeVars.semantic.warningMuted,
|
|
30
|
+
border: themeVars.semantic.warning,
|
|
31
|
+
fg: themeVars.semantic.warning,
|
|
32
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared clamp-to-viewport positioning (P6 glass-native chrome, U6.5 F5/F11).
|
|
3
|
+
*
|
|
4
|
+
* A popup/panel that anchors to a pointer position (a right-click context menu)
|
|
5
|
+
* or otherwise sets its own absolute/fixed `top`/`left` can open partly — or
|
|
6
|
+
* entirely — off the visible window when the anchor sits near an edge. This
|
|
7
|
+
* utility takes the *desired* rect (position + measured size) and returns a
|
|
8
|
+
* shifted `{ top, left }` that keeps the whole rect inside the window: it never
|
|
9
|
+
* resizes, only slides the popup so
|
|
10
|
+
*
|
|
11
|
+
* top ≥ margin, bottom ≤ innerHeight − margin,
|
|
12
|
+
* left ≥ margin, right ≤ innerWidth − margin.
|
|
13
|
+
*
|
|
14
|
+
* If the rect is larger than the viewport on an axis the leading edge wins
|
|
15
|
+
* (the popup pins to `margin` and lets the far edge overflow) so the anchor /
|
|
16
|
+
* first items stay reachable rather than the popup vanishing upward or left.
|
|
17
|
+
*
|
|
18
|
+
* Keep this the single home for the clamp — do not re-derive the arithmetic at
|
|
19
|
+
* each call site.
|
|
20
|
+
*/
|
|
21
|
+
export interface ClampDesiredRect {
|
|
22
|
+
readonly top: number;
|
|
23
|
+
readonly left: number;
|
|
24
|
+
readonly width: number;
|
|
25
|
+
readonly height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ClampedPoint {
|
|
29
|
+
readonly top: number;
|
|
30
|
+
readonly left: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function clampRectToViewport(desired: ClampDesiredRect, margin = 8): ClampedPoint {
|
|
34
|
+
const viewportWidth = window.innerWidth;
|
|
35
|
+
const viewportHeight = window.innerHeight;
|
|
36
|
+
// Math.max(margin, …) guards the case where the popup is wider/taller than the
|
|
37
|
+
// window: the upper bound collapses to `margin`, pinning the leading edge.
|
|
38
|
+
const maxLeft = Math.max(margin, viewportWidth - desired.width - margin);
|
|
39
|
+
const maxTop = Math.max(margin, viewportHeight - desired.height - margin);
|
|
40
|
+
return {
|
|
41
|
+
left: Math.min(Math.max(desired.left, margin), maxLeft),
|
|
42
|
+
top: Math.min(Math.max(desired.top, margin), maxTop),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
|
2
|
+
import {
|
|
3
|
+
faArrowPointer,
|
|
4
|
+
faArrowsRotate,
|
|
5
|
+
faArrowsToDot,
|
|
6
|
+
faArrowUpFromBracket,
|
|
7
|
+
faBookOpen,
|
|
8
|
+
faBorderAll,
|
|
9
|
+
faBorderTopLeft,
|
|
10
|
+
faCamera,
|
|
11
|
+
faChartSimple,
|
|
12
|
+
faCheck,
|
|
13
|
+
faCircleExclamation,
|
|
14
|
+
faCircleHalfStroke,
|
|
15
|
+
faCircleInfo,
|
|
16
|
+
faCode,
|
|
17
|
+
faCompress,
|
|
18
|
+
faCrosshairs,
|
|
19
|
+
faCube,
|
|
20
|
+
faCubesStacked,
|
|
21
|
+
faDiagramProject,
|
|
22
|
+
faExpand,
|
|
23
|
+
faEye,
|
|
24
|
+
faEyeSlash,
|
|
25
|
+
faFile,
|
|
26
|
+
faFileCode,
|
|
27
|
+
faFileExport,
|
|
28
|
+
faFolder,
|
|
29
|
+
faFolderOpen,
|
|
30
|
+
faFolderTree,
|
|
31
|
+
faForwardStep,
|
|
32
|
+
faGripLines,
|
|
33
|
+
faHammer,
|
|
34
|
+
faHand,
|
|
35
|
+
faImage,
|
|
36
|
+
faLightbulb,
|
|
37
|
+
faList,
|
|
38
|
+
faLock,
|
|
39
|
+
faLockOpen,
|
|
40
|
+
faMagnet,
|
|
41
|
+
faMagnifyingGlass,
|
|
42
|
+
faMagnifyingGlassMinus,
|
|
43
|
+
faMagnifyingGlassPlus,
|
|
44
|
+
faMaximize,
|
|
45
|
+
faMusic,
|
|
46
|
+
faPause,
|
|
47
|
+
faPlay,
|
|
48
|
+
faPlus,
|
|
49
|
+
faPuzzlePiece,
|
|
50
|
+
faRotate,
|
|
51
|
+
faRotateLeft,
|
|
52
|
+
faRotateRight,
|
|
53
|
+
faScissors,
|
|
54
|
+
faSliders,
|
|
55
|
+
faStop,
|
|
56
|
+
faTable,
|
|
57
|
+
faTableCells,
|
|
58
|
+
faTrash,
|
|
59
|
+
faTriangleExclamation,
|
|
60
|
+
faUpDownLeftRight,
|
|
61
|
+
faUpRightAndDownLeftFromCenter,
|
|
62
|
+
faUsers,
|
|
63
|
+
faXmark,
|
|
64
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A definition under a name of OURS, carrying a Font Awesome drawing until an
|
|
68
|
+
* icon set supplies its own (see `editorIcons.modeling`). The name is the only
|
|
69
|
+
* thing that changes: `icon` — the width/height/path tuple every renderer
|
|
70
|
+
* actually paints — is the source definition's, so nothing moves on screen
|
|
71
|
+
* until a set keys the new name.
|
|
72
|
+
*/
|
|
73
|
+
function meshGlyph(iconName: string, source: IconDefinition): IconDefinition {
|
|
74
|
+
return { ...source, iconName: iconName as IconDefinition['iconName'] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Semantic icon registry for editor chrome. Components may still import a
|
|
79
|
+
* genuinely domain-specific Font Awesome definition, but recurring actions,
|
|
80
|
+
* content kinds, status, and viewport tools use these stable meanings.
|
|
81
|
+
*/
|
|
82
|
+
export const editorIcons = {
|
|
83
|
+
action: {
|
|
84
|
+
add: faPlus,
|
|
85
|
+
close: faXmark,
|
|
86
|
+
delete: faTrash,
|
|
87
|
+
search: faMagnifyingGlass,
|
|
88
|
+
refresh: faArrowsRotate,
|
|
89
|
+
undo: faRotateLeft,
|
|
90
|
+
redo: faRotateRight,
|
|
91
|
+
build: faHammer,
|
|
92
|
+
export: faFileExport,
|
|
93
|
+
select: faArrowPointer,
|
|
94
|
+
},
|
|
95
|
+
transport: {
|
|
96
|
+
play: faPlay,
|
|
97
|
+
pause: faPause,
|
|
98
|
+
restart: faRotateRight,
|
|
99
|
+
stop: faStop,
|
|
100
|
+
step: faForwardStep,
|
|
101
|
+
},
|
|
102
|
+
content: {
|
|
103
|
+
project: faFolderOpen,
|
|
104
|
+
folder: faFolder,
|
|
105
|
+
file: faFile,
|
|
106
|
+
source: faFileCode,
|
|
107
|
+
scene: faBookOpen,
|
|
108
|
+
entity: faCube,
|
|
109
|
+
component: faPuzzlePiece,
|
|
110
|
+
camera: faCamera,
|
|
111
|
+
light: faLightbulb,
|
|
112
|
+
image: faImage,
|
|
113
|
+
audio: faMusic,
|
|
114
|
+
data: faTable,
|
|
115
|
+
code: faCode,
|
|
116
|
+
stateMachine: faDiagramProject,
|
|
117
|
+
},
|
|
118
|
+
status: {
|
|
119
|
+
success: faCheck,
|
|
120
|
+
info: faCircleInfo,
|
|
121
|
+
warning: faTriangleExclamation,
|
|
122
|
+
error: faCircleExclamation,
|
|
123
|
+
visible: faEye,
|
|
124
|
+
hidden: faEyeSlash,
|
|
125
|
+
locked: faLock,
|
|
126
|
+
unlocked: faLockOpen,
|
|
127
|
+
},
|
|
128
|
+
/** The 3D viewport's navigation cluster (`ViewportFurniture`). */
|
|
129
|
+
viewport: {
|
|
130
|
+
zoomIn: faMagnifyingGlassPlus,
|
|
131
|
+
zoomOut: faMagnifyingGlassMinus,
|
|
132
|
+
pan: faHand,
|
|
133
|
+
frame: faExpand,
|
|
134
|
+
/**
|
|
135
|
+
* IT OWNS ITS NAME, for the reason `modeling` below states in full:
|
|
136
|
+
* Blender's mark for this toggle is a grid IN PERSPECTIVE — a trapezoid,
|
|
137
|
+
* measured 32x26 device on `modeling-edit-none.png`'s navigation capsule —
|
|
138
|
+
* while three other sites ask for `border-all` and all three mean a FLAT
|
|
139
|
+
* grid (the canvas viewport's grid toggle, the 3D overlay's, and the CSS
|
|
140
|
+
* border editor's "all borders"). Redrawing the shared name would have
|
|
141
|
+
* put a receding floor on a CSS border button. Font Awesome's `border-all`
|
|
142
|
+
* stays as the drawing, so a set without this key paints what it painted.
|
|
143
|
+
*/
|
|
144
|
+
projection: meshGlyph('viewport-projection', faBorderAll),
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* Mesh-modeling operators — the shelf glyphs a modeling document shows
|
|
148
|
+
* (Blender's toolbar column). One per operator the `mesh` capability ships;
|
|
149
|
+
* a contribution reaches them through `@volter/editor-sdk/widgets`.
|
|
150
|
+
*
|
|
151
|
+
* EACH OWNS ITS NAME. An icon set keys a glyph by `icon.iconName`
|
|
152
|
+
* (`EditorIcon` → `activeIconGlyph`), so while these pointed at
|
|
153
|
+
* `scissors`/`compress`/`grip-lines`/`table-cells` the operator mark and
|
|
154
|
+
* every OTHER site using that name were one drawing: redrawing Knife as a
|
|
155
|
+
* cut cube would have recut the editor's scissors everywhere. Blender draws
|
|
156
|
+
* an operator AS THE OPERATION ON A CUBE, which is meaningless as a generic
|
|
157
|
+
* mark, so the two must be separable — hence the `mesh-*` names below, which
|
|
158
|
+
* no other site asks for.
|
|
159
|
+
*
|
|
160
|
+
* Font Awesome's `IconPrefix`/`IconName` are closed string unions of ITS
|
|
161
|
+
* catalog, so a name of ours cannot typecheck as one: the prefix stays the
|
|
162
|
+
* source definition's (nothing looks a glyph up by prefix) and the name is
|
|
163
|
+
* asserted. The `icon` tuple stays the source's too, so a set that does not
|
|
164
|
+
* carry the `mesh-*` name still paints exactly what it painted before.
|
|
165
|
+
*/
|
|
166
|
+
modeling: {
|
|
167
|
+
extrude: meshGlyph('mesh-extrude', faArrowUpFromBracket),
|
|
168
|
+
inset: meshGlyph('mesh-inset', faCompress),
|
|
169
|
+
bevel: meshGlyph('mesh-bevel', faBorderTopLeft),
|
|
170
|
+
loopCut: meshGlyph('mesh-loop-cut', faGripLines),
|
|
171
|
+
knife: meshGlyph('mesh-knife', faScissors),
|
|
172
|
+
smooth: meshGlyph('mesh-smooth', faCircleHalfStroke),
|
|
173
|
+
shrinkFatten: meshGlyph('mesh-shrink-fatten', faUpRightAndDownLeftFromCenter),
|
|
174
|
+
subdivide: meshGlyph('mesh-subdivide', faTableCells),
|
|
175
|
+
},
|
|
176
|
+
tool: {
|
|
177
|
+
extension: faPuzzlePiece,
|
|
178
|
+
/**
|
|
179
|
+
* THE TRANSFORM TOOLS OF THE SHELF (`ToolStrip`) — Blender's Edit Mode
|
|
180
|
+
* toolbar group 2, in Blender's own order: Move, Rotate, Scale,
|
|
181
|
+
* Transform.
|
|
182
|
+
*
|
|
183
|
+
* Move and Rotate keep the shared Font Awesome names because the Blender
|
|
184
|
+
* set's drawings for `up-down-left-right` and `rotate` say the same thing
|
|
185
|
+
* at every other site that asks for them. Scale and Transform do not:
|
|
186
|
+
* they asked for `maximize` (Font Awesome's window-expand cross) and
|
|
187
|
+
* `arrows-to-dot` (a crosshair), which are generic meanings other
|
|
188
|
+
* surfaces can reasonably want, and Blender's marks for the two tools —
|
|
189
|
+
* a small square growing into a larger outlined one, and a square inside
|
|
190
|
+
* a broken ring of arrows — are not those meanings. Same rule as
|
|
191
|
+
* `modeling` above: a glyph that IS an operation owns its own name.
|
|
192
|
+
*/
|
|
193
|
+
/** Blender's group 1, Select Box — the tool that arms no gizmo. It shares
|
|
194
|
+
* `arrow-pointer` with the action set's `select` above, because the two
|
|
195
|
+
* mean the same thing and the Blender set already carries one drawing of
|
|
196
|
+
* it in Blender's idiom (`blender-icons.source.mjs`, under `-- tools --`).
|
|
197
|
+
* Blender's own mark is that pointer inside a dashed marquee; the marquee
|
|
198
|
+
* is what this glyph does not carry, and that is the one difference
|
|
199
|
+
* between the two shelves' first button. */
|
|
200
|
+
select: faArrowPointer,
|
|
201
|
+
move: faUpDownLeftRight,
|
|
202
|
+
rotate: faRotate,
|
|
203
|
+
scale: meshGlyph('tool-scale', faMaximize),
|
|
204
|
+
transform: meshGlyph('tool-transform', faArrowsToDot),
|
|
205
|
+
snap: faMagnet,
|
|
206
|
+
frame: faCrosshairs,
|
|
207
|
+
grid: faBorderAll,
|
|
208
|
+
profiler: faChartSimple,
|
|
209
|
+
},
|
|
210
|
+
/**
|
|
211
|
+
* A STATIC PANEL'S OWN GLYPH — what Blender puts in an area's EDITOR-TYPE
|
|
212
|
+
* WELL, and the only place this editor draws one (`workspace-regions.ts`'s
|
|
213
|
+
* `groupTabs`). `workspace-static-panels.ts` declares WHICH glyph per panel
|
|
214
|
+
* by name and takes {@link WorkspacePanelGlyphName} as a type-only import,
|
|
215
|
+
* so the inventory stays a dependency-free table and no panel can name a
|
|
216
|
+
* glyph this one does not resolve.
|
|
217
|
+
*
|
|
218
|
+
* TWO OF THESE ARE MEASURED AND FIVE ARE OURS, and the split is a fact
|
|
219
|
+
* about the reference frames rather than about effort. `outliner` and
|
|
220
|
+
* `properties` are Blender's own marks, read at native 2x off
|
|
221
|
+
* `outliner.png` and `modeling-edit-none.png`'s Properties area and drawn
|
|
222
|
+
* in `@vgai/blender`'s `blender-icons.source.mjs`, which carries the pixel
|
|
223
|
+
* edges. The other five have NO Blender reading at all: the reference set
|
|
224
|
+
* photographs the Layout, Modeling, Sculpting and Texture Paint workspaces
|
|
225
|
+
* — 3D View, Outliner, Properties, Timeline, Image Editor and the asset
|
|
226
|
+
* shelf — with every editor-type dropdown CLOSED, so it contains no File
|
|
227
|
+
* Browser, no Asset Browser, and no picture of the editors our Content,
|
|
228
|
+
* Library, Conversations, Project work and History panels correspond to.
|
|
229
|
+
* They reuse names the Blender set already draws, chosen here, so a well
|
|
230
|
+
* stays in that set's idiom instead of falling through to a Font Awesome
|
|
231
|
+
* silhouette beside two measured marks.
|
|
232
|
+
*/
|
|
233
|
+
panel: {
|
|
234
|
+
outliner: meshGlyph('outliner', faFolderTree),
|
|
235
|
+
properties: meshGlyph('properties', faSliders),
|
|
236
|
+
'folder-open': faFolderOpen,
|
|
237
|
+
'cubes-stacked': faCubesStacked,
|
|
238
|
+
users: faUsers,
|
|
239
|
+
list: faList,
|
|
240
|
+
'rotate-left': faRotateLeft,
|
|
241
|
+
},
|
|
242
|
+
} as const;
|
|
243
|
+
|
|
244
|
+
/** The glyph names a static panel may declare (`workspace-static-panels.ts`). */
|
|
245
|
+
export type WorkspacePanelGlyphName = keyof (typeof editorIcons)['panel'];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { fontWeight, space, themeVars } from '../theme';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared typography and metrics for panel titles and disclosure sections.
|
|
6
|
+
*
|
|
7
|
+
* The heights are the VARS, not `chromeSize`'s constants: a material may
|
|
8
|
+
* retune the chrome's density, and the constant is only the editor's own
|
|
9
|
+
* default. Read at module load it cannot learn the active material — the
|
|
10
|
+
* Blender material declares a 26px panel header and every Panel still drew
|
|
11
|
+
* the default 30, because `--vgai-panel-header-height` (which does carry the
|
|
12
|
+
* material's value) was not what this style read.
|
|
13
|
+
*/
|
|
14
|
+
export const panelHeaderLabelStyle: React.CSSProperties = {
|
|
15
|
+
minHeight: 'var(--vgai-panel-header-height)',
|
|
16
|
+
padding: `0 ${space[4]}px`,
|
|
17
|
+
color: themeVars.content.muted,
|
|
18
|
+
fontSize: 'var(--vgai-font-base)',
|
|
19
|
+
fontWeight: fontWeight.semibold,
|
|
20
|
+
textTransform: 'uppercase',
|
|
21
|
+
display: 'flex',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
gap: space[2],
|
|
24
|
+
userSelect: 'none',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Disclosure rows are one density step below full panel headers. */
|
|
28
|
+
export const sectionHeaderLabelStyle: React.CSSProperties = {
|
|
29
|
+
...panelHeaderLabelStyle,
|
|
30
|
+
minHeight: 'var(--vgai-local-toolbar-height)',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Complete header rail treatment for a standalone panel. Glass treatments
|
|
34
|
+
* emit a transparent structural surface; opaque/reduced-transparency themes
|
|
35
|
+
* retain their occluding stack through the same variable. Headers never
|
|
36
|
+
* own the content-frost filter. */
|
|
37
|
+
export const panelHeaderStyle: React.CSSProperties = {
|
|
38
|
+
...panelHeaderLabelStyle,
|
|
39
|
+
background: 'var(--vgai-surface-sticky)',
|
|
40
|
+
borderBottom: '1px solid var(--vgai-structural-divider)',
|
|
41
|
+
flexShrink: 0,
|
|
42
|
+
};
|