@volter/blender-engine 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.
Files changed (48) hide show
  1. package/LICENSE +724 -0
  2. package/README.md +48 -0
  3. package/browser/blender-emscripten-engine.mts +289 -0
  4. package/browser/blender-engine.mts +412 -0
  5. package/browser/blender-wali-engine.mts +362 -0
  6. package/browser/index.ts +7 -0
  7. package/browser/protocol.ts +202 -0
  8. package/browser/rna.ts +697 -0
  9. package/browser/runtime.ts +511 -0
  10. package/browser/session-frame.mts +169 -0
  11. package/browser/session.py +4166 -0
  12. package/browser/three/agx-base-srgb.lut +0 -0
  13. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  14. package/browser/three/agx-look-punchy.lut +0 -0
  15. package/browser/three/attach-presenter.ts +140 -0
  16. package/browser/three/blender-agx.ts +235 -0
  17. package/browser/three/blender-base64.ts +42 -0
  18. package/browser/three/blender-corner-normals.ts +432 -0
  19. package/browser/three/blender-display-lut.ts +145 -0
  20. package/browser/three/blender-filmic.ts +49 -0
  21. package/browser/three/blender-frame-columns.ts +100 -0
  22. package/browser/three/blender-gradient-texture.ts +57 -0
  23. package/browser/three/blender-runtime-armature.ts +528 -0
  24. package/browser/three/blender-runtime-frame.ts +39 -0
  25. package/browser/three/blender-runtime-geometry.ts +342 -0
  26. package/browser/three/blender-runtime-lighting.ts +829 -0
  27. package/browser/three/blender-runtime-shadows.ts +107 -0
  28. package/browser/three/blender-runtime-view.ts +1481 -0
  29. package/browser/three/blender-runtime-volume.ts +128 -0
  30. package/browser/three/blender-runtime-weights.ts +306 -0
  31. package/browser/three/blender-sky.ts +461 -0
  32. package/browser/three/blender-standard.ts +68 -0
  33. package/browser/three/blender-triangulate.ts +181 -0
  34. package/browser/three/filmic-srgb.lut +0 -0
  35. package/browser/three/presenter.ts +265 -0
  36. package/browser/three/release.ts +27 -0
  37. package/browser/three/sky-precompute-worker.ts +45 -0
  38. package/browser/three/sky-worker.ts +79 -0
  39. package/browser/three/world-field-sampler.ts +358 -0
  40. package/browser/three/world-math.ts +59 -0
  41. package/browser/vgai_three.py +554 -0
  42. package/browser/worker.ts +648 -0
  43. package/package.json +48 -0
  44. package/wasm/BUNDLE.json +65 -0
  45. package/wasm/DEPENDENCY-LICENSES.txt +4879 -0
  46. package/wasm/blender_browser.data.br +0 -0
  47. package/wasm/blender_browser.js +2 -0
  48. package/wasm/blender_browser.wasm.br +0 -0
package/browser/rna.ts ADDED
@@ -0,0 +1,697 @@
1
+ /**
2
+ * THE RNA DOOR'S CONTRACT — what `session.py`'s `rna_view`, `rna_context` and
3
+ * `rna_set` answer, as types. TYPES ONLY, no imports and no values: the
4
+ * Properties sections (`../contributions/blender-properties-*`) read these
5
+ * shapes, and a type-only module costs their closure nothing.
6
+ *
7
+ * Everything here is Blender's own introspection of Blender's own data
8
+ * (`bl_rna.properties`) — the ruling's "everything Blender SHOWS about a
9
+ * datablock, we show, in OUR panels, read from the engine through bpy and RNA"
10
+ * (ARCHITECTURE-CORE §Blender north star, "Inspection parity, not editing
11
+ * parity"). Nothing in this file is a vgai invention over RNA: every field is
12
+ * a `PropertyRNA` member, a `bpy.types` name, or an address `path_from_id()`
13
+ * produced.
14
+ */
15
+
16
+ /** `PropertyRNA.type` — Blender's own property-type enum. */
17
+ export type BlenderRnaType =
18
+ | 'BOOLEAN'
19
+ | 'INT'
20
+ | 'FLOAT'
21
+ | 'STRING'
22
+ | 'ENUM'
23
+ | 'POINTER'
24
+ | 'COLLECTION';
25
+
26
+ /** A POINTER property's target: where it lives and what it is. */
27
+ export interface BlenderRnaPointer {
28
+ /** The target's own address, or null for a struct RNA cannot address. */
29
+ readonly path: string | null;
30
+ readonly name: string | null;
31
+ readonly type: string;
32
+ }
33
+
34
+ /** A COLLECTION property's value: its length, and at most the door's own
35
+ * `names` limit of names — absent entirely when the collection's item type
36
+ * has no `name` (a mesh's `vertices`), which is what keeps a 100k-element
37
+ * collection a COUNT. */
38
+ export interface BlenderRnaCollectionValue {
39
+ readonly count: number;
40
+ readonly names?: readonly string[];
41
+ }
42
+
43
+ /** One item of an `EnumProperty`. `icon` is Blender's own icon NAME
44
+ * (`MESH_DATA`), which our icon set does not draw — it rides along because
45
+ * the door reports what RNA holds, not what we happen to paint. */
46
+ export interface BlenderRnaEnumItem {
47
+ readonly identifier: string;
48
+ readonly name: string;
49
+ readonly description: string;
50
+ readonly icon: string;
51
+ }
52
+
53
+ export type BlenderRnaValue =
54
+ | boolean
55
+ | number
56
+ | string
57
+ | readonly (boolean | number)[]
58
+ | readonly string[]
59
+ | BlenderRnaPointer
60
+ | BlenderRnaCollectionValue
61
+ | null;
62
+
63
+ /** ONE `bl_rna` PROPERTY. */
64
+ export interface BlenderRnaRow {
65
+ readonly identifier: string;
66
+ /** `PropertyRNA.name` — the UI name Blender itself would label it with. */
67
+ readonly name: string;
68
+ readonly type: BlenderRnaType;
69
+ /** `PropertyRNA.subtype` — `COLOR`, `DISTANCE`, `ANGLE`, `FILE_PATH`,
70
+ * `FACTOR`, `NONE`… This is what turns a 3-float array into a colour
71
+ * swatch instead of three numbers. */
72
+ readonly subtype: string;
73
+ readonly description: string;
74
+ /** The RNA STRUCT that declares this property (`Object`, `ID`, `Mesh`) —
75
+ * the only grouping RNA exposes, and the one the generic view groups by.
76
+ * Blender's panel layout is not this and is not read (the ruling). */
77
+ readonly group: string;
78
+ readonly groupName: string;
79
+ readonly arrayLength: number;
80
+ /** Blender says so: declared read-only, or this datablock refuses the write
81
+ * (`is_property_readonly`). Rendered dimmed, never as a disabled control. */
82
+ readonly readonly: boolean;
83
+ /** `PropertyRNA.is_hidden` — RNA's own `PROP_HIDDEN` flag
84
+ * (`rna_rna.cc:795-799`). Blender declares the property and draws it
85
+ * nowhere: `ID.original` on every datablock (`rna_ID.cc:2440-2448`),
86
+ * `ViewLayer.depsgraph` (`rna_layer.cc:733`), the NLA tweak storage
87
+ * (`rna_animation.cc:1701,1713`). The door reports it; the generic view
88
+ * OMITS it, and no curated list names one. */
89
+ readonly hidden?: boolean;
90
+ /** UNDRAWN STATE — RNA never gave this property UI text, so
91
+ * `rna_define.cc:1311-1312`'s defaults still stand: `name` is the
92
+ * identifier and `description` is empty. The modifier panel-open booleans
93
+ * are the worked case (`rna_def_modifier_panel_open_prop`,
94
+ * `rna_modifier.cc:2694-2705`, which sets no `PROP_HIDDEN` — so this is a
95
+ * second rule rather than a restatement of {@link hidden}): a panel arrow's
96
+ * state, not data about the datablock. The door reports it; the generic
97
+ * view OMITS it, and no curated list names one. */
98
+ readonly undrawn?: boolean;
99
+ readonly softMin?: number;
100
+ readonly softMax?: number;
101
+ readonly hardMin?: number;
102
+ readonly hardMax?: number;
103
+ readonly step?: number;
104
+ readonly precision?: number;
105
+ readonly lengthMax?: number;
106
+ readonly isFlag?: boolean;
107
+ readonly items?: readonly BlenderRnaEnumItem[];
108
+ /** `fixed_type` — what a POINTER points at / a COLLECTION holds. */
109
+ readonly itemType?: string;
110
+ readonly value?: BlenderRnaValue;
111
+ /** An array too long to serialise, described instead (`"64 values"`). */
112
+ readonly valueOmitted?: string;
113
+ /** The read itself threw — reported rather than swallowed, so a property
114
+ * that cannot be read is visibly a property that cannot be read. */
115
+ readonly valueError?: string;
116
+ }
117
+
118
+ export interface BlenderRnaGroup {
119
+ readonly id: string;
120
+ readonly label: string;
121
+ readonly rows: readonly BlenderRnaRow[];
122
+ }
123
+
124
+ export interface BlenderRnaStructView {
125
+ readonly path: string;
126
+ readonly kind: 'struct';
127
+ /** `bl_rna.identifier` — `Object`, `Mesh`, `Bone`, `SubsurfModifier`. */
128
+ readonly type: string;
129
+ readonly typeName: string;
130
+ readonly name: string | null;
131
+ readonly count: number;
132
+ readonly groups: readonly BlenderRnaGroup[];
133
+ }
134
+
135
+ export interface BlenderRnaCollectionItem {
136
+ readonly name: string | null;
137
+ readonly path: string | null;
138
+ readonly type: string;
139
+ }
140
+
141
+ export interface BlenderRnaCollectionView {
142
+ readonly path: string;
143
+ readonly kind: 'collection';
144
+ readonly count: number;
145
+ readonly items: readonly BlenderRnaCollectionItem[];
146
+ }
147
+
148
+ export type BlenderRnaView = BlenderRnaStructView | BlenderRnaCollectionView;
149
+
150
+ /** An addressed datablock the context names (the active bone, the active
151
+ * modifier…). */
152
+ export interface BlenderRnaNamed {
153
+ readonly name: string | null;
154
+ readonly path: string | null;
155
+ readonly type: string | null;
156
+ }
157
+
158
+ export interface BlenderRnaMaterialSlot extends BlenderRnaNamed {
159
+ readonly index: number;
160
+ readonly slots: number;
161
+ }
162
+
163
+ export interface BlenderRnaVertexGroup extends BlenderRnaNamed {
164
+ readonly index: number;
165
+ }
166
+
167
+ /**
168
+ * ONE TEXTURE USER, as `buttons_texture.cc` enumerates them
169
+ * (`buttons_texture_users_from_context`, `:244-366`): the struct that HOLDS a
170
+ * texture pointer, the property it holds it in, and the texture itself.
171
+ * `label` is the C's own group name ("Compositor", "Line Style", "Modifiers",
172
+ * "Particles", "Fields", "Brush"), and the order is the C's, so user 0 is the
173
+ * one `buttons_texture_context_compute` makes active by default.
174
+ */
175
+ export interface BlenderRnaTextureUser {
176
+ readonly label: string;
177
+ readonly name: string | null;
178
+ readonly path: string | null;
179
+ readonly property: string;
180
+ readonly texture: BlenderRnaNamed | null;
181
+ }
182
+
183
+ /**
184
+ * ONE PROPERTIES TAB, as Blender would show it for this context.
185
+ *
186
+ * `id` is `buttons_context.cc`'s own `BCONTEXT_*` in lower case; the ORDER of
187
+ * the list is `ED_buttons_tabs_list` (`space_buttons.cc:201-256`); `paths` are
188
+ * the datablocks that tab shows, in the order it shows them. A tab absent from
189
+ * the list is a tab Blender would not draw for this object.
190
+ */
191
+ export interface BlenderRnaTab {
192
+ readonly id: string;
193
+ readonly label: string;
194
+ /** OUR icon-set glyph name for this tab (`blender.icons.json`), not
195
+ * Blender's `ICON_*`: the glyph is our own drawing in Blender's idiom. */
196
+ readonly icon: string;
197
+ readonly paths: readonly { readonly label: string; readonly path: string }[];
198
+ }
199
+
200
+ export interface BlenderRnaContext {
201
+ readonly scene: string;
202
+ readonly viewLayer: string;
203
+ readonly world: string | null;
204
+ readonly collection: string | null;
205
+ /** `Object.mode` — `OBJECT`, `EDIT`, `POSE`, `WEIGHT_PAINT`… */
206
+ readonly mode: string | null;
207
+ /** `scene.render.engine`, which is `context.engine` — what every
208
+ * `COMPAT_ENGINES` poll in `bl_ui` tests. `BlenderCuratedPanel.engine`
209
+ * reads it rather than copying the condition. */
210
+ readonly engine: string;
211
+ readonly active: {
212
+ readonly name: string;
213
+ readonly type: string;
214
+ readonly path: string;
215
+ readonly dataPath: string | null;
216
+ readonly dataType: string | null;
217
+ } | null;
218
+ readonly activeBone: BlenderRnaNamed | null;
219
+ readonly activePoseBone: BlenderRnaNamed | null;
220
+ readonly activeMaterial: BlenderRnaMaterialSlot | null;
221
+ readonly activeModifier: BlenderRnaNamed | null;
222
+ readonly activeVertexGroup: BlenderRnaVertexGroup | null;
223
+ readonly activeShapeKey: BlenderRnaNamed | null;
224
+ readonly activeConstraint: BlenderRnaNamed | null;
225
+ readonly activeParticleSystem: BlenderRnaNamed | null;
226
+ readonly textureUsers: readonly BlenderRnaTextureUser[];
227
+ readonly activeTextureUser: BlenderRnaTextureUser | null;
228
+ readonly tabs: readonly BlenderRnaTab[];
229
+ }
230
+
231
+ /** What `rna_set` answers: the property it wrote, re-read from the engine. */
232
+ export interface BlenderRnaWrite {
233
+ readonly path: string;
234
+ readonly property: string;
235
+ readonly value: BlenderRnaValue;
236
+ }
237
+
238
+ // ---- the outliner tree -----------------------------------------------------
239
+ //
240
+ // WHAT `session.py`'s `rna_outliner` ANSWERS: Blender's VIEW LAYER tree for the
241
+ // session's scene, as rows (WORK.md §Blender in the tab is Blender,
242
+ // "Inspection parity", I3). Every field below is Blender's own — a `TSE_*`
243
+ // element type, an `ICON_*` name `tree_element_get_icon` picks, a restriction
244
+ // column `outliner_draw_restrictbuts` draws — and nothing here is a vgai
245
+ // invention over the Outliner.
246
+
247
+ /**
248
+ * ONE ROW'S RESTRICTION COLUMNS, as `outliner_draw_restrictbuts`
249
+ * (`outliner_draw.cc:1173-1854`) reads them. Each member is TRUE when the thing
250
+ * is RESTRICTED, which is the sense Blender's own property carries
251
+ * (`hide_viewport`, `hide_render`, `exclude`); a modifier's `show_render` and a
252
+ * constraint's `enabled` are inverted at the door so every column reads the
253
+ * same way round. A member ABSENT is a column Blender draws on no row of that
254
+ * type.
255
+ */
256
+ export interface BlenderOutlinerRestrict {
257
+ /** The EYE — an object's view layer base (`Object.hide_get()`), a layer
258
+ * collection's `hide_viewport`, a constraint's `enabled`. */
259
+ readonly hide?: boolean;
260
+ /** The RENDER CAMERA — `Object.hide_render`, `Collection.hide_render`, a
261
+ * modifier's `show_render`. */
262
+ readonly render?: boolean;
263
+ /** The MONITOR — `Object.hide_viewport`, a modifier's `show_viewport`, a
264
+ * pose bone's `hide`. Blender's Disable in Viewports column, which is OFF by
265
+ * default (`space_outliner.cc:399`) and reported all the same. */
266
+ readonly viewport?: boolean;
267
+ /** The CHECKBOX, layer collections only — `LayerCollection.exclude`. */
268
+ readonly exclude?: boolean;
269
+ }
270
+
271
+ export interface BlenderOutlinerRow {
272
+ /** The row's IDENTITY — its `path`, except where Blender draws one datablock
273
+ * twice in one tree (an object linked into two collections, the
274
+ * `TE_CHILD_NOT_IN_COLLECTION` duplicate), where an `@2` ordinal keeps the
275
+ * rows distinct. */
276
+ readonly id: string;
277
+ /** The engine's own address, which opens in the RNA door unchanged. */
278
+ readonly path: string;
279
+ readonly name: string;
280
+ /** Blender's own element type — `TSE_SOME_ID`, `TSE_LAYER_COLLECTION`,
281
+ * `TSE_MODIFIER`, `TSE_BONE`, `TSE_DEFGROUP_BASE`… */
282
+ readonly type: string;
283
+ /** The `ICON_*` name `tree_element_get_icon` picks for this row, WITHOUT the
284
+ * `ICON_` prefix — the spelling {@link BlenderRnaEnumItem.icon} already
285
+ * uses. */
286
+ readonly icon: string;
287
+ /** Blender's default open state: the Scene Collection and every editable
288
+ * layer collection, and nothing else (`outliner_tree.cc:139` against
289
+ * `tree_display_view_layer.cc:130,167`). */
290
+ readonly expanded: boolean;
291
+ readonly children: readonly BlenderOutlinerRow[];
292
+ /** How many children the door's page left behind (`_OUTLINER_PAGE`). Absent
293
+ * when the whole list is here. */
294
+ readonly more?: number;
295
+ /** The RNA struct this row's datablock is (`Object`, `Mesh`, `Material`). */
296
+ readonly struct?: string;
297
+ /** `Object.type` on an object row, which is what picks its glyph. */
298
+ readonly objectType?: string;
299
+ /** THE OBJECT THIS ROW BELONGS TO. Blender's own activation walks back to the
300
+ * owning object (`outliner_search_back_te(te, ID_OB)`,
301
+ * `outliner_select.cc:863`), so a datablock row selects its object and the
302
+ * Properties rail follows — which is what this carries. */
303
+ readonly object?: string;
304
+ /** A layer collection row's COLLECTION address. The row itself is addressed
305
+ * by its POSITION in the view layer, because a `LayerCollection` has no
306
+ * `path_from_id()` (I2's finding); this is the collection datablock it
307
+ * shows. */
308
+ readonly data?: string;
309
+ readonly selected?: boolean;
310
+ readonly active?: boolean;
311
+ /** `TE_CHILD_NOT_IN_COLLECTION` — a child drawn under its parent although it
312
+ * is not in that collection. Blender leaves it unexpanded and draws it no
313
+ * restriction columns at all (`outliner_draw.cc:1282-1286`). */
314
+ readonly notInCollection?: boolean;
315
+ readonly restrict?: BlenderOutlinerRestrict;
316
+ }
317
+
318
+ export interface BlenderOutlinerTree {
319
+ readonly scene: string;
320
+ readonly viewLayer: string;
321
+ /** `bpy.context.mode` — the editor-wide mode a `poll` tests (`OBJECT`,
322
+ * `EDIT_MESH`, `POSE`, `PAINT_WEIGHT`…). */
323
+ readonly mode: string;
324
+ /** `Object.mode` of the engine's active object, which is what the Outliner's
325
+ * own pose rows key on. */
326
+ readonly objectMode: string | null;
327
+ readonly active: string | null;
328
+ /** The roots — one Scene Collection row, under the default filters. */
329
+ readonly rows: readonly BlenderOutlinerRow[];
330
+ }
331
+
332
+ /** What `outliner_set` answers: the column it wrote, re-read from the engine. */
333
+ export interface BlenderOutlinerWrite {
334
+ readonly path: string;
335
+ readonly column: string;
336
+ readonly value: boolean;
337
+ }
338
+
339
+ /* ---- the node editor's tree ---------------------------------------------- */
340
+
341
+ /** `NodeSocket.type` — the `SOCK_*` that picks the socket's COLOUR
342
+ * (`std_node_socket_colors[]`, `drawnode.cc:987-1013`). Left open, because
343
+ * Blender adds socket types and the door reports what the engine answers. */
344
+ export type BlenderNodeSocketType = string;
345
+
346
+ /** `NodeSocket.display_shape` — `rna_node_socket.cc:793-802`. */
347
+ export type BlenderNodeSocketShape =
348
+ | 'CIRCLE'
349
+ | 'SQUARE'
350
+ | 'DIAMOND'
351
+ | 'CIRCLE_DOT'
352
+ | 'SQUARE_DOT'
353
+ | 'DIAMOND_DOT'
354
+ | 'LINE'
355
+ | 'VOLUME_GRID'
356
+ | 'LIST';
357
+
358
+ export interface BlenderNodeSocket {
359
+ readonly identifier: string;
360
+ readonly name: string;
361
+ readonly label: string | null;
362
+ readonly type: BlenderNodeSocketType;
363
+ readonly shape: BlenderNodeSocketShape;
364
+ /** `enabled` and `hide` together are the node editor's own availability
365
+ * test: an unavailable socket occupies no row and draws no mark. */
366
+ readonly enabled: boolean;
367
+ readonly hide: boolean;
368
+ /** `hide_value` — Blender draws the name alone, never the inline widget. */
369
+ readonly hideValue: boolean;
370
+ readonly linked: boolean;
371
+ readonly multiInput: boolean;
372
+ readonly index: number;
373
+ /** `default_value`, flattened: a number, a string, a name (for a pointer
374
+ * socket), up to four floats (a colour or a vector), or null where the
375
+ * socket carries none — a shader or a geometry socket. */
376
+ readonly value: number | string | boolean | readonly number[] | null;
377
+ }
378
+
379
+ /** `Node.color_tag` (`rna_nodetree.cc:79-100`, `:9480-9484`), which RNA reads
380
+ * from `bke::node_color_tag(*node)` — the value `node_get_colorid`
381
+ * (`node_draw.cc:1388-1434`) switches on to pick the header colour. */
382
+ export type BlenderNodeColorTag =
383
+ | 'NONE'
384
+ | 'ATTRIBUTE'
385
+ | 'COLOR'
386
+ | 'CONVERTER'
387
+ | 'DISTORT'
388
+ | 'FILTER'
389
+ | 'GEOMETRY'
390
+ | 'INPUT'
391
+ | 'MATTE'
392
+ | 'OUTPUT'
393
+ | 'SCRIPT'
394
+ | 'SHADER'
395
+ | 'TEXTURE'
396
+ | 'VECTOR'
397
+ | 'PATTERN'
398
+ | 'INTERFACE'
399
+ | 'GROUP';
400
+
401
+ export interface BlenderNode {
402
+ readonly name: string;
403
+ readonly idname: string;
404
+ readonly type: string;
405
+ /** `bl_label` — what the header prints when the node carries no `label`. */
406
+ readonly typeLabel: string;
407
+ readonly label: string | null;
408
+ readonly colorTag: BlenderNodeColorTag;
409
+ /** Tree-space `[x, y]`: the node's top-left corner, which is where
410
+ * `node_update_basis` starts laying out (`node_draw.cc:1295-1300`). */
411
+ readonly location: readonly number[];
412
+ readonly width: number;
413
+ /** `Node.hide` — Blender's COLLAPSED node, drawn as a short rounded bar
414
+ * (`node_update_collapsed`, `node_draw.cc:1326-1390`). */
415
+ readonly collapsed: boolean;
416
+ readonly muted: boolean;
417
+ readonly selected: boolean;
418
+ readonly useCustomColor: boolean;
419
+ readonly color: readonly number[];
420
+ /** The frame node this one sits inside, by name, or null. */
421
+ readonly parent: string | null;
422
+ /** `NODE_DO_OUTPUT` through its one RNA spelling. */
423
+ readonly activeOutput: boolean;
424
+ /**
425
+ * HOW MANY SOCKET PANELS THE NODE DECLARES (`Node.panel_states`). A panel
426
+ * state carries a `persistent_uid` and `is_collapsed` and nothing else
427
+ * (`rna_nodetree.cc:9289-9299`, `:9430-9434`); which SOCKETS belong to a
428
+ * panel, and what a panel is CALLED, live in the node's C++ declaration
429
+ * that `node_update_basis_from_declaration` (`node_draw.cc:1086-1218`)
430
+ * walks, and bpy exposes neither — so that half is TRACED from Blender's
431
+ * source (ruled 2026-09-19) and this door owes only the live half. The
432
+ * trace and its table belong to the CONSUMER, `@volter/editor-blender`, because they
433
+ * read a Blender checkout rather than the running engine; this package
434
+ * answers what the engine knows and nothing else.
435
+ */
436
+ readonly panelCount: number;
437
+ /**
438
+ * The states themselves, IN DECLARATION ORDER — Blender indexes
439
+ * `panel_states_array` by the declaration's panel index
440
+ * (`node_draw.cc:1037`) and RNA's collection is that array, so position is
441
+ * the join between this and the traced declaration. `identifier` is the
442
+ * declaration's `persistent_uid`, carried so a mis-join is visible.
443
+ */
444
+ readonly panels: readonly { readonly identifier: number; readonly collapsed: boolean }[];
445
+ /** `Node.show_options` → `NODE_OPTIONS`: with it clear, a declaration's
446
+ * LAYOUT rows draw nothing at all (`node_draw.cc:739-746`). */
447
+ readonly showOptions: boolean;
448
+ readonly inputs: readonly BlenderNodeSocket[];
449
+ readonly outputs: readonly BlenderNodeSocket[];
450
+ }
451
+
452
+ export interface BlenderNodeLink {
453
+ readonly fromNode: string;
454
+ readonly fromSocket: string;
455
+ readonly toNode: string;
456
+ readonly toSocket: string;
457
+ readonly muted: boolean;
458
+ readonly valid: boolean;
459
+ }
460
+
461
+ /** What `rna_node_tree` answers. `path` null with `useNodes` false is a
462
+ * material whose `use_nodes` is off; `path` null with `material` null is no
463
+ * material at all — both states Blender draws as a bare background, never an
464
+ * error (`node_draw_space`'s else branch, `node_draw.cc:4849-4853`). */
465
+ export interface BlenderNodeTree {
466
+ readonly path: string | null;
467
+ readonly type?: string;
468
+ readonly typeLabel?: string;
469
+ readonly material: string | null;
470
+ readonly useNodes: boolean | null;
471
+ readonly active?: string | null;
472
+ readonly nodes: readonly BlenderNode[];
473
+ readonly links: readonly BlenderNodeLink[];
474
+ }
475
+
476
+ /**
477
+ * What `rna_uv_layout` answers — ONE MESH'S UV LAYOUT, as Blender's UV editor
478
+ * would draw it.
479
+ *
480
+ * THE PER-CORNER ARRAYS CROSS AS BASE64 typed-array bytes, for the reason
481
+ * I4's weights do: a layout is `loops` two-float corners plus a
482
+ * triangulation, and the generic RNA door answers a struct at a time.
483
+ * `uvBase64` is `Float32Array` of `loops * 2` (u, v per corner, in corner
484
+ * order); `triangleBase64` is `Uint32Array` of `triangles * 3` CORNER indices
485
+ * into that array — Blender's own `calc_loop_triangles` fan, never one of
486
+ * ours; `loopStartBase64` / `loopTotalBase64` are `Uint32Array` of
487
+ * `polygons`, which is what makes a face's UV outline a contiguous walk;
488
+ * `pinBase64` is `Uint8Array` of `loops` and is null when nothing is pinned.
489
+ *
490
+ * `active` null with a non-empty `layers` cannot happen (a mesh with UV maps
491
+ * has an active one); `active` null with `layers` empty is a mesh that has no
492
+ * UV map at all, which is a STATE — `arena-vanguard.blend`'s 17,778-vertex
493
+ * body is exactly that, measured 2026-09-19 — and the door answers the header
494
+ * alone.
495
+ *
496
+ * `selection` is null at this pin and says so rather than being absent: UV
497
+ * selection lives in the BMesh an edit-mode session holds, and
498
+ * `MeshUVLoopLayer` (`rna_mesh.cc:2380-2456`) declares `uv`, `pin`, `name`,
499
+ * `active`, `active_render` and `active_clone` and no selection of any kind.
500
+ */
501
+ export interface BlenderUvLayout {
502
+ readonly object: string | null;
503
+ readonly mesh: string | null;
504
+ readonly path?: string;
505
+ readonly layers: readonly string[];
506
+ readonly active: string | null;
507
+ /** `bpy.context.mode` — reported, never required (orchestrator ruling 1,
508
+ * 2026-09-19: inspection is not mode-gated). */
509
+ readonly mode: string;
510
+ readonly loops: number;
511
+ readonly polygons: number;
512
+ readonly materials?: readonly (string | null)[];
513
+ readonly uvBase64?: string;
514
+ readonly triangleBase64?: string;
515
+ readonly loopStartBase64?: string;
516
+ readonly loopTotalBase64?: string;
517
+ readonly triangles?: number;
518
+ readonly pinBase64?: string | null;
519
+ readonly selection?: string | null;
520
+ readonly bounds?: readonly [number, number, number, number];
521
+ readonly image?: {
522
+ readonly name: string;
523
+ readonly width: number;
524
+ readonly height: number;
525
+ readonly source: string;
526
+ readonly hasData: boolean;
527
+ } | null;
528
+ }
529
+
530
+ /**
531
+ * ONE MESH'S SKIN BINDING (`session.py`'s `rna_rig`) — what a
532
+ * `THREE.SkinnedMesh` needs, and nothing Blender does not carry.
533
+ *
534
+ * WE VISUALIZE WITH THREE.JS, NOT BLENDER (owner rule, 2026-09-20). Blender
535
+ * holds the animation as DATA; three.js plays it. This door and
536
+ * {@link BlenderActionClip} are the whole of what crosses to make that true —
537
+ * once per rig, rather than a depsgraph evaluation and a re-export per frame.
538
+ *
539
+ * THE BIND POSE IS `pose`, NOT `rest`, and that is the load-bearing fact: the
540
+ * export door runs with `evaluate: True`, so the mesh columns the presenter
541
+ * holds are the mesh ALREADY DEFORMED at Blender's current frame. A skeleton
542
+ * bound in that same pose makes the skinning an identity there, so the picture
543
+ * at the bind frame is exactly the frame Blender presented — and Blender's
544
+ * `frame_current` never has to move again for playback.
545
+ *
546
+ * THE INDICES ARE BLENDER VERTEX INDICES. `DrawArrays.sourceVertex` maps a
547
+ * drawn vertex back to its Blender one, and the presenter expands these arrays
548
+ * through it (`blender-runtime-skin.ts`) exactly as I4's weight ramp does.
549
+ */
550
+ export interface BlenderRigBone {
551
+ readonly name: string;
552
+ readonly parent: string | null;
553
+ /** `Bone.matrix_local` — the REST pose in the armature object's space,
554
+ * row-major as four rows. Diagnostic here; the bind is `pose`. */
555
+ readonly rest: readonly (readonly [number, number, number, number])[];
556
+ /** `PoseBone.matrix` (`pchan->pose_mat`), the same matrix I4's bone overlay
557
+ * draws — the BIND pose, in the armature object's space. */
558
+ readonly pose: readonly (readonly [number, number, number, number])[];
559
+ readonly length: number;
560
+ readonly deform: boolean;
561
+ readonly connected: boolean;
562
+ }
563
+
564
+ export interface BlenderRigBinding {
565
+ readonly object: string | null;
566
+ readonly mesh?: string;
567
+ readonly armature: string | null;
568
+ readonly armatureData?: string;
569
+ readonly bones: readonly BlenderRigBone[];
570
+ readonly vertexCount: number;
571
+ /** `scene.frame_current` when the binding was read — the frame the exported
572
+ * columns, and therefore the bind pose, belong to. */
573
+ readonly frame?: number;
574
+ readonly mode?: string;
575
+ /** Bones whose pose is a SOLVER's rather than their channels'. The clip is
576
+ * derived from the F-Curves alone, so a constrained bone's playback cannot
577
+ * reproduce Blender's own answer; the view names them instead of drifting. */
578
+ readonly constrainedBones?: readonly string[];
579
+ /** Uint16, `vertexCount * 4`. */
580
+ readonly skinIndexBase64?: string;
581
+ /** Float32, `vertexCount * 4`, each vertex's four normalized to sum to one. */
582
+ readonly skinWeightBase64?: string;
583
+ readonly maxInfluences?: number;
584
+ readonly truncatedVertices?: number;
585
+ readonly unweightedVertices?: number;
586
+ readonly unmappedGroups?: readonly string[];
587
+ /** Why there is no binding, when there is none. Never a silent empty. */
588
+ readonly reason?: string;
589
+ }
590
+
591
+ /** EVERY SKIN BINDING THE SCENE NEEDS, which is the PRESENTER's question: a
592
+ * presented frame carries every object at once and the presenter has to know
593
+ * which of its meshes are `THREE.SkinnedMesh`es before it builds them. With
594
+ * `named` set, the list is that one mesh. */
595
+ export interface BlenderRig {
596
+ /** `scene.frame_current` when the bindings were read — the frame the
597
+ * exported columns, and therefore every bind pose here, belong to. */
598
+ readonly frame: number;
599
+ readonly named: string | null;
600
+ readonly rigs: readonly BlenderRigBinding[];
601
+ }
602
+
603
+ /**
604
+ * ONE TRACK of {@link BlenderActionClip}: a bone's LOCAL `position`,
605
+ * `quaternion` or `scale` sampled at every integer frame.
606
+ *
607
+ * `constant` says the door found every sample equal to the first and shipped
608
+ * two keys instead of N — which is most of a rig, most of the time.
609
+ */
610
+ export interface BlenderClipTrack {
611
+ readonly bone: string;
612
+ readonly property: 'position' | 'quaternion' | 'scale';
613
+ readonly stride: number;
614
+ readonly count: number;
615
+ readonly constant: boolean;
616
+ /** Float32 seconds, `count` of them, zero at `clipStart`. */
617
+ readonly timeBase64: string;
618
+ /** Float32, `count * stride`. */
619
+ readonly valueBase64: string;
620
+ }
621
+
622
+ /** One column of the Timeline's summary row: a frame at which SOMETHING is
623
+ * keyed, with the most significant `Keyframe.type` any contributing curve
624
+ * carries and whether any of them is selected — Blender's two colour axes for
625
+ * a diamond (`.common.anim`'s six type pairs, `userdef_default_theme.c:320-331`). */
626
+ export interface BlenderClipKeyColumn {
627
+ readonly frame: number;
628
+ readonly type: string;
629
+ readonly select: boolean;
630
+ }
631
+
632
+ /**
633
+ * ONE ANIMATED OBJECT'S CONTRIBUTION to the summary row, and whether it is
634
+ * SELECTED — the whole of what Blender's `show_keys_from_selected_only`
635
+ * decides between.
636
+ *
637
+ * Blender's Timeline takes that flag from the SCENE rather than from the dope
638
+ * sheet (`ac->scene->flag & SCE_KEYS_NO_SELONLY` → `ADS_FILTER_ONLYSEL`,
639
+ * `anim_filter.cc:254-270`) and then skips any object whose base is not
640
+ * selected (`anim_filter.cc:2307`). `selected` is that base flag through RNA,
641
+ * `Object.select_get()`.
642
+ *
643
+ * The door reports every animated object either way; which side of the filter
644
+ * a Timeline is on is VIEW state, because a headless Blender has no
645
+ * `SpaceDopeSheet` to hold it and no scene flag we would be entitled to write.
646
+ */
647
+ export interface BlenderClipSummaryObject {
648
+ readonly object: string;
649
+ readonly action: string;
650
+ readonly selected: boolean;
651
+ readonly keyframes: readonly BlenderClipKeyColumn[];
652
+ }
653
+
654
+ /**
655
+ * ONE ACTION AS A THREE.JS CLIP (`session.py`'s `rna_action_clip`).
656
+ *
657
+ * THE SAMPLES ARE A BAKE, and it is a stated difference rather than an implied
658
+ * one: Blender's channels are `PoseBone.location/rotation_(euler|quaternion)/scale` in the
659
+ * bone's own REST space while three's `Bone` carries a transform relative to
660
+ * its PARENT BONE, so the two are separated by a matrix product that no
661
+ * per-channel relabelling can do. Composing per key would then also have to
662
+ * reproduce Blender's Bezier handles between keys; sampling at every integer
663
+ * frame and shipping LINEAR tracks is exact at every frame a Timeline can
664
+ * scrub to, and linear in between.
665
+ */
666
+ export interface BlenderActionClip {
667
+ readonly object: string | null;
668
+ readonly armature: string | null;
669
+ readonly action: string | null;
670
+ readonly slot: string | null;
671
+ /** `layered` at this pin — an `Action` has no `.fcurves`; they live at
672
+ * `layers[0].strips[0].channelbags[0].fcurves`. `legacy` is the branch for
673
+ * a build where the old attribute is still there. */
674
+ readonly channels?: string;
675
+ /** The scene's own name — the Timeline's one write addresses
676
+ * `bpy.data.scenes["<name>"]`, because an RNA path starts at `bpy.data.`. */
677
+ readonly scene?: string;
678
+ readonly frameCurrent: number;
679
+ readonly frameStart: number;
680
+ readonly frameEnd: number;
681
+ readonly fps: number;
682
+ readonly fcurves?: number;
683
+ readonly keys?: number;
684
+ /** The SUBJECT's own columns — the action this clip plays. */
685
+ readonly keyframes: readonly BlenderClipKeyColumn[];
686
+ /** Every animated object in the view layer, for the summary row's
687
+ * selection filter. Always present; empty for a file with no animation. */
688
+ readonly summary?: readonly BlenderClipSummaryObject[];
689
+ readonly tracks: readonly BlenderClipTrack[];
690
+ readonly clipStart?: number;
691
+ readonly clipEnd?: number;
692
+ readonly duration?: number;
693
+ readonly sampled?: number;
694
+ /** Bones the action names that the armature does not have. */
695
+ readonly unplayedBones?: readonly string[];
696
+ readonly reason?: string;
697
+ }