@volter/project 0.0.0-stage → 0.5.204
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 +6 -0
- package/README.md +18 -3
- package/package.json +64 -4
- package/schemas/engine-api.json +57 -0
- package/schemas/engine-api.md +22 -0
- package/schemas/volter-project.schema.json +468 -0
- package/schemas/volter-settings.schema.json +149 -0
- package/src/adapter/adapter-module.ts +997 -0
- package/src/adapter/adapter-surface.ts +34 -0
- package/src/adapter/authoring-seam-contract.ts +224 -0
- package/src/adapter/authoring.ts +1336 -0
- package/src/adapter/binding.ts +445 -0
- package/src/adapter/debug-registry-ref.ts +19 -0
- package/src/adapter/editor-looks.ts +519 -0
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/registry.ts +116 -0
- package/src/adapter/frame-capture.ts +80 -0
- package/src/adapter/host-context.ts +114 -0
- package/src/adapter/index.ts +209 -0
- package/src/adapter/ingest/contract-system-slots.ts +28 -0
- package/src/adapter/ingest/game-contract-seams.ts +86 -0
- package/src/adapter/ingest/game-contract.ts +506 -0
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/loop-gate-report.ts +53 -0
- package/src/adapter/manifest-interpreter.ts +64 -0
- package/src/adapter/native-entry-surface.ts +100 -0
- package/src/adapter/physics-adapter-2d.ts +30 -0
- package/src/adapter/render-audio.ts +20 -0
- package/src/adapter/render-memory.ts +34 -0
- package/src/adapter/renderer-config.ts +84 -0
- package/src/adapter/root-adapter.ts +243 -0
- package/src/adapter/root-seam-contract.ts +61 -0
- package/src/adapter/root-stacking.ts +67 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/system-adapter.ts +865 -0
- package/src/adapter/system-seam-contract.ts +138 -0
- package/src/adapter/system-slot.ts +301 -0
- package/src/adapter/transform.ts +35 -0
- package/src/adapter/workspace-arrangement.ts +92 -0
- package/src/core/system-phase.ts +52 -0
- package/src/inspection.ts +281 -0
- package/src/manifest/configuration-kinds.ts +241 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +45 -0
- package/src/manifest/kind-modules.ts +58 -0
- package/src/manifest/load-file.ts +33 -0
- package/src/manifest/load.ts +380 -0
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/runtime-environment.ts +48 -0
- package/src/manifest/schema.ts +690 -0
- package/src/settings/keys.ts +152 -0
- package/src/settings/schema.ts +151 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE LOOK POINTS — a workspace layout, a keymap and a style bundle as DATA a
|
|
3
|
+
* package contributes (WORKBENCH.md §Contribution points: "Layouts, keymaps
|
|
4
|
+
* and styles are DATA points: they need no code to load, which is what makes
|
|
5
|
+
* a 'styled build' a manifest choice rather than a code change").
|
|
6
|
+
*
|
|
7
|
+
* A contribution module declares its point and exports ONE object:
|
|
8
|
+
*
|
|
9
|
+
* `model.layout.ts` export const point = 'workspace.layout'; export const layout: WorkspaceLayoutContribution
|
|
10
|
+
* `blender.keymap.ts` export const point = 'workspace.keymap'; export const keymap: KeymapContribution
|
|
11
|
+
* `blender.style.ts` export const point = 'workspace.style'; export const style: StyleContribution
|
|
12
|
+
*
|
|
13
|
+
* The editor registers what a project's declared packages contribute beside
|
|
14
|
+
* its own built-ins — Look, Animate and Design as workspaces, the volter keymap,
|
|
15
|
+
* the Classic/Glass/Maya/Substance bundles — and never asks which package is
|
|
16
|
+
* open: a build without the Blender look has no Model workspace.
|
|
17
|
+
*/
|
|
18
|
+
import type { WorkspaceArrangement } from './workspace-arrangement';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Every keyboard action the editor's chrome binds. A keymap binds these and
|
|
22
|
+
* nothing else; a game never reads this table (play-mode input isolation is a
|
|
23
|
+
* different mechanism).
|
|
24
|
+
*/
|
|
25
|
+
export type EditorKeyActionId =
|
|
26
|
+
| 'edit.undo'
|
|
27
|
+
| 'edit.redo'
|
|
28
|
+
| 'edit.copy'
|
|
29
|
+
| 'edit.cut'
|
|
30
|
+
| 'edit.paste'
|
|
31
|
+
| 'edit.duplicate'
|
|
32
|
+
| 'edit.group'
|
|
33
|
+
| 'edit.wrap'
|
|
34
|
+
| 'edit.unwrap'
|
|
35
|
+
| 'edit.selectAll'
|
|
36
|
+
| 'edit.deselectAll'
|
|
37
|
+
| 'edit.save'
|
|
38
|
+
| 'edit.delete'
|
|
39
|
+
| 'edit.enterScope'
|
|
40
|
+
| 'edit.exitScopeOrDeselect'
|
|
41
|
+
| 'edit.nudgeUp'
|
|
42
|
+
| 'edit.nudgeUpCoarse'
|
|
43
|
+
| 'edit.nudgeDown'
|
|
44
|
+
| 'edit.nudgeDownCoarse'
|
|
45
|
+
| 'edit.nudgeLeft'
|
|
46
|
+
| 'edit.nudgeLeftCoarse'
|
|
47
|
+
| 'edit.nudgeRight'
|
|
48
|
+
| 'edit.nudgeRightCoarse'
|
|
49
|
+
| 'view.resetPan'
|
|
50
|
+
| 'view.commandPalette'
|
|
51
|
+
| 'view.toggleConsole'
|
|
52
|
+
| 'view.focusMode'
|
|
53
|
+
| 'workspace.cycleNext'
|
|
54
|
+
| 'workspace.cyclePrevious'
|
|
55
|
+
/** Arm the tool that draws NO transform gizmo — Blender's Select Box, whose
|
|
56
|
+
* 3D View keymap puts it on `W` (`blender_default.py:1950-1955`,
|
|
57
|
+
* `op_tool_cycle("builtin.select_box", …)`). This editor's own keymap binds
|
|
58
|
+
* it to nothing: W/E/R/T are its four transform tools. */
|
|
59
|
+
| 'transform.select'
|
|
60
|
+
| 'transform.combined'
|
|
61
|
+
| 'transform.translate'
|
|
62
|
+
| 'transform.rotate'
|
|
63
|
+
| 'transform.scale'
|
|
64
|
+
| 'viewport.toggleSnap'
|
|
65
|
+
| 'viewport.frameSelection'
|
|
66
|
+
| 'viewport.cyclePivot'
|
|
67
|
+
| 'canvas.panMode'
|
|
68
|
+
| 'canvas.rulerMode'
|
|
69
|
+
| 'canvas.toggleGrid'
|
|
70
|
+
| 'viewport.vertexSnapHold'
|
|
71
|
+
| 'viewport.snapToFloor'
|
|
72
|
+
| 'view.top'
|
|
73
|
+
| 'view.front'
|
|
74
|
+
| 'view.right'
|
|
75
|
+
| 'view.bottom'
|
|
76
|
+
| 'view.back'
|
|
77
|
+
| 'view.left'
|
|
78
|
+
/** Step the view: orbit 15° about the world's up or the view's horizon, turn to the opposite
|
|
79
|
+
* side, or roll 15° (Blender's `view3d.view_orbit` and `view3d.view_roll` on the numpad). */
|
|
80
|
+
| 'view.orbitLeft'
|
|
81
|
+
| 'view.orbitRight'
|
|
82
|
+
| 'view.orbitUp'
|
|
83
|
+
| 'view.orbitDown'
|
|
84
|
+
| 'view.opposite'
|
|
85
|
+
| 'view.rollLeft'
|
|
86
|
+
| 'view.rollRight'
|
|
87
|
+
/** Frame everything in the view, selected or not (Blender's `view3d.view_all`, Home). */
|
|
88
|
+
| 'view.all'
|
|
89
|
+
/** Step the view nearer or farther by the keymap's zoom step (`view3d.zoom`, numpad +/-). */
|
|
90
|
+
| 'view.zoomIn'
|
|
91
|
+
| 'view.zoomOut'
|
|
92
|
+
| 'view.perspective'
|
|
93
|
+
/** Switch the view between perspective and orthographic, keeping where it looks from
|
|
94
|
+
* (Blender's `view3d.view_persportho`, numpad 5). */
|
|
95
|
+
| 'view.projection'
|
|
96
|
+
| 'view.camera';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One chord. `key` is matched case-insensitively against `KeyboardEvent.key`;
|
|
100
|
+
* `code` (when set) is matched against `KeyboardEvent.code` instead, for
|
|
101
|
+
* chords whose typed character varies by layout or modifier (Option+letter on
|
|
102
|
+
* macOS types a symbol). `mod` is ⌘ on macOS and Ctrl elsewhere.
|
|
103
|
+
*/
|
|
104
|
+
export interface KeyChord {
|
|
105
|
+
readonly key: string;
|
|
106
|
+
readonly code?: string;
|
|
107
|
+
readonly mod?: boolean;
|
|
108
|
+
/** The Control key itself on every platform, macOS included (Blender's Ctrl), where `mod`
|
|
109
|
+
* would be ⌘. */
|
|
110
|
+
readonly ctrl?: boolean;
|
|
111
|
+
readonly shift?: boolean;
|
|
112
|
+
readonly alt?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The chords a keymap binds for the actions it names. An action a keymap
|
|
116
|
+
* omits keeps the editor's own binding; an EMPTY list unbinds it. */
|
|
117
|
+
export type KeymapBindings = Partial<Record<EditorKeyActionId, readonly KeyChord[]>>;
|
|
118
|
+
|
|
119
|
+
export interface KeymapContribution {
|
|
120
|
+
/** The `keymap` settings value that selects it. */
|
|
121
|
+
readonly id: string;
|
|
122
|
+
readonly title: string;
|
|
123
|
+
readonly description: string;
|
|
124
|
+
readonly bindings: KeymapBindings;
|
|
125
|
+
/** Which mouse button orbits the viewport. Absent: the editor's own. */
|
|
126
|
+
readonly navigation?: KeymapNavigation;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* THE VIEWPORT'S MOUSE, as a keymap states it: the button that orbits. The editor's own orbits
|
|
131
|
+
* with the right button and pans with the middle; a keymap that orbits with the middle (Blender's
|
|
132
|
+
* `view3d.rotate` on MIDDLEMOUSE) pans with Shift and the same button, as that keymap does.
|
|
133
|
+
*/
|
|
134
|
+
export interface KeymapNavigation {
|
|
135
|
+
readonly orbit: 'middle' | 'right';
|
|
136
|
+
/**
|
|
137
|
+
* The orbit as a TURNTABLE that keeps the view's roll and may pass over the top (Blender's
|
|
138
|
+
* `view3d.rotate` in Turntable mode): a sideways drag spins the view about the world's up, a
|
|
139
|
+
* vertical one pitches it about the horizon, each at this angle per CSS pixel. Absent: the
|
|
140
|
+
* editor's own orbit, which holds the view level and stops at the poles.
|
|
141
|
+
*/
|
|
142
|
+
readonly turntable?: { readonly degreesPerPixel: number };
|
|
143
|
+
/**
|
|
144
|
+
* An orthographic view still down an axis turns perspective when a rotate starts, as Blender's
|
|
145
|
+
* Auto Perspective does (`ED_view3d_persp_ensure`); the axis views themselves are orthographic.
|
|
146
|
+
*/
|
|
147
|
+
readonly autoPerspective?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* ZOOM: `step` is the distance factor one zoom key moves by (Blender's `view_zoom_apply_step`,
|
|
150
|
+
* 1.2); `drag: 'dolly'` makes a zoom drag Blender's Dolly style (`viewzoom_scale_value`: the
|
|
151
|
+
* distance scales by `2 · (len / len₀ − 1) + 1`, `len` the pointer's height below the region's
|
|
152
|
+
* top plus 5). Absent: the editor's own.
|
|
153
|
+
*/
|
|
154
|
+
readonly zoom?: { readonly step: number; readonly drag?: 'dolly' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type WorkspaceLayoutRegions = NonNullable<WorkspaceArrangement['regions']>;
|
|
158
|
+
|
|
159
|
+
export interface WorkspaceLayoutContribution {
|
|
160
|
+
readonly id: string;
|
|
161
|
+
readonly title: string;
|
|
162
|
+
readonly description: string;
|
|
163
|
+
/** What the workspace needs the project to DECLARE before it applies:
|
|
164
|
+
* `'mounts'` (at least one root that plays) or a document kind the
|
|
165
|
+
* project's table lists. Omitted: every project. */
|
|
166
|
+
readonly requires?: 'mounts' | { readonly documentKind: string };
|
|
167
|
+
/** Which chrome regions the workspace shows; absent means every region. */
|
|
168
|
+
readonly regions?: WorkspaceLayoutRegions;
|
|
169
|
+
/** The workspace's EDITOR AREAS beside the centre document — a second
|
|
170
|
+
* editor group holding a `workspace.document` contribution, the shape
|
|
171
|
+
* Blender's own screens have (`WorkspaceAreaContribution`). Absent: the
|
|
172
|
+
* centre document alone. */
|
|
173
|
+
readonly areas?: WorkspaceArrangement['areas'];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* An OPAQUE material a style bundle carries: Classic's physics (opaque
|
|
178
|
+
* surfaces, conventional bars and panels) under its own corner radii and
|
|
179
|
+
* shadows. Chroma stays the palette's; Glass is the editor's own and is not
|
|
180
|
+
* contributable.
|
|
181
|
+
*/
|
|
182
|
+
export interface MaterialContribution {
|
|
183
|
+
readonly id: string;
|
|
184
|
+
readonly title: string;
|
|
185
|
+
readonly description: string;
|
|
186
|
+
/** Corner radii, smallest to largest, and the pill. */
|
|
187
|
+
readonly shape: {
|
|
188
|
+
readonly small: string;
|
|
189
|
+
readonly medium: string;
|
|
190
|
+
readonly large: string;
|
|
191
|
+
readonly full: string;
|
|
192
|
+
};
|
|
193
|
+
/** `box-shadow` values (`none` allowed) for raised widgets, menus, dialogs. */
|
|
194
|
+
readonly elevation: {
|
|
195
|
+
readonly small: string;
|
|
196
|
+
readonly medium: string;
|
|
197
|
+
readonly large: string;
|
|
198
|
+
};
|
|
199
|
+
/** How tight the chrome sits: control heights, the type scale and the
|
|
200
|
+
* chrome rows, in px. Every field is optional; absent ones keep the
|
|
201
|
+
* editor's own. */
|
|
202
|
+
readonly density?: DensityContribution;
|
|
203
|
+
/** What the look states about the 3D stage ({@link StageContribution}). */
|
|
204
|
+
readonly stage?: StageContribution;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface DensityContribution {
|
|
208
|
+
readonly control?: {
|
|
209
|
+
readonly compact?: number;
|
|
210
|
+
readonly default?: number;
|
|
211
|
+
readonly comfortable?: number;
|
|
212
|
+
};
|
|
213
|
+
readonly font?: {
|
|
214
|
+
readonly xs?: number;
|
|
215
|
+
readonly sm?: number;
|
|
216
|
+
readonly base?: number;
|
|
217
|
+
readonly md?: number;
|
|
218
|
+
readonly lg?: number;
|
|
219
|
+
readonly xl?: number;
|
|
220
|
+
readonly '2xl'?: number;
|
|
221
|
+
readonly heading?: number;
|
|
222
|
+
};
|
|
223
|
+
/** Glyph sizes, independent of the type scale: Blender's chrome glyphs are
|
|
224
|
+
* 14 px whatever the text around them measures. */
|
|
225
|
+
readonly icon?: {
|
|
226
|
+
readonly xs?: number;
|
|
227
|
+
readonly sm?: number;
|
|
228
|
+
readonly md?: number;
|
|
229
|
+
readonly lg?: number;
|
|
230
|
+
readonly xl?: number;
|
|
231
|
+
readonly '2xl'?: number;
|
|
232
|
+
};
|
|
233
|
+
readonly chrome?: {
|
|
234
|
+
readonly commandBar?: number;
|
|
235
|
+
readonly panelHeader?: number;
|
|
236
|
+
readonly localToolbar?: number;
|
|
237
|
+
readonly treeRow?: number;
|
|
238
|
+
/** How far one level of a tree steps right. Absent keeps the editor's own
|
|
239
|
+
* 14; Blender's Outliner steps by its ROW HEIGHT — a square grid, 20 —
|
|
240
|
+
* with the chevron owning one cell and the type glyph the next. */
|
|
241
|
+
readonly treeIndent?: number;
|
|
242
|
+
readonly statusBar?: number;
|
|
243
|
+
/** The tool rail: a tool's box, its width, the gap between groups. */
|
|
244
|
+
readonly toolSize?: number;
|
|
245
|
+
readonly toolWidth?: number;
|
|
246
|
+
readonly toolGap?: number;
|
|
247
|
+
/** How wide the seam between two docked AREAS is cut, in px. Absent
|
|
248
|
+
* keeps the editor's own hairline; Blender's groove measures 3.5. The
|
|
249
|
+
* colour is the palette's `color.boundary.area`. */
|
|
250
|
+
readonly areaSeam?: number;
|
|
251
|
+
/** THE EMBOSS beside that groove, as a percentage of WHITE mixed into the
|
|
252
|
+
* area's OWN fill — so it is one number and every area computes its own
|
|
253
|
+
* colour from it. Absent (or 0) paints nothing, which is what every skin
|
|
254
|
+
* but Blender does; Blender's measures 8.2. See `theme.ts`'s
|
|
255
|
+
* `areaEmbossValue` for the five fills it was solved against. */
|
|
256
|
+
readonly areaEmboss?: number;
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* WHAT A LOOK STATES ABOUT THE 3D STAGE, as opposed to about the chrome around it: how the
|
|
262
|
+
* stage's furniture is drawn (sizes, widths, the selection box's form). Absent members keep
|
|
263
|
+
* the editor's own. What the stage DOES — its boot tool, its box-select rule, its world's up
|
|
264
|
+
* axis — is its presentation's (`@volter/sdk/kit/viewport-presentation`), never the
|
|
265
|
+
* look's (ARCHITECTURE.md rule 7).
|
|
266
|
+
*/
|
|
267
|
+
export interface StageContribution {
|
|
268
|
+
/**
|
|
269
|
+
* THE TRANSFORM GIZMO'S SCREEN SIZE, in px per gizmo unit — Blender's
|
|
270
|
+
* `U.gizmo_size` (`DNA_userdef_types.h:1095`, default 75; Preferences ▸
|
|
271
|
+
* Viewport ▸ Gizmos ▸ Size, `rna_userdef.cc:5415-5419`).
|
|
272
|
+
*
|
|
273
|
+
* A gizmo is a CONSTANT SCREEN SIZE, never a fraction of the object or of
|
|
274
|
+
* the viewport: `wm_gizmo.cc:450-474` computes
|
|
275
|
+
* `scale_final = scale_basis · UI_SCALE_FAC · U.gizmo_size ·
|
|
276
|
+
* ED_view3d_pixel_size_no_ui_scale(rv3d, origin)`, and that last term is
|
|
277
|
+
* world units per device pixel while `UI_SCALE_FAC` is device pixels per
|
|
278
|
+
* UI pixel — so one gizmo unit is exactly `U.gizmo_size` CSS px at any
|
|
279
|
+
* display scale.
|
|
280
|
+
*
|
|
281
|
+
* Absent keeps the editor's own gizmo, which is a fraction of the
|
|
282
|
+
* VIEWPORT (three's `TransformControls` at `size` 1). What the number
|
|
283
|
+
* means on our side — three's handles are authored on a unit half
|
|
284
|
+
* Blender's — is `editor-viewport.ts`'s to know, and it says so there.
|
|
285
|
+
*/
|
|
286
|
+
readonly gizmoSize?: number;
|
|
287
|
+
/**
|
|
288
|
+
* THE GIZMO'S HANDLES AT REST AND HIGHLIGHTED. `gizmoOpacity` is a resting handle's opacity
|
|
289
|
+
* (a highlighted one is opaque): Blender's 0.6 (`transform_gizmo_3d.cc`,
|
|
290
|
+
* `gizmo_get_axis_color`), Godot's 0.9 (`editors/3d/manipulator_gizmo_opacity`), Unity's
|
|
291
|
+
* 0.93 (`Handles.cs`, the axis colours' alpha). A handle with no fixed highlight colour
|
|
292
|
+
* (`color.gizmo.hover`) highlights in its own axis colour with its saturation multiplied by
|
|
293
|
+
* `gizmoHighlightSaturation` and its value set to `gizmoHighlightValue`, each kept when
|
|
294
|
+
* absent: Blender keeps both, Godot's are 0.25 and 1 (`node_3d_editor_plugin.cpp`,
|
|
295
|
+
* `col.from_hsv(col.get_h(), col.get_s() * 0.25, 1.0, 1)`).
|
|
296
|
+
*/
|
|
297
|
+
readonly gizmoOpacity?: number;
|
|
298
|
+
/**
|
|
299
|
+
* THE MOVE ARROWS' SHAPE: `gizmoArrowLength` is how far an arrow's tip stands from the
|
|
300
|
+
* centre, in rotation-ring radii (three's own 1.2; Godot's 1.6, measured on
|
|
301
|
+
* `tuto_3d5.png` as a 142 px tip against a 90 px ring), and `gizmoArrowHead` scales its
|
|
302
|
+
* head (three's own 1; Godot's about 1.5). Which handles the tool offers is the view's
|
|
303
|
+
* (`interaction.transformHandles`), never the look's.
|
|
304
|
+
*/
|
|
305
|
+
readonly gizmoArrowLength?: number;
|
|
306
|
+
readonly gizmoArrowHead?: number;
|
|
307
|
+
/** THE ROTATION RINGS' THICKNESS, as a multiple of three's own (Godot's rings are about
|
|
308
|
+
* twice as thick, `tuto_3d5.png`). */
|
|
309
|
+
readonly gizmoRingWidth?: number;
|
|
310
|
+
/**
|
|
311
|
+
* THE NAVIGATION GIZMO'S FORM AND CORNER: lettered `balls` (the editor's own, Blender's and
|
|
312
|
+
* Godot's), `cones` round a centre cube (Unity's scene gizmo) or a lettered axis `triad`
|
|
313
|
+
* (Unreal's); `top-right` (the editor's own) or `bottom-left` (Unreal's). Whether a click on
|
|
314
|
+
* it turns the view is the view's (`overlays.navigation`).
|
|
315
|
+
*/
|
|
316
|
+
readonly navigationGizmo?: 'balls' | 'godot' | 'cones' | 'triad';
|
|
317
|
+
readonly navigationCorner?: 'top-right' | 'bottom-left';
|
|
318
|
+
/** The triad's size, a multiple of its own 24 px; line and letter stay inside the gizmo's
|
|
319
|
+
* 90 px box up to about 1.3 (Unreal's triad, letters included, is about 40 px). */
|
|
320
|
+
readonly navigationSize?: number;
|
|
321
|
+
readonly gizmoHighlightSaturation?: number;
|
|
322
|
+
readonly gizmoHighlightValue?: number;
|
|
323
|
+
/**
|
|
324
|
+
* THE FLOOR GRID'S LINES, in device pixels: the minor lines' width, the major lines' width,
|
|
325
|
+
* and how far the major lines' colour is carried past the minor's from the backdrop (1 draws
|
|
326
|
+
* both levels alike). A look that states none keeps the editor's own floor, one hairline
|
|
327
|
+
* level — the grid the kit drew before its floor was fitted to Blender. Blender's are 1.5,
|
|
328
|
+
* 2.25 and 39/21, measured across one scanline of `modeling-object-none.png`: the 1 m line
|
|
329
|
+
* 4 px at half rise plateauing at 83, the 10 m line 6 px plateauing at 101
|
|
330
|
+
* (`docs/VIEWPORT-STAGE.md`: widths are the look's, like colours).
|
|
331
|
+
*/
|
|
332
|
+
readonly gridLineWidth?: number;
|
|
333
|
+
readonly gridMajorWidth?: number;
|
|
334
|
+
readonly gridMajorContrast?: number;
|
|
335
|
+
/** THE AXIS LINES' WIDTH in device pixels (the editor's own and Blender's are 2, measured
|
|
336
|
+
* on `modeling-object-none.png`). Which axis lines show is the view's
|
|
337
|
+
* (`overlays.axes`), never the look's. */
|
|
338
|
+
readonly axisLineWidth?: number;
|
|
339
|
+
/**
|
|
340
|
+
* HOW THE SELECTION BOX MARK IS DRAWN (the view's `overlays.selection.box`): `corners`, the
|
|
341
|
+
* editor's own brackets, or `edges`, the full box (Godot's selection box); and its stroke in
|
|
342
|
+
* CSS px (the editor's own is 3; Godot's box is a hairline).
|
|
343
|
+
*/
|
|
344
|
+
readonly selectionBox?: 'corners' | 'edges';
|
|
345
|
+
/** Which axes the box is measured along: the world's (the editor's own) or the object's,
|
|
346
|
+
* so it turns with a rotated object (Godot's). */
|
|
347
|
+
readonly selectionBoxFrame?: 'world' | 'object';
|
|
348
|
+
/**
|
|
349
|
+
* THE SELECTION OUTLINE'S FORM: `soft`, the editor's own blurred halo, or `crisp`, a hard
|
|
350
|
+
* line `outlineWidth` device pixels wide (Blender's, Unity's and Unreal's are crisp); and
|
|
351
|
+
* whether the parts of the selection other objects hide are drawn too (`outlineHidden`,
|
|
352
|
+
* the editor's own; Unity's and Blender's are not).
|
|
353
|
+
*/
|
|
354
|
+
readonly outlineStyle?: 'soft' | 'crisp';
|
|
355
|
+
readonly outlineWidth?: number;
|
|
356
|
+
readonly outlineHidden?: boolean;
|
|
357
|
+
/** THE SELECTION WIRE'S OPACITY (the editor's own 0.5; Unity's blue wire is about 0.25,
|
|
358
|
+
* `SceneVisExVisible.png`). Its colour is the palette's `color.viewport.wire`. */
|
|
359
|
+
readonly wireOpacity?: number;
|
|
360
|
+
readonly selectionBoxWidth?: number;
|
|
361
|
+
/**
|
|
362
|
+
* THE BOX-SELECT OUTLINE'S DASH, in device pixels: each dash this long in the palette's
|
|
363
|
+
* `color.viewport.marqueeLine`, each gap as long in its `marqueeGap`. Absent draws the
|
|
364
|
+
* editor's own solid line. Blender's is 4: `wm_gesture_draw_rect` (`wm_gesture.cc`) draws the
|
|
365
|
+
* box with a `dash_width` of 8 framebuffer pixels at a `udash_factor` of 0.5, the first half
|
|
366
|
+
* of each period in its `color` and the second in its `color2`. What a box drag selects is
|
|
367
|
+
* the view's (`interaction.boxSelect`), never the look's.
|
|
368
|
+
*/
|
|
369
|
+
readonly marqueeDash?: number;
|
|
370
|
+
/** THE STAGE'S OWN CHROME: which overlay controls the viewport carries and where. See
|
|
371
|
+
* {@link StageChromeContribution}; absent keeps the editor's own set. */
|
|
372
|
+
readonly chrome?: StageChromeContribution;
|
|
373
|
+
/** The stage's controls in the target's words. See {@link StageWordsContribution}. */
|
|
374
|
+
readonly words?: StageWordsContribution;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* THE VIEWPORT'S OWN CHROME, as each target's viewport arranges it (read from the reference
|
|
379
|
+
* frames, `docs/VIEWPORT-STAGE.md`): a look places the stage's controls, it never adds one.
|
|
380
|
+
* Each member is independently optional; absent keeps the editor's own arrangement (Blender's:
|
|
381
|
+
* the view text top-left, the tool shelf, the display controls in the top-right corner).
|
|
382
|
+
*
|
|
383
|
+
* - `bar`: a row across the stage's top. `strip` is a flush panel band (Godot's 3D toolbar,
|
|
384
|
+
* Unity's Scene view toolbar); `pills` is a row of rounded pills floating over the view
|
|
385
|
+
* (Unreal's level viewport toolbar). Absent, there is no bar.
|
|
386
|
+
* - `viewName`: where the view's name is. `text` is Blender's lines at the top-left; `menu`
|
|
387
|
+
* a pill at the top-left that opens the view menu (Godot's "⋮ Perspective"); `gizmo` a label
|
|
388
|
+
* under the navigation gizmo that toggles the projection (Unity's "Persp"); `bar` the first
|
|
389
|
+
* pill of the bar, opening the view menu (Unreal's "Perspective").
|
|
390
|
+
* - `tools`: the transform tools on the `shelf` (Blender's, Unity's Tools overlay) or at the
|
|
391
|
+
* bar's `bar-start` (Godot) or `bar-end` (Unreal).
|
|
392
|
+
* - `display`: the display controls (shading, grid, helpers, lights) in the top-right
|
|
393
|
+
* `corner` or at the bar's `bar-start` (Unity, Unreal) or `bar-end` (Godot).
|
|
394
|
+
* - `transformControls`: the stage's transform controls (orientation, pivot, snap and its
|
|
395
|
+
* values) in the document's `header` (the editor's own, Blender's) or on the `bar` after the
|
|
396
|
+
* tools (Unreal's row: world/local, the snap values; Godot's: local space, snap).
|
|
397
|
+
*
|
|
398
|
+
* Whether the zoom and pan cluster and the camera readout are drawn at all is the view's
|
|
399
|
+
* (`overlays.navigationControls`, `overlays.cameraReadout`), not the look's.
|
|
400
|
+
*/
|
|
401
|
+
export interface StageChromeContribution {
|
|
402
|
+
readonly bar?: 'strip' | 'pills';
|
|
403
|
+
readonly viewName?: 'text' | 'menu' | 'gizmo' | 'bar';
|
|
404
|
+
readonly tools?: 'shelf' | 'bar-start' | 'bar-end';
|
|
405
|
+
readonly display?: 'corner' | 'bar-start' | 'bar-end';
|
|
406
|
+
readonly transformControls?: 'header' | 'bar';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* THE STAGE'S CONTROLS IN THE TARGET'S OWN WORDS: what the shading menu calls each mode
|
|
411
|
+
* (Unreal's `Lit`, Unity's `Shaded`, Godot's `Display Normal` for the editor's `solid`) and
|
|
412
|
+
* what the helpers menu is called, drawn as a word where named (Unreal's `Show`, Unity's
|
|
413
|
+
* `Gizmos`). A mode or menu the look does not name keeps the editor's own word and mark.
|
|
414
|
+
*/
|
|
415
|
+
export interface StageWordsContribution {
|
|
416
|
+
readonly shading?: Readonly<Partial<Record<StageShadingModeId, string>>>;
|
|
417
|
+
readonly helpers?: string;
|
|
418
|
+
}
|
|
419
|
+
/** The stage's shading modes, by id (`@volter/editor-threejs` `viewport-shading`). */
|
|
420
|
+
export type StageShadingModeId =
|
|
421
|
+
| 'solid'
|
|
422
|
+
| 'clay'
|
|
423
|
+
| 'unlit'
|
|
424
|
+
| 'wireframe'
|
|
425
|
+
| 'matcap'
|
|
426
|
+
| 'normals'
|
|
427
|
+
| 'overdraw'
|
|
428
|
+
| 'preview'
|
|
429
|
+
| 'rendered';
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A glyph's CATEGORY — the colour channel a set may carry per glyph, painted
|
|
433
|
+
* as `var(--volter-category-<tone>, currentColor)` so a palette that names no
|
|
434
|
+
* `color.category` group paints the glyph exactly as a monochrome one.
|
|
435
|
+
*
|
|
436
|
+
* The vocabulary is Blender's Properties-tab rail read as data: that rail
|
|
437
|
+
* groups by HUE, and at 14 px the hue is what tells one tab from another
|
|
438
|
+
* (`properties-object.png` — the shapes are near-indistinguishable). Each
|
|
439
|
+
* member is a MEASURED ink; see `EditorTheme.color.category` for the values
|
|
440
|
+
* and their source coordinates.
|
|
441
|
+
*/
|
|
442
|
+
export type IconCategoryTone =
|
|
443
|
+
| 'object'
|
|
444
|
+
| 'modifier'
|
|
445
|
+
| 'material'
|
|
446
|
+
| 'tool'
|
|
447
|
+
| 'operator'
|
|
448
|
+
| 'data'
|
|
449
|
+
// Blender's SCENE group and its COLLECTION, added 2026-09-19 (WORK.md
|
|
450
|
+
// §Blender in the tab is Blender, "Inspection parity", I2 decision 3). They
|
|
451
|
+
// are not a reading of the rail: every Blender icon declares its group in
|
|
452
|
+
// `UI_icons.hh` through a `DEF_ICON_<GROUP>` macro, and
|
|
453
|
+
// `interface_icons.cc:126-132` maps those onto the theme members
|
|
454
|
+
// `interface/resources.cc:1059-1078` reads — `DEF_ICON_SCENE` →
|
|
455
|
+
// `TH_ICON_SCENE` → `.tui.icon_scene` #cccccc
|
|
456
|
+
// (`userdef_default_theme.c:272`), `DEF_ICON_COLLECTION` →
|
|
457
|
+
// `.tui.icon_collection` #ffffff (`:273`). The Render, Output, View Layer
|
|
458
|
+
// and Scene tabs are `DEF_ICON_SCENE`; the Collection tab is
|
|
459
|
+
// `DEF_ICON_COLLECTION(GROUP)` (`UI_icons.hh:248`).
|
|
460
|
+
| 'scene'
|
|
461
|
+
| 'collection'
|
|
462
|
+
// The SELECTION TOOLS' marquee, which Blender bakes into their tool icons' geometry
|
|
463
|
+
// (`ops.generic.select_box`) rather than reading from a theme member.
|
|
464
|
+
| 'select';
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* An ICON SET a style bundle carries: glyphs keyed by the Font Awesome icon
|
|
468
|
+
* name each editor site names (`plus`, `xmark`, `trash`, `gear`, …), each one
|
|
469
|
+
* `path` painted with `currentColor` in its `viewBox` (default `0 0 16 16`).
|
|
470
|
+
* A set may be partial — a glyph it lacks paints the editor's own.
|
|
471
|
+
*
|
|
472
|
+
* `tone` and `tonedPath` are the COLOUR channel, and they are two shapes of
|
|
473
|
+
* one idea because Blender draws two shapes of it:
|
|
474
|
+
*
|
|
475
|
+
* - `tone` alone tints the WHOLE glyph — the Properties rail's category
|
|
476
|
+
* tabs and the outliner's type glyphs, where the mark IS the category.
|
|
477
|
+
* - `tone` with `tonedPath` splits the glyph in two: `path` stays neutral
|
|
478
|
+
* (`currentColor`) and `tonedPath` takes the category ink. That is
|
|
479
|
+
* exactly how an operator mark reads in `modeling-edit-none.png` — the
|
|
480
|
+
* cube stays white and only the OPERATED element is tinted — and a
|
|
481
|
+
* single per-glyph tone cannot express it.
|
|
482
|
+
*
|
|
483
|
+
* Either way an explicit `tone` prop at the SITE wins, so a de-emphasized
|
|
484
|
+
* (`dim`/`muted`) glyph stays de-emphasized.
|
|
485
|
+
*/
|
|
486
|
+
export interface IconSetContribution {
|
|
487
|
+
readonly id: string;
|
|
488
|
+
readonly title: string;
|
|
489
|
+
readonly glyphs: Readonly<
|
|
490
|
+
Record<
|
|
491
|
+
string,
|
|
492
|
+
{
|
|
493
|
+
readonly viewBox?: string;
|
|
494
|
+
readonly path: string;
|
|
495
|
+
readonly tone?: IconCategoryTone;
|
|
496
|
+
readonly tonedPath?: string;
|
|
497
|
+
}
|
|
498
|
+
>
|
|
499
|
+
>;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export interface StyleContribution {
|
|
503
|
+
readonly id: string;
|
|
504
|
+
readonly title: string;
|
|
505
|
+
/** A palette id the editor knows — a built-in, or the `palette` document
|
|
506
|
+
* this contribution carries. */
|
|
507
|
+
readonly paletteId: string;
|
|
508
|
+
/** A material id the editor knows — `classic`, `glass`, or the `material`
|
|
509
|
+
* this contribution carries. */
|
|
510
|
+
readonly materialId: string;
|
|
511
|
+
readonly material?: MaterialContribution;
|
|
512
|
+
/** The icon set this bundle applies — the `icons` it carries, or one the
|
|
513
|
+
* editor knows. Absent: the editor's own glyphs. */
|
|
514
|
+
readonly iconSetId?: string;
|
|
515
|
+
readonly icons?: IconSetContribution;
|
|
516
|
+
/** A v3 palette DOCUMENT (the same shape a person imports), registered into
|
|
517
|
+
* the editor's theme library with this bundle. */
|
|
518
|
+
readonly palette?: unknown;
|
|
519
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every finder answers with.
|
|
3
|
+
*
|
|
4
|
+
* A finder is a PURE function over project source/files with explicit inputs
|
|
5
|
+
* and outputs — it never reads a filesystem, never fetches, and never consults
|
|
6
|
+
* ambient state. What it cannot answer it says so in {@link FinderResult.notes}
|
|
7
|
+
* rather than degrading silently: "has none" and "nobody could look" are
|
|
8
|
+
* different facts, and only the second is a defect.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { DocumentEntry } from '../adapter-module';
|
|
12
|
+
|
|
13
|
+
export interface FinderResult {
|
|
14
|
+
/** Scene-table entries this finder produced, in discovery order. */
|
|
15
|
+
readonly entries: readonly DocumentEntry[];
|
|
16
|
+
/**
|
|
17
|
+
* The entry this finder's own answer makes the default, when its answer
|
|
18
|
+
* settles it UNAMBIGUOUSLY (one candidate). A finder never picks between
|
|
19
|
+
* candidates — that is the adapter author's declaration to make, and an
|
|
20
|
+
* absent default stays the honest fact that none was declared.
|
|
21
|
+
*/
|
|
22
|
+
readonly default?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Loud, non-fatal diagnostics: a selection identifier that is not in the
|
|
25
|
+
* source, an entrypoint whose bytes could not be read. Every note names the
|
|
26
|
+
* file it is about. The host surfaces these; it never swallows them.
|
|
27
|
+
*/
|
|
28
|
+
readonly notes: readonly string[];
|
|
29
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE FINDER REGISTRY — the open set of finders an adapter may SELECT by
|
|
3
|
+
* name (ARCHITECTURE-CORE §The editor protocol, "Documents, not scenes").
|
|
4
|
+
*
|
|
5
|
+
* A finder is a pure function from the project's gathered source to document
|
|
6
|
+
* entries, plus the Zod schema of its own parameters. The engine registers
|
|
7
|
+
* its two here (`./index.ts`); a contribution registers more through the
|
|
8
|
+
* same door. `adapter-module.ts` validates only the selection ENVELOPE; the
|
|
9
|
+
* parameters are checked against the finder's schema when the loader runs
|
|
10
|
+
* it — an unregistered name or a bad parameter refuses loudly, naming both.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ZodType } from 'zod';
|
|
14
|
+
import type { FinderSelection } from '../adapter-module';
|
|
15
|
+
import type { FinderResult } from './finder-result';
|
|
16
|
+
|
|
17
|
+
/** What a registration's selection type must carry: the name. A finder's own
|
|
18
|
+
* params interface satisfies this without an index signature. */
|
|
19
|
+
export type FinderSelectionEnvelope = { readonly finder: string };
|
|
20
|
+
|
|
21
|
+
export interface FinderRegistration<
|
|
22
|
+
S extends FinderSelectionEnvelope = FinderSelection,
|
|
23
|
+
I = unknown,
|
|
24
|
+
> {
|
|
25
|
+
readonly name: S['finder'];
|
|
26
|
+
/** The selection's full shape, `finder` included; `.strict()` by the
|
|
27
|
+
* same rule as every other adapter table. */
|
|
28
|
+
readonly schema: ZodType<S>;
|
|
29
|
+
readonly run: (selection: S, input: I) => FinderResult;
|
|
30
|
+
/**
|
|
31
|
+
* WHAT OF THE MATCHED PROJECT IT READS. `sources` (the default) is the text of every file
|
|
32
|
+
* under the selection's `include` globs (`input.sources`) and their paths; `files` is the
|
|
33
|
+
* paths alone (`input.files`). A finder over BINARY documents says `files`, so the host never
|
|
34
|
+
* fetches a file it could not hand over as text: `@volter/editor-blender`'s
|
|
35
|
+
* `modelsFromBlendFiles` lists `.blend` files, and reading each one through the text-source
|
|
36
|
+
* route asked a multi-megabyte binary of a route capped at 2 MiB of text — a
|
|
37
|
+
* `413 Payload Too Large` per model per resolve, for bytes nothing would have read.
|
|
38
|
+
*/
|
|
39
|
+
readonly reads?: 'sources' | 'files';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const registry = new Map<string, FinderRegistration>();
|
|
43
|
+
|
|
44
|
+
/** Register a finder. A second registration under one name replaces the
|
|
45
|
+
* first (a module re-evaluated on save), never throws. */
|
|
46
|
+
export function registerFinder<S extends FinderSelectionEnvelope, I>(
|
|
47
|
+
registration: FinderRegistration<S, I>,
|
|
48
|
+
): () => void {
|
|
49
|
+
const entry = registration as unknown as FinderRegistration;
|
|
50
|
+
registry.set(registration.name, entry);
|
|
51
|
+
return () => {
|
|
52
|
+
if (registry.get(registration.name) === entry) registry.delete(registration.name);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What a CONTRIBUTION exports to register a finder (ARCHITECTURE-CORE §The
|
|
58
|
+
* project model): a plain object a project module `editor/contributions/<name>.finder.ts`
|
|
59
|
+
* exports as `finder`, handed by the HOST to {@link registerContributedFinder}.
|
|
60
|
+
* A selection may carry `include` globs; the host then supplies the matching
|
|
61
|
+
* project sources on `input.sources`, which is how a finder reads the
|
|
62
|
+
* project without a file system.
|
|
63
|
+
*/
|
|
64
|
+
export type FinderContribution = FinderRegistration<FinderSelection, unknown>;
|
|
65
|
+
|
|
66
|
+
/** Register a contribution's finder after checking its shape by name. */
|
|
67
|
+
export function registerContributedFinder(value: unknown, source: string): () => void {
|
|
68
|
+
const record = (value ?? {}) as Partial<FinderContribution>;
|
|
69
|
+
const problems: string[] = [];
|
|
70
|
+
if (typeof record.name !== 'string' || record.name.length === 0)
|
|
71
|
+
problems.push('`name` must be a non-empty string');
|
|
72
|
+
const schema = record.schema as { safeParse?: unknown } | undefined;
|
|
73
|
+
if (!schema || typeof schema.safeParse !== 'function')
|
|
74
|
+
problems.push('`schema` must be a Zod schema');
|
|
75
|
+
if (typeof record.run !== 'function') problems.push('`run(selection, input)` must be a function');
|
|
76
|
+
if (record.reads !== undefined && record.reads !== 'sources' && record.reads !== 'files')
|
|
77
|
+
problems.push('`reads`, when given, must be `sources` or `files`');
|
|
78
|
+
if (problems.length > 0) throw new Error(`${source}: not a finder — ${problems.join('; ')}`);
|
|
79
|
+
return registerFinder(record as FinderContribution);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether the named finder reads its files' TEXT ({@link FinderRegistration.reads}). An
|
|
83
|
+
* unregistered name answers false: its selection refuses by name when it runs
|
|
84
|
+
* ({@link runRegisteredFinder}), so text fetched for it would be read by nothing — and a
|
|
85
|
+
* contribution still loading is exactly when a paths-only finder is not registered yet. */
|
|
86
|
+
export function finderReadsSources(name: string): boolean {
|
|
87
|
+
const registration = registry.get(name);
|
|
88
|
+
return registration !== undefined && registration.reads !== 'files';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every registered finder name, in registration order. */
|
|
92
|
+
export function registeredFinderNames(): readonly string[] {
|
|
93
|
+
return [...registry.keys()];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Validate a selection against its finder's schema and run it. */
|
|
97
|
+
export function runRegisteredFinder<I>(selection: FinderSelection, input: I): FinderResult {
|
|
98
|
+
const registration = registry.get(selection.finder);
|
|
99
|
+
if (!registration) {
|
|
100
|
+
const known = registeredFinderNames();
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Unknown finder ${JSON.stringify(selection.finder)} — registered: ${
|
|
103
|
+
known.length > 0 ? known.map((n) => `"${n}"`).join(', ') : 'none'
|
|
104
|
+
}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const parsed = registration.schema.safeParse(selection);
|
|
108
|
+
if (!parsed.success) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Finder ${JSON.stringify(selection.finder)}: ${parsed.error.issues
|
|
111
|
+
.map((issue) => `${issue.path.join('.') || '(selection)'}: ${issue.message}`)
|
|
112
|
+
.join('; ')}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return registration.run(parsed.data, input);
|
|
116
|
+
}
|