@volter/editor-blender 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1409 -0
- package/README.md +17 -0
- package/contributions/blender-header-menus.tsx +483 -0
- package/contributions/blender-icon-trace.mjs +403 -0
- package/contributions/blender-icons.source.mjs +2925 -0
- package/contributions/blender-node-editor.document.tsx +1402 -0
- package/contributions/blender-node-geometry.ts +1138 -0
- package/contributions/blender-node-panels.source.mjs +485 -0
- package/contributions/blender-outliner-authoring.ts +1729 -0
- package/contributions/blender-outliner-model.ts +389 -0
- package/contributions/blender-palette.source.mjs +319 -0
- package/contributions/blender-properties-model.ts +351 -0
- package/contributions/blender-properties-tab.tsx +100 -0
- package/contributions/blender-properties-view.tsx +1191 -0
- package/contributions/blender-runtime-skin.ts +619 -0
- package/contributions/blender-runtime.document.tsx +232 -0
- package/contributions/blender-timeline-geometry.ts +323 -0
- package/contributions/blender-timeline.document.tsx +1056 -0
- package/contributions/blender-uv-editor.document.tsx +483 -0
- package/contributions/blender-uv-geometry.ts +305 -0
- package/contributions/blender-version.status.tsx +93 -0
- package/contributions/blender.command.ts +102 -0
- package/contributions/blender.icons.json +1247 -0
- package/contributions/blender.icons.traced.json +1561 -0
- package/contributions/blender.keymap.ts +39 -0
- package/contributions/blender.node-panels.json +2436 -0
- package/contributions/blender.palette.json +93 -0
- package/contributions/blender.status.tsx +263 -0
- package/contributions/blender.style.ts +271 -0
- package/contributions/model.layout.ts +53 -0
- package/contributions/models.finder.ts +59 -0
- package/contributions/properties-bone-constraints.inspector.tsx +50 -0
- package/contributions/properties-bone.inspector.tsx +184 -0
- package/contributions/properties-collection.inspector.tsx +96 -0
- package/contributions/properties-constraints.inspector.tsx +69 -0
- package/contributions/properties-data.inspector.tsx +229 -0
- package/contributions/properties-material.inspector.tsx +121 -0
- package/contributions/properties-modifiers.inspector.tsx +74 -0
- package/contributions/properties-object.inspector.tsx +215 -0
- package/contributions/properties-output.inspector.tsx +210 -0
- package/contributions/properties-particles.inspector.tsx +494 -0
- package/contributions/properties-physics.inspector.tsx +614 -0
- package/contributions/properties-render.inspector.tsx +446 -0
- package/contributions/properties-scene.inspector.tsx +174 -0
- package/contributions/properties-texture.inspector.tsx +300 -0
- package/contributions/properties-view-layer.inspector.tsx +145 -0
- package/contributions/properties-world.inspector.tsx +130 -0
- package/contributions/sculpt.layout.ts +25 -0
- package/contributions/shading.layout.ts +99 -0
- package/contributions/texture.layout.ts +16 -0
- package/contributions/uv-editing.layout.ts +93 -0
- package/host/blender-runtime-host.ts +1256 -0
- package/package.json +77 -0
- package/src/layouts.tsx +48 -0
- package/src/looks.ts +14 -0
- package/src/node-view-state.ts +125 -0
- package/src/timeline-view-state.ts +154 -0
- package/src/uv-view-state.ts +125 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE UV EDITOR'S GEOMETRY AND COLOUR, every constant read from Blender's own
|
|
3
|
+
* source at the engine's pin (5.2.0, `fbe6228777e7`) — WORK.md §Blender in the
|
|
4
|
+
* tab is Blender, "Inspection parity", I5; ARCHITECTURE-CORE §Blender north
|
|
5
|
+
* star ("the reference is Blender's SOURCE as well as its frames").
|
|
6
|
+
*
|
|
7
|
+
* The drawing this describes is `MeshUVs` — the overlay class that REPLACED
|
|
8
|
+
* the 5.2-era `overlay_edit_uv.cc`, which is why that filename does not exist
|
|
9
|
+
* in the checkout — at `draw/engines/overlay/overlay_mesh.hh:483-760`, plus
|
|
10
|
+
* the four shaders it binds
|
|
11
|
+
* (`shaders/overlay_edit_uv_{edges_frag,verts_vert,verts_frag,faces_vert,face_dots_vert}.glsl`),
|
|
12
|
+
* `space_image.cc`'s `image_create` for the space's own defaults and
|
|
13
|
+
* `userdef_default_theme.c`'s `.space_image` for the colours.
|
|
14
|
+
*
|
|
15
|
+
* ## What is NOT mode-gated, and why every flag below reads "unselected"
|
|
16
|
+
*
|
|
17
|
+
* Blender draws UVs only when `show_uv_edit_ = space_mode_is_uv &&
|
|
18
|
+
* object_mode_is_edit` (`overlay_mesh.hh:593`). Both halves belong to its UI
|
|
19
|
+
* layer, which this editor never runs (orchestrator ruling 1, 2026-09-19:
|
|
20
|
+
* "inspection is not mode-gated"). So the view reads the DATA in whatever mode
|
|
21
|
+
* the engine is in and names the mode in its status line.
|
|
22
|
+
*
|
|
23
|
+
* The same ruling's second half decides the selection colours: "the view reads
|
|
24
|
+
* the flags the data carries and draws everything unselected when the flags
|
|
25
|
+
* are absent". MEASURED at this pin: `MeshUVLoopLayer`
|
|
26
|
+
* (`makesrna/intern/rna_mesh.cc:2380-2456`) declares `uv`, `pin`, `name`,
|
|
27
|
+
* `active`, `active_render` and `active_clone` — and NO vertex or edge
|
|
28
|
+
* selection at all, because UV selection lives in the BMesh an edit-mode
|
|
29
|
+
* session holds. `pin` IS declared, so pins are drawn; everything else takes
|
|
30
|
+
* its unselected branch, and the SELECTED constants below are recorded beside
|
|
31
|
+
* them rather than dropped, because a colour that is never reached is still
|
|
32
|
+
* the specification of the one that is.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// ------------------------------------------------------------------- colours
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `.space_image` (`release/datafiles/userdef/userdef_default_theme.c:578-608`),
|
|
39
|
+
* whole. Alpha is carried where Blender carries it: `face` is `0x0a` and
|
|
40
|
+
* `face_select` `0x3c` out of 255, which is what makes a UV island read as a
|
|
41
|
+
* wash rather than a fill.
|
|
42
|
+
*/
|
|
43
|
+
export const UV_THEME = {
|
|
44
|
+
/** `.back` `RGBA(0x30303000)` (`:580`) — alpha ZERO, so the space paints no
|
|
45
|
+
* backdrop of its own and the region's own surface shows. The node editor's
|
|
46
|
+
* `.back` is the same shape (`:661`), and U8's ruling 2 is what gives this
|
|
47
|
+
* view a surface colour of its own instead. */
|
|
48
|
+
back: '#303030',
|
|
49
|
+
backAlpha: 0,
|
|
50
|
+
/** `.grid` `RGBA(0x303030ff)` (`:586`) — FULL alpha, unlike the node
|
|
51
|
+
* editor's. See `UV_GRID_OPEN` below for what that measures to. */
|
|
52
|
+
grid: '#303030',
|
|
53
|
+
/** `.wire_edit` (`:587`) — the unselected edge under the OUTLINE line style,
|
|
54
|
+
* halved by the shader when `use_edge_select` is on
|
|
55
|
+
* (`overlay_edit_uv_edges_frag.glsl:27`). */
|
|
56
|
+
wireEdit: '#c0c0c0',
|
|
57
|
+
/** `.vertex_select` (`:588`) — `theme.colors.vert_select`, the fill of a
|
|
58
|
+
* selected UV vertex (`overlay_edit_uv_verts_vert.glsl:20`). */
|
|
59
|
+
vertexSelect: '#ff8500',
|
|
60
|
+
/** `.edge_select` (`:589`) — mixed into every line style by
|
|
61
|
+
* `selection_fac` (`overlay_edit_uv_edges_frag.glsl:31,41,46,50`). */
|
|
62
|
+
edgeSelect: '#ff8500',
|
|
63
|
+
/** `.face` `RGBA(0xffffff0a)` (`:590`) — the unselected island wash. */
|
|
64
|
+
face: '#ffffff',
|
|
65
|
+
faceAlpha: 0x0a / 255,
|
|
66
|
+
/** `.face_select` `RGBA(0xff85003c)` (`:591`). */
|
|
67
|
+
faceSelect: '#ff8500',
|
|
68
|
+
faceSelectAlpha: 0x3c / 255,
|
|
69
|
+
/** `.editmesh_active` `RGBA(0xffffff40)` (`:596`) — the ACTIVE face, which
|
|
70
|
+
* `overlay_edit_uv_faces_vert.glsl:25` puts over the selected colour. */
|
|
71
|
+
editmeshActive: '#ffffff',
|
|
72
|
+
editmeshActiveAlpha: 0x40 / 255,
|
|
73
|
+
/** `.uv_shadow` (`:606`) — the SHADOW line style, which is what the
|
|
74
|
+
* wireframe pass draws outside UV edit mode
|
|
75
|
+
* (`edit_uv_line_style_from_space_image`, `overlay_mesh.hh:1091-1111`).
|
|
76
|
+
* **That is this view's line style**, because this view is never in UV edit
|
|
77
|
+
* mode: `sima->mode == SI_MODE_UV` is a space flag, and there is no space. */
|
|
78
|
+
uvShadow: '#707070',
|
|
79
|
+
/** `.preview_back` (`:601`). Recorded whole because the theme block is, and
|
|
80
|
+
* named as NOT USED here: it is `TH_PREVIEW_BACK`, the backdrop behind a
|
|
81
|
+
* node/texture preview, not the UV tile's. */
|
|
82
|
+
previewBack: '#727272',
|
|
83
|
+
/** `TH_VERTEX` and `TH_FACEDOT` and `TH_WIRE` are NOT DECLARED in
|
|
84
|
+
* `.space_image` — checked key by key against `:578-608`. `resources.cc`
|
|
85
|
+
* resolves each straight off the active space's `ThemeSpace`
|
|
86
|
+
* (`:407-408`, `:361-362`, and the facedot case beside them), so the C
|
|
87
|
+
* struct's zero stands and all three are BLACK — the same reading I4
|
|
88
|
+
* recorded for `TH_VERTEX_UNREFERENCED`. Both shaders force alpha to 1 over
|
|
89
|
+
* that zero (`…verts_vert.glsl:19`: `float4(color.rgb, 1.0f)`;
|
|
90
|
+
* `…face_dots_vert.glsl:18`: `float4(theme.colors.wire.rgb, 1.0f)`), so an
|
|
91
|
+
* unselected UV vertex and an unselected face dot are opaque black. */
|
|
92
|
+
vertex: '#000000',
|
|
93
|
+
faceDot: '#000000',
|
|
94
|
+
wire: '#000000',
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
97
|
+
/** `.space_image`'s SIZES, the same block (`:592-595`). */
|
|
98
|
+
export const UV_SIZES = {
|
|
99
|
+
/** `.vertex_size = 3` (`:592`). */
|
|
100
|
+
vertexSize: 3,
|
|
101
|
+
/** `.edge_width = 1` (`:593`). */
|
|
102
|
+
edgeWidth: 1,
|
|
103
|
+
/** `.outline_width = 1` (`:594`). */
|
|
104
|
+
outlineWidth: 1,
|
|
105
|
+
/** `.facedot_size = 3` (`:595`). */
|
|
106
|
+
facedotSize: 3,
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
// ------------------------------------------------------------------ geometry
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `UI_SCALE_FAC` is 1 here, for the reason I5's node work already measured:
|
|
113
|
+
* `U.widget_unit` is `int(roundf(18 * scale_factor)) + 2 * pixelsize` = 20 at
|
|
114
|
+
* scale 1 (`windowmanager/intern/wm_window.cc:779`), which is the value every
|
|
115
|
+
* node constant divides by — so the session's scale factor is 1 and every
|
|
116
|
+
* `* UI_SCALE_FAC` below is the identity.
|
|
117
|
+
*/
|
|
118
|
+
export const UI_SCALE_FAC = 1;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A UV VERTEX DOT'S DIAMETER IN PIXELS.
|
|
122
|
+
* `(TH_VERTEX_SIZE * UI_SCALE_FAC + 1.5) * √2` (`overlay_mesh.hh:709-712`) —
|
|
123
|
+
* 3 → **6.364**. The `√2` is there because the point sprite is a SQUARE whose
|
|
124
|
+
* inscribed circle must still hold the dot after the outline, and the 1.5 is
|
|
125
|
+
* the outline's own room.
|
|
126
|
+
*/
|
|
127
|
+
export const UV_VERT_DOT_SIZE = (UV_SIZES.vertexSize * UI_SCALE_FAC + 1.5) * Math.SQRT2;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The outline ring inside that dot: `outline_width` 0.75 pushed as a shader
|
|
131
|
+
* constant (`overlay_mesh.hh:713`) — NOT the theme's `.outline_width` 1, which
|
|
132
|
+
* is a different number for a different mark, and the two are recorded
|
|
133
|
+
* together here because reading one for the other is the obvious mistake.
|
|
134
|
+
* `…verts_vert.glsl:33-39` turns it into four concentric radii; at
|
|
135
|
+
* `radius = dot_size/2` the pure fill ends at `radius - 0.75 - 1`.
|
|
136
|
+
*/
|
|
137
|
+
export const UV_VERT_OUTLINE_WIDTH = 0.75;
|
|
138
|
+
|
|
139
|
+
/** A FACE DOT'S DIAMETER: `TH_FACEDOT_SIZE * UI_SCALE_FAC`
|
|
140
|
+
* (`overlay_mesh.hh:727-729`) — 3, with no `√2` and no outline. */
|
|
141
|
+
export const UV_FACEDOT_SIZE = UV_SIZES.facedotSize * UI_SCALE_FAC;
|
|
142
|
+
|
|
143
|
+
/** A DASHED EDGE'S PERIOD: `4.0f * UI_SCALE_FAC` (`overlay_mesh.hh:670`) — 4
|
|
144
|
+
* pixels, of which the shader inks the first HALF
|
|
145
|
+
* (`…edges_frag.glsl:38`: `fract(line_distance / dash_length) < 0.5`). */
|
|
146
|
+
export const UV_DASH_LENGTH = 4 * UI_SCALE_FAC;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* THE THREE OPACITIES, and all three default to 1.0 —
|
|
150
|
+
* `image_create` (`editors/space_image/space_image.cc:116-118`) sets
|
|
151
|
+
* `uv_opacity`, `uv_face_opacity` and `uv_edge_opacity` to `1.0f` each.
|
|
152
|
+
*
|
|
153
|
+
* WHICH ONE APPLIES IS THE MODE QUESTION AGAIN: the face pass takes
|
|
154
|
+
* `uv_opacity` when `object_mode_is_edit && space_mode_is_uv` and
|
|
155
|
+
* `uv_face_opacity` otherwise (`overlay_mesh.hh:741-743`), and the wireframe
|
|
156
|
+
* pass always takes `uv_edge_opacity` (`:672`). Since all three are 1.0 the
|
|
157
|
+
* branch cannot change a pixel here, which is why the view takes the
|
|
158
|
+
* NOT-in-edit-mode branch (the honest one) and the number is the same.
|
|
159
|
+
*
|
|
160
|
+
* The one thing the branch DOES change is the final multiply: an object that
|
|
161
|
+
* is not the active edit-mode object has its alpha quartered
|
|
162
|
+
* (`…edges_frag.glsl:71`, `…faces_vert.glsl:26`: `alpha * 0.25`). The view
|
|
163
|
+
* draws ONE object — the active one — so it takes the full-alpha branch, and
|
|
164
|
+
* says so.
|
|
165
|
+
*/
|
|
166
|
+
export const UV_OPACITY = 1;
|
|
167
|
+
export const UV_FACE_OPACITY = 1;
|
|
168
|
+
export const UV_EDGE_OPACITY = 1;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* THE TILE GRID. `image_create` (`space_image.cc:129-133`) sets
|
|
172
|
+
* `tile_grid_shape` to `1, 1` and `custom_grid_subdiv` to `10, 10`, so the
|
|
173
|
+
* stock drawing is the 0–1 tile with a ten-by-ten subdivision. The step is
|
|
174
|
+
* `1 / custom_grid_subdiv[i]` under `SI_GRID_SHAPE_FIXED`
|
|
175
|
+
* (`image_draw.cc:575-576`).
|
|
176
|
+
*/
|
|
177
|
+
export const UV_TILE_GRID: readonly [number, number] = [1, 1];
|
|
178
|
+
export const UV_GRID_SUBDIV: readonly [number, number] = [10, 10];
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* THE GRID'S VISIBILITY IS THE ONE THING THE SOURCE HERE CANNOT SETTLE, and it
|
|
182
|
+
* is named rather than guessed at.
|
|
183
|
+
*
|
|
184
|
+
* `.space_image.grid` is `#303030` at FULL alpha (`:586`) — unlike the node
|
|
185
|
+
* editor's, whose alpha is zero, which is why that view draws no grid at all.
|
|
186
|
+
* But the image region is cleared by `DRW_draw_view`
|
|
187
|
+
* (`space_image.cc:758`), the overlay ENGINE's own clear, and
|
|
188
|
+
* `draw/engines/overlay`'s grid pass is not in this checkout's sparse set. So
|
|
189
|
+
* the source states the grid's colour and not the value it sits on, and
|
|
190
|
+
* `#303030` on this view's own `#303030` surface is INVISIBLE — measured, not
|
|
191
|
+
* assumed. The view draws the grid at the theme's colour anyway, because
|
|
192
|
+
* drawing a different one would be inventing a mark; the contrast is the
|
|
193
|
+
* first thing the owed sighted frame-beside-frame read settles.
|
|
194
|
+
*/
|
|
195
|
+
export const UV_GRID_OPEN =
|
|
196
|
+
"Blender's `.space_image.grid` is #303030 at full alpha " +
|
|
197
|
+
"(userdef_default_theme.c:586) and this view's surface is #303030, so the tile " +
|
|
198
|
+
"grid draws at the theme's colour and is invisible against it. The image " +
|
|
199
|
+
"region's own clear is the overlay engine's (DRW_draw_view, space_image.cc:758), " +
|
|
200
|
+
'which is not in the source checkout — the sighted read settles the contrast.';
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* THE LINE STYLE THIS VIEW DRAWS, and why it is not the default one.
|
|
204
|
+
*
|
|
205
|
+
* `edit_uv_line_style_from_space_image` (`overlay_mesh.hh:1091-1111`) returns
|
|
206
|
+
* one of five: with `sima->mode == SI_MODE_UV` it maps `dt_uv` — whose enum is
|
|
207
|
+
* `OUTLINE, DASH, BLACK, WHITE` in that order (`rna_space.cc:4010-4016`) and
|
|
208
|
+
* whose `image_create` leaves it at 0, so Blender's stock UV editor draws
|
|
209
|
+
* **OUTLINE** — and OTHERWISE it returns `SHADOW`.
|
|
210
|
+
*
|
|
211
|
+
* There is no `SpaceImage` here and therefore no `mode`, so the honest branch
|
|
212
|
+
* is the else: **SHADOW**, `theme.colors.uv_shadow` `#707070`
|
|
213
|
+
* (`…edges_frag.glsl:48-50`). The OUTLINE constants are carried beside it
|
|
214
|
+
* because that is what the frame-beside-frame read will be against once a
|
|
215
|
+
* mode-carrying surface exists.
|
|
216
|
+
*/
|
|
217
|
+
export const UV_LINE_STYLES = {
|
|
218
|
+
outline: {
|
|
219
|
+
/** `mix(wire_edit, edge_select, selection_fac)` with `use_edge_select`
|
|
220
|
+
* off (`…edges_frag.glsl:34`), over a black outer line (`:36`). */
|
|
221
|
+
inner: UV_THEME.wireEdit,
|
|
222
|
+
outer: '#000000',
|
|
223
|
+
},
|
|
224
|
+
dash: { inner: '#595959', outer: null, period: UV_DASH_LENGTH },
|
|
225
|
+
black: { inner: '#000000', outer: null },
|
|
226
|
+
white: { inner: '#ffffff', outer: null },
|
|
227
|
+
shadow: { inner: UV_THEME.uvShadow, outer: null },
|
|
228
|
+
} as const;
|
|
229
|
+
|
|
230
|
+
/** `0.35` in the DASH style's `float4(float3(0.35f), 1.0f)`
|
|
231
|
+
* (`…edges_frag.glsl:39`) is a LINEAR value, and every other colour in that
|
|
232
|
+
* shader comes from the theme as sRGB — so the dash's grey is `0.35`
|
|
233
|
+
* linear = **#595959** sRGB, which is what `UV_LINE_STYLES.dash.inner`
|
|
234
|
+
* carries. (`0.35^(1/2.4)` with the sRGB knee: 0.3487 → 0.6236 → 159.) */
|
|
235
|
+
export const UV_DASH_LINEAR_GREY = 0.35;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* THE EDGE'S DRAWN WIDTH. `…edges_frag.glsl:53-54` inks out to
|
|
239
|
+
* `max(theme.sizes.edge - 0.5, 0)` and fades over the next
|
|
240
|
+
* `max(theme.sizes.edge, 1)` — with `.edge_width` 1 that is a 0.5-px core in a
|
|
241
|
+
* 1-px falloff either side of the line's centre, so the mark reads as a
|
|
242
|
+
* **1 px** line with a soft edge. The view draws 1 px.
|
|
243
|
+
*/
|
|
244
|
+
export const UV_EDGE_DRAWN_WIDTH = Math.max(UV_SIZES.edgeWidth, 1);
|
|
245
|
+
|
|
246
|
+
/** A PIN is not a theme colour: `…verts_vert.glsl:15` declares
|
|
247
|
+
* `constexpr float4 pinned_col = float4(1.0f, 0.0f, 0.0f, 1.0f)` with a
|
|
248
|
+
* `/* TODO: Theme? *\/` beside it, so a pinned UV vertex is a hard **red**,
|
|
249
|
+
* fill AND outline (`:20-21`). */
|
|
250
|
+
export const UV_PIN_COLOR = '#ff0000';
|
|
251
|
+
|
|
252
|
+
// -------------------------------------------------------------- the decoding
|
|
253
|
+
|
|
254
|
+
/** One base64 payload as bytes. The doors ship typed-array bytes because a UV
|
|
255
|
+
* layout is `loops` corners and JSON numbers would be four times the wire for
|
|
256
|
+
* the same values — the shape I4's weights already established. */
|
|
257
|
+
export function uvBytes(base64: string): Uint8Array {
|
|
258
|
+
const binary = atob(base64);
|
|
259
|
+
const bytes = new Uint8Array(binary.length);
|
|
260
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
261
|
+
return bytes;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function uvFloat32(base64: string): Float32Array {
|
|
265
|
+
const bytes = uvBytes(base64);
|
|
266
|
+
return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 2);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function uvUint32(base64: string): Uint32Array {
|
|
270
|
+
const bytes = uvBytes(base64);
|
|
271
|
+
return new Uint32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 2);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** An `#rrggbb` plus an alpha as an `rgba()` — SVG's `fill-opacity` would do
|
|
275
|
+
* for a fill, but an alpha that belongs to the COLOUR (Blender's `face` is
|
|
276
|
+
* `0xffffff0a`, one value) is clearer carried with it. */
|
|
277
|
+
export function uvRgba(hex: string, alpha: number): string {
|
|
278
|
+
const n = Number.parseInt(hex.slice(1), 16);
|
|
279
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${Math.round(alpha * 1000) / 1000})`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** THE VIEW'S OWN CHROME, as named constants rather than inline literals —
|
|
283
|
+
* `.space_image.text` `#e6e6e6` (`userdef_default_theme.c:582`) for the
|
|
284
|
+
* status line, and Blender's selection orange `#ed5700` (the palette's
|
|
285
|
+
* `viewport.selection`, itself derived from `.space_view3d.select`) for a
|
|
286
|
+
* refusal, which is the same hue I2's rail already wears for "this is the
|
|
287
|
+
* one you are looking at". They live here because a `@volter/editor-blender` view may
|
|
288
|
+
* not import the editor's theme (the package's host-import pin is ZERO) and
|
|
289
|
+
* because the style-token ratchet counts LITERALS in a `.tsx`, which is
|
|
290
|
+
* exactly the right pressure: a colour with a citation is a constant. */
|
|
291
|
+
export const UV_CHROME = {
|
|
292
|
+
text: '#e6e6e6',
|
|
293
|
+
refusal: '#ed5700',
|
|
294
|
+
/** `.space_image`'s own separator value: the palette's `region.outliner`
|
|
295
|
+
* neighbour, one level under the surface, which is what every other panel
|
|
296
|
+
* edge in this look already draws. */
|
|
297
|
+
rule: '#232323',
|
|
298
|
+
/** The status line's own box. Blender's editor footers are one text row in
|
|
299
|
+
* a `widget_unit`-tall strip; at `U.widget_unit` 20 (`wm_window.cc:779`)
|
|
300
|
+
* and 11-px text that is 3 px of padding either side of the row, and the
|
|
301
|
+
* 12-px gap is the same `NODE_MARGIN_X/3` rhythm the node view's labels
|
|
302
|
+
* already use. Named here for the reason the colours are. */
|
|
303
|
+
statusPadding: '3px 8px',
|
|
304
|
+
statusGap: 12,
|
|
305
|
+
} as const;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The VERSION at the far right of the status bar (`workspace.status`).
|
|
3
|
+
*
|
|
4
|
+
* Blender's bottom row ends with a status glyph and `5.2.0` — the app's own
|
|
5
|
+
* version, always on screen, so "which build am I looking at" is never a
|
|
6
|
+
* menu trip. This is that, for this editor.
|
|
7
|
+
*
|
|
8
|
+
* WHERE THE NUMBER COMES FROM. The open project's manifest declares the
|
|
9
|
+
* engine it is built against, and the host door exposes it as
|
|
10
|
+
* `editorHost().project.engineVersion()` — the exact field
|
|
11
|
+
* `ProjectHeader.tsx` renders as `v{engineVersion}`. This reuses that one
|
|
12
|
+
* source rather than importing a `package.json`: the version that matters to
|
|
13
|
+
* a person looking at a project is the one the project is pinned to, and a
|
|
14
|
+
* second reading could disagree with the header's.
|
|
15
|
+
*
|
|
16
|
+
* It is read through the SDK's host door and NOT through `@editor/*`, and
|
|
17
|
+
* its two inline styles spell the theme tokens directly
|
|
18
|
+
* (`var(--vgai-space-2)`, `var(--vgai-font-md)`) rather than importing the
|
|
19
|
+
* host's `fontSizeVar`/`spaceVar` handles: `@volter/editor-blender` is pinned at ZERO
|
|
20
|
+
* host imports (`validate-editor-closure.mjs`), the first package to reach
|
|
21
|
+
* that destination, and a token NAME is the published contract while the
|
|
22
|
+
* handle object is the host's own sugar. The widget kit's move to
|
|
23
|
+
* `@volter/editor-sdk/widgets` is deferred (WORK.md §Skews as packages, item 5);
|
|
24
|
+
* until it lands, a package styling inline writes the `var()`.
|
|
25
|
+
*
|
|
26
|
+
* Nothing before a project is open (there is no version to tell the truth
|
|
27
|
+
* about yet), per the registry contract's "an item that is currently
|
|
28
|
+
* meaningless returns null itself".
|
|
29
|
+
*
|
|
30
|
+
* The connection half of Blender's pair is the host's own job and already
|
|
31
|
+
* exists (`status-contributions.tsx`'s `editor-lease` item, which is quiet
|
|
32
|
+
* while the editor is served and speaks when it is not); this contributes
|
|
33
|
+
* only the version, and the host's counters are untouched.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { editorHost } from '@volter/editor-sdk/host';
|
|
37
|
+
import { useSyncExternalStore } from 'react';
|
|
38
|
+
|
|
39
|
+
export const point = 'workspace.status';
|
|
40
|
+
export const title = 'Version';
|
|
41
|
+
export const align = 'right';
|
|
42
|
+
/** The back of the contributed band: last item on the right, as in Blender.
|
|
43
|
+
* `tool-loader.ts`'s `MAX_DECLARED_ORDER` is 998. */
|
|
44
|
+
export const order = 998;
|
|
45
|
+
|
|
46
|
+
export default function BlenderVersionStatus() {
|
|
47
|
+
const { project } = editorHost();
|
|
48
|
+
const version = useSyncExternalStore(
|
|
49
|
+
project.subscribe,
|
|
50
|
+
project.engineVersion,
|
|
51
|
+
project.engineVersion,
|
|
52
|
+
);
|
|
53
|
+
if (version === null) return null;
|
|
54
|
+
return (
|
|
55
|
+
<span
|
|
56
|
+
className="vgai-status-copy"
|
|
57
|
+
data-testid="status-blender-version"
|
|
58
|
+
data-engine-version={version}
|
|
59
|
+
// NO COLOUR AND NO `sm`. Measured on the native-2x frames, Blender's
|
|
60
|
+
// `5.2.0` is `#878787` (135, a flat plateau — 53 px at 134 and 33 at 135
|
|
61
|
+
// across the digits) with a 9.0 CSS cap height (18 device). 135 is the
|
|
62
|
+
// DIMMEST text in Blender's window and the same ink its mouse hints take
|
|
63
|
+
// at the other end of the band, so the band is one voice and this
|
|
64
|
+
// inherits it rather than pinning `content.muted` (194) and becoming the
|
|
65
|
+
// loudest thing in the quietest strip. The cap puts the size on the
|
|
66
|
+
// `md`/`base` rung (11px -> 8.8), not `sm` (10px -> 8.0).
|
|
67
|
+
style={{
|
|
68
|
+
display: 'inline-flex',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
gap: 'var(--vgai-space-2)',
|
|
71
|
+
fontSize: 'var(--vgai-font-md)',
|
|
72
|
+
}}
|
|
73
|
+
title={`This project is built against engine ${version}`}
|
|
74
|
+
>
|
|
75
|
+
{/* Blender's thin rule between the status glyph and the number, measured
|
|
76
|
+
on `modeling.png` at device x 3369..3371, y 2069..2097: 1.5 CSS wide,
|
|
77
|
+
14.5 CSS tall, and inked 134 — the TEXT's own colour, not a boundary.
|
|
78
|
+
It was drawn here at `boundary.strong` (#3c3c3c, 60), which against a
|
|
79
|
+
#171717 band is all but invisible; `currentColor` is both the measured
|
|
80
|
+
colour and the one that cannot drift from the number beside it. The
|
|
81
|
+
height rung closest to 14.5 is `icon-md` (14). */}
|
|
82
|
+
<span
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
style={{
|
|
85
|
+
width: 'var(--vgai-stroke-resting)',
|
|
86
|
+
height: 'var(--vgai-icon-md)',
|
|
87
|
+
background: 'currentColor',
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
{version}
|
|
91
|
+
</span>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE BLENDER VERBS of the session wire (`@volter/editor-sdk/commands`, a
|
|
3
|
+
* `workspace.command` contribution): `blender-start`, `blender-stop`,
|
|
4
|
+
* `blender-execute`, `blender-scene-info`, `blender-object-info`,
|
|
5
|
+
* `blender-screenshot-view`, `blender-read-file`, `blender-write-file`,
|
|
6
|
+
* `blender-list-files`, `blender-status`, the RNA door's `blender-rna`,
|
|
7
|
+
* `blender-rna-context` and `blender-rna-set`, and the tree door's
|
|
8
|
+
* `blender-outliner` and `blender-outliner-set`.
|
|
9
|
+
*
|
|
10
|
+
* These left `command-listener.ts`'s switch (and their rows left
|
|
11
|
+
* `command-table.ts`) when Blender's editor half left the host — WORK.md
|
|
12
|
+
* §The workbench, item D. `vgai blender-mcp` is transport only: every
|
|
13
|
+
* `execute_blender_code`, `get_scene_info`, `get_object_info` and
|
|
14
|
+
* `get_viewport_screenshot` still arrives as the same `blender-*` command
|
|
15
|
+
* through the same relay, and is answered here instead of there.
|
|
16
|
+
*
|
|
17
|
+
* EACH ROW CARRIES ITS OWN RELAY BUDGET, transcribed from the host table it
|
|
18
|
+
* left — the page reports the rows it registered and the server sizes its
|
|
19
|
+
* wait from them, so a contributed verb never falls to the generic 5s
|
|
20
|
+
* default (the failure `command-table.ts`'s header records). Blender
|
|
21
|
+
* runs in this tab's worker; a chunk can model for a while (the engine
|
|
22
|
+
* boots on the first call), and every one presents a frame.
|
|
23
|
+
*
|
|
24
|
+
* THIS LANE HOLDS NO HOST INTERNAL (2026-09-19, WORK.md §Skews as packages
|
|
25
|
+
* under the Code-OSS frame, item 6). It took the shell store from
|
|
26
|
+
* `@editor/shell-store-door` to hand to the runtime host, which needed it for
|
|
27
|
+
* one call — presenting the Model document — and that call is now
|
|
28
|
+
* `host.workspace.open`, an address the door routes. A handler's whole context
|
|
29
|
+
* is the command it is given plus `@volter/editor-sdk/host`.
|
|
30
|
+
*/
|
|
31
|
+
import type {
|
|
32
|
+
CommandContribution,
|
|
33
|
+
CommandDerivedRefresh,
|
|
34
|
+
CommandSpec,
|
|
35
|
+
} from '@volter/editor-sdk/commands';
|
|
36
|
+
import { handleBlenderCommand } from '../host/blender-runtime-host';
|
|
37
|
+
|
|
38
|
+
export const point = 'workspace.command';
|
|
39
|
+
|
|
40
|
+
/** One Blender verb: its budget, and the lane's one handler. A row with no
|
|
41
|
+
* `timeoutMs` takes the host's own generic budget — the value the rows it
|
|
42
|
+
* left spelled as `noDerivedRefresh()`'s default, and not restated here so
|
|
43
|
+
* the two cannot drift. `derivedRefresh` IS always stated: the registry's
|
|
44
|
+
* default for a contributed row is `'always'`, and only the two verbs that
|
|
45
|
+
* change what the Model document shows owe a derivation.
|
|
46
|
+
*/
|
|
47
|
+
const verb = (derivedRefresh: CommandDerivedRefresh, timeoutMs?: number): CommandSpec => ({
|
|
48
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
49
|
+
derivedRefresh,
|
|
50
|
+
handle: (cmd) => handleBlenderCommand(cmd as { type: string; [key: string]: unknown }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const commands: CommandContribution['commands'] = {
|
|
54
|
+
'blender-start': verb('none', 120_000),
|
|
55
|
+
'blender-stop': verb('none'),
|
|
56
|
+
'blender-execute': verb('always', 30 * 60_000),
|
|
57
|
+
'blender-scene-info': verb('none', 60_000),
|
|
58
|
+
'blender-object-info': verb('none', 60_000),
|
|
59
|
+
'blender-screenshot-view': verb('always', 60_000),
|
|
60
|
+
'blender-read-file': verb('none', 60_000),
|
|
61
|
+
'blender-write-file': verb('none', 60_000),
|
|
62
|
+
'blender-list-files': verb('none'),
|
|
63
|
+
// THE RNA DOOR (WORK.md §Blender in the tab is Blender, "Inspection
|
|
64
|
+
// parity", I1). Two READS — a datablock's `bl_rna.properties` and the
|
|
65
|
+
// Properties context — and one WRITE. The reads present nothing, so they
|
|
66
|
+
// owe no derivation; `blender-rna-set` mutates the model and the session
|
|
67
|
+
// presents the frame, so it derives like `blender-execute`.
|
|
68
|
+
'blender-rna': verb('none', 60_000),
|
|
69
|
+
'blender-rna-context': verb('none', 60_000),
|
|
70
|
+
'blender-rna-set': verb('always', 60_000),
|
|
71
|
+
// THE TREE DOOR (I3): Blender's View Layer tree, and one restriction column
|
|
72
|
+
// written. Same split — the read presents nothing; the column write mutates
|
|
73
|
+
// the model and presents, so it derives.
|
|
74
|
+
'blender-outliner': verb('none', 60_000),
|
|
75
|
+
// THE NODE-TREE DOOR (I5). A READ, so it presents nothing and owes no
|
|
76
|
+
// derivation; there is deliberately no writer beside it.
|
|
77
|
+
'blender-node-tree': verb('none', 60_000),
|
|
78
|
+
// THE UV DOOR (I5, UV Editing). A READ of one mesh's UV layout; pinning,
|
|
79
|
+
// unwrapping and selecting are edits and there is no writer beside it.
|
|
80
|
+
'blender-uv-layout': verb('none', 60_000),
|
|
81
|
+
// THE RIG AND CLIP DOORS (the Timeline). Both READS — the skin binding a
|
|
82
|
+
// `THREE.SkinnedMesh` is built from and the action as three.js tracks — so
|
|
83
|
+
// neither presents and neither owes a derivation. There is deliberately no
|
|
84
|
+
// writer beside them: keying, moving a key and setting a range are edits,
|
|
85
|
+
// and the ONE write the Timeline makes (`scene.frame_current`, once on
|
|
86
|
+
// pause and at scrub-end) goes through `blender-rna-set`, which already
|
|
87
|
+
// exists and already presents.
|
|
88
|
+
'blender-rig': verb('none', 60_000),
|
|
89
|
+
'blender-action-clip': verb('none', 120_000),
|
|
90
|
+
// THE NODE VIEW'S OWN ACTIONS as a session verb — the keyboard ruling's
|
|
91
|
+
// `vgai.*`-command-per-action pattern, and the only way the drawer's view
|
|
92
|
+
// can be read or driven at all: `editor.document.*` is scoped to the active
|
|
93
|
+
// CENTER document by its own contract, and a utility is not one. Read-only
|
|
94
|
+
// over the MODEL: `look` and the view transform are inspection state, and a
|
|
95
|
+
// gesture that would edit is answered with the same refusal the pointer
|
|
96
|
+
// handlers give.
|
|
97
|
+
'blender-node-view': verb('none'),
|
|
98
|
+
'blender-outliner-set': verb('always', 60_000),
|
|
99
|
+
// A read of whether this tab HAS a session; it never starts one — and the
|
|
100
|
+
// one verb that answers before a project session exists.
|
|
101
|
+
'blender-status': verb('none'),
|
|
102
|
+
};
|