@volter/editor-blender 0.1.149 → 0.1.151

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 (93) hide show
  1. package/LICENSE +18 -9
  2. package/README.md +5 -5
  3. package/contributions/blender-animation-editors.tsx +17 -69
  4. package/contributions/blender-display-settings.ts +36 -0
  5. package/contributions/blender-export-gltf.ts +228 -0
  6. package/contributions/blender-export.verbs.ts +39 -0
  7. package/contributions/blender-gltf-export.verbs.ts +312 -0
  8. package/contributions/blender-header-menus.tsx +37 -133
  9. package/contributions/blender-icon-trace.mjs +8 -98
  10. package/contributions/blender-icons.source.mjs +96 -860
  11. package/contributions/blender-mixer-pose.ts +369 -0
  12. package/contributions/blender-model-opening.tsx +3 -7
  13. package/contributions/blender-node-editor.document.tsx +28 -180
  14. package/contributions/blender-node-geometry.ts +44 -230
  15. package/contributions/blender-node-panels.source.mjs +7 -121
  16. package/contributions/blender-outliner-authoring.ts +61 -415
  17. package/contributions/blender-outliner-model.ts +15 -112
  18. package/contributions/blender-play-area.tsx +100 -0
  19. package/contributions/blender-playback-movie.ts +409 -0
  20. package/contributions/blender-pose.ts +18 -368
  21. package/contributions/blender-properties-context.ts +1 -1
  22. package/contributions/blender-properties-model.ts +15 -90
  23. package/contributions/blender-properties-tab.tsx +3 -50
  24. package/contributions/blender-properties-view.tsx +15 -147
  25. package/contributions/blender-remedy.ts +14 -0
  26. package/contributions/blender-runtime-skin.ts +103 -99
  27. package/contributions/blender-runtime.document.tsx +217 -339
  28. package/contributions/blender-scene-mixer.ts +51 -0
  29. package/contributions/blender-screenshot.service.ts +28 -0
  30. package/contributions/blender-timeline-geometry.ts +16 -136
  31. package/contributions/blender-timeline.document.tsx +142 -332
  32. package/contributions/blender-uv-editor.document.tsx +48 -112
  33. package/contributions/blender-uv-geometry.ts +14 -179
  34. package/contributions/blender-version.status.tsx +43 -23
  35. package/contributions/blender.environment.ts +2 -2
  36. package/contributions/blender.icons.json +0 -9
  37. package/contributions/blender.keymap.ts +9 -51
  38. package/contributions/blender.node-panels.json +4 -13
  39. package/contributions/blender.page.ts +63 -0
  40. package/contributions/blender.status.tsx +9 -145
  41. package/contributions/blender.style.ts +25 -152
  42. package/contributions/blender.verbs.ts +52 -0
  43. package/contributions/model.layout.ts +2 -34
  44. package/contributions/models.finder.ts +12 -32
  45. package/contributions/properties-bone-constraints.inspector.tsx +1 -16
  46. package/contributions/properties-bone.inspector.tsx +2 -31
  47. package/contributions/properties-collection.inspector.tsx +1 -34
  48. package/contributions/properties-constraints.inspector.tsx +1 -21
  49. package/contributions/properties-data.inspector.tsx +2 -39
  50. package/contributions/properties-material.inspector.tsx +2 -33
  51. package/contributions/properties-modifiers.inspector.tsx +2 -30
  52. package/contributions/properties-object.inspector.tsx +2 -44
  53. package/contributions/properties-output.inspector.tsx +1 -27
  54. package/contributions/properties-particles.inspector.tsx +2 -46
  55. package/contributions/properties-physics.inspector.tsx +2 -41
  56. package/contributions/properties-render.inspector.tsx +1 -45
  57. package/contributions/properties-scene.inspector.tsx +1 -35
  58. package/contributions/properties-texture.inspector.tsx +1 -49
  59. package/contributions/properties-view-layer.inspector.tsx +1 -29
  60. package/contributions/properties-world.inspector.tsx +2 -39
  61. package/contributions/shading.layout.ts +3 -78
  62. package/contributions/uv-editing.layout.ts +3 -70
  63. package/dist-node/serving.mjs +13 -30
  64. package/host/blender-runtime-host.ts +541 -372
  65. package/host/scene-sound.ts +143 -0
  66. package/node/blender-mcp.ts +49 -93
  67. package/package.json +1209 -46
  68. package/producticons/blender-icons.otf +0 -0
  69. package/producticons/blender-product-icon-theme.json +129 -0
  70. package/serving/blender-routes.ts +9 -43
  71. package/serving/blender-wasm-artifact.ts +29 -166
  72. package/serving/index.ts +1 -1
  73. package/src/api.ts +9 -0
  74. package/src/layouts.tsx +1 -17
  75. package/src/looks.ts +1 -12
  76. package/src/model-document-saving.ts +25 -0
  77. package/src/model-play-extension.ts +173 -0
  78. package/src/node-view-state.ts +3 -25
  79. package/src/play-mode.ts +64 -38
  80. package/src/presentation.ts +6 -18
  81. package/src/regions.ts +3 -43
  82. package/src/start-document.ts +15 -0
  83. package/src/timeline-view-state.ts +4 -39
  84. package/src/uv-view-state.ts +4 -20
  85. package/themes/blender.json +299 -0
  86. package/view/blender-view-routes.ts +1 -1
  87. package/contributions/blender-game-panel.tsx +0 -809
  88. package/contributions/blender-palette.source.mjs +0 -350
  89. package/contributions/blender-play-skin.ts +0 -508
  90. package/contributions/blender-screenshot.menu.ts +0 -32
  91. package/contributions/blender.command.ts +0 -109
  92. package/contributions/blender.palette.json +0 -102
  93. package/src/play-live.ts +0 -27
@@ -1,31 +1,4 @@
1
- /**
2
- * BLENDER'S NODE EDITOR, AS NUMBERS — the geometry and the colours the node
3
- * view draws with, each one READ from Blender 5.2.0's own source at the
4
- * engine's pin (`fbe6228777e7`) and cited beside it.
5
- *
6
- * WORK.md §Blender in the tab is Blender, "Inspection parity", I5;
7
- * ARCHITECTURE-CORE §Blender north star, "Inspection parity, not editing
8
- * parity" and "The reference is Blender's SOURCE as well as its frames".
9
- * Blender's own drawing code is never run, ported as a UI system, or
10
- * recorded — it is READ as the specification of what to draw, which is what
11
- * every citation below is doing.
12
- *
13
- * THE SCALE. Every constant here is stated at `UI_SCALE_FAC = 1` and
14
- * `U.pixelsize = 1`, which makes `U.widget_unit = int(round(18 * 1)) + 2 * 1`
15
- * = **20** (`windowmanager/intern/wm_window.cc:779`). The node editor draws
16
- * WITHOUT DPI — "Nodes draw without DPI - the view zoom is flexible", the
17
- * comment above the macro block (`space_node/node_intern.hh:332`) — so a node's
18
- * tree-space size is these numbers exactly and the VIEW's zoom is the only
19
- * scale between them and the screen.
20
- *
21
- * WHY THE LAYOUT IS COMPUTED HERE AND NOT READ. `Node.dimensions` is RNA's
22
- * answer for a node's drawn size, and it is `bNode.runtime->draw_bounds`,
23
- * which only the node editor's own draw pass fills in. Measured 2026-09-19 on
24
- * the engine in the tab: every node of a freshly built Principled material
25
- * answers `dimensions == [0, 0]`, because this Blender has no node editor
26
- * region and never will. So the height is derived the way
27
- * `node_update_basis` derives it, from facts RNA does answer.
28
- */
1
+
29
2
 
30
3
  /* -------------------------------------------------------------------------- */
31
4
  /* Geometry — `space_node/node_intern.hh:332-344` */
@@ -65,10 +38,6 @@ export const NODE_ITEM_SPACING_Y = 0.1 * WIDGET_UNIT;
65
38
  /** `#define NODE_HEADER_ICON_SIZE (0.8f * U.widget_unit)` (`node_draw.cc:2143`). */
66
39
  export const NODE_HEADER_ICON_SIZE = 0.8 * WIDGET_UNIT;
67
40
 
68
- /** `#define NODE_MULTI_INPUT_LINK_GAP (0.25f * U.widget_unit)`
69
- * (`node_intern.hh:341`) — how far apart a multi-input socket fans its links. */
70
- export const NODE_MULTI_INPUT_LINK_GAP = 0.25 * WIDGET_UNIT;
71
-
72
41
  /** The 0.5 every filled rect is grown by "to prevent transparent gaps with the
73
42
  * outline" (`node_draw.cc:2920`), and the radius that goes with it. */
74
43
  export const NODE_FILL_PADDING = 0.5;
@@ -77,20 +46,10 @@ export const NODE_FILL_RADIUS = BASIS_RAD + NODE_FILL_PADDING;
77
46
  * (`node_draw.cc:3184-3211`). */
78
47
  export const NODE_OUTLINE_RADIUS = BASIS_RAD + PIXEL_SIZE;
79
48
 
80
- /** `NODE_GRID_UNIT` (`editors/include/ED_node_c.hh:31-32`) — the dot grid's
81
- * finest step, and the snapping unit. */
82
- export const NODE_GRID_STEP = 20;
83
-
84
49
  /** `LINK_WIDTH` (`space_node/drawnode.cc:1959`) — a noodle's stroke at 100 %,
85
50
  * clamped never to go below this as the view zooms out
86
51
  * (`drawnode.cc:2299-2302`); a FIELD link draws at 0.7 of it. */
87
52
  export const LINK_WIDTH = 2.5;
88
- export const LINK_FIELD_WIDTH_FACTOR = 0.7;
89
-
90
- /** `NODE_LINK_RESOL` (`node_intern.hh:343`) — how many segments Blender's own
91
- * CPU evaluation of a noodle uses. SVG draws the cubic itself, so this is
92
- * here as the record of what the reference samples, not as a step count. */
93
- export const NODE_LINK_RESOL = 12;
94
53
 
95
54
  /** `NODE_TREE_SCALE_SMALL` (`node_draw.cc:1596`) — below `1/aspect > 0.2`,
96
55
  * `draw_node_details` is false and Blender draws NO sockets, no reroute
@@ -113,7 +72,6 @@ export const REROUTE_RADIUS = NODE_SOCKSIZE;
113
72
  * margin)`, and the baseline sits `0.5 * margin_top + 0.35 * label_size`
114
73
  * below the top (`node_draw.cc:3474-3484`). */
115
74
  export const FRAME_MARGIN = 1.5 * WIDGET_UNIT;
116
- export const FRAME_LABEL_SIZE_DEFAULT = 20;
117
75
 
118
76
  /** The label's point size — `UI_DEFAULT_TEXT_POINTS` (`UI_interface_c.hh:425`),
119
77
  * which `style->widget` takes (`interface_style.cc:91`). A node's name is an
@@ -128,30 +86,12 @@ export const UI_TEXT_POINTS = 11;
128
86
  /** Every value is the DEFAULT THEME's, by member and line. A member absent
129
87
  * from the initializer takes the struct's zero and is called out. */
130
88
  export const NODE_THEME = {
131
- /** `wcol_numslider.item` — the FILLED portion of a number slider's back
132
- * (`widget_numslider`, `interface_widgets.cc`, which draws the item rect to
133
- * the value's proportion before the text).
134
- *
135
- * SAMPLED FROM THE FRAME, not recalled: `rgb(70,113,178)` at the Roughness
136
- * and Alpha fills on Blender 5.2.0 LTS's own shader editor at 2x, against
137
- * `rgb(83,83,83)` in the same row's unfilled remainder. It is BLUE — the
138
- * playhead's family — and the first pass here guessed a grey a step lighter
139
- * than the field, drew it, and produced a slider nobody could see. The
140
- * frame was on screen the whole time. */
89
+ /** wcol_numslider.item fills the value’s fraction of the slider; source colour is rgb(70,113,178). */
141
90
  sliderFill: '#4671b2',
142
91
  /** `.back` `0x1a1a1a00` (`:653`). The alpha is 0x00 and IRRELEVANT:
143
92
  * `draw_background_color` clears with the RGB and a hard 1.0
144
93
  * (`node_draw.cc:4713-4718`), so the canvas is opaque #1a1a1a. */
145
94
  background: '#1a1a1a',
146
- /** `.grid` `0x30303000` (`:661`) — alpha ZERO. Blender's dot grid is
147
- * therefore INVISIBLE under the shipped theme, and drawing one here would
148
- * be a mark the reference does not make. `.grid_levels` is 3 (`:671`) and
149
- * the draw is `view2d_dot_grid_draw(&v2d, TH_GRID, NODE_GRID_STEP_SIZE,
150
- * grid_levels)` (`node_draw.cc:4755-4756`) — a DOT grid, not
151
- * `UI_view2d_multi_grid_draw`, which the node editor does not call. */
152
- grid: '#303030',
153
- gridAlpha: 0,
154
- gridLevels: 3,
155
95
  /** `.syntaxl` `0x303030ff` (`:673`), which `resources.cc:659-660` returns
156
96
  * for `TH_NODE`. EVERY node's BODY is this colour whatever its class
157
97
  * (`node_draw.cc:3149`); only the header carries the class hue. */
@@ -171,11 +111,6 @@ export const NODE_THEME = {
171
111
  /** `.wire` `0x1a1a1aff` (`:662`) → `TH_WIRE`: a noodle's OUTLINE
172
112
  * (`drawnode.cc:2309`) and a socket's outline (`node_draw.cc:1502-1505`). */
173
113
  wire: '#1a1a1a',
174
- /** `.edge_select` `0xffffffb3` (`:665`), whose Python name IS `wire_select`
175
- * (`rna_userdef.cc:3604-3605`) — a SELECTED noodle blends toward this by
176
- * its own alpha (`drawnode.cc:2338-2352`). */
177
- wireSelect: '#ffffff',
178
- wireSelectAlpha: 0xb3 / 255,
179
114
  /** `.noodle_curving` `4` (`:670`) → `TH_NODE_CURVING`
180
115
  * (`resources.cc:713-714`). NOTE the divergence worth recording: RNA's
181
116
  * property default is 5 (`rna_userdef.cc:3658-3663`), but the SHIPPED THEME
@@ -293,7 +228,6 @@ export interface LaidOutSocket {
293
228
  readonly shape: string;
294
229
  readonly output: boolean;
295
230
  readonly linked: boolean;
296
- readonly multiInput: boolean;
297
231
  readonly hideValue: boolean;
298
232
  readonly value: number | string | boolean | readonly number[] | null;
299
233
  /** See {@link LayoutSocketInput.softMin} — carried through so the inline
@@ -314,7 +248,6 @@ export interface LaidOutSocket {
314
248
  }
315
249
 
316
250
  export interface LaidOutNode {
317
- readonly name: string;
318
251
  readonly x: number;
319
252
  readonly yTop: number;
320
253
  readonly yBottom: number;
@@ -333,7 +266,6 @@ interface LayoutSocketInput {
333
266
  readonly hide: boolean;
334
267
  readonly hideValue: boolean;
335
268
  readonly linked: boolean;
336
- readonly multiInput: boolean;
337
269
  readonly value: number | string | boolean | readonly number[] | null;
338
270
  /** The `default_value` property's SOFT range, present only when the socket
339
271
  * is a bounded scalar — which is exactly Blender's own test for drawing a
@@ -343,8 +275,7 @@ interface LayoutSocketInput {
343
275
  }
344
276
 
345
277
  interface LayoutNodeInput {
346
- readonly name: string;
347
- readonly location: readonly number[];
278
+ readonly location: readonly [number, number];
348
279
  readonly width: number;
349
280
  readonly collapsed: boolean;
350
281
  readonly inputs: readonly LayoutSocketInput[];
@@ -357,43 +288,10 @@ export function socketDraws(socket: LayoutSocketInput): boolean {
357
288
  return socket.enabled && !socket.hide;
358
289
  }
359
290
 
360
- /**
361
- * ONE NODE, LAID OUT — `node_update_basis` (`node_draw.cc:1288-1324`) run
362
- * over the facts RNA answers.
363
- *
364
- * ```
365
- * loc = node.location node_draw.cc:1295
366
- * dy = loc.y node_draw.cc:1297
367
- * dy -= NODE_DY node_draw.cc:1300 // the header, exactly 20
368
- * …outputs, then inputs…
369
- * xmin = loc.x ; xmax = loc.x + NODE_WIDTH(node) node_draw.cc:1309-1310
370
- * ymax = loc.y ; ymin = min(dy, loc.y - 2 * NODE_DY) node_draw.cc:1311-1312
371
- * ```
372
- * — so a node is never shorter than two header-heights, 40.
373
- *
374
- * WHICH OF BLENDER'S TWO BODY PATHS THIS IS, and the one divergence it
375
- * carries. `node_update_basis` branches on `is_node_panels_supported()` =
376
- * `decl->use_custom_socket_order` (`node_draw.cc:343-346, 1302-1307`):
377
- *
378
- * - the DECLARATION path (`node_update_basis_from_declaration`, `:1086-1218`)
379
- * spaces items through one margin table in units of `NODE_ITEM_SPACING_Y`
380
- * (`:886-1000`): 2× before the first socket, 1× between two sockets, 2×
381
- * after the last;
382
- * - the LEGACY path (`node_update_basis_from_socket_lists`, `:1227-1283`)
383
- * uses `NODE_DYS / 2` = 5 at the top, `NODE_ITEM_SPACING_Y` between
384
- * sockets, `NODE_DY / 4` = 5 between the outputs and the inputs, and
385
- * `NODE_DYS / 2` = 5 at the bottom.
386
- *
387
- * RNA EXPOSES NO `use_custom_socket_order`, so this cannot be read from the
388
- * engine. The declaration path is the one Blender 5.2 lays nearly every
389
- * built-in node out with, so it is the one implemented, and the cost is
390
- * STATED rather than hidden: a node Blender lays out the legacy way sits
391
- * 1 px lower at its first socket, 3 px lower at its first input, and ends
392
- * 1 px further down — at most 5 px over a whole node.
393
- */
291
+ /** Default node geometry uses declaration socket spacing: location anchors the top-left, the header is NODE_DY high, and the body is at least two headers tall. Traced custom-order declarations use layoutNodeWithPanels. */
394
292
  export function layoutNode(node: LayoutNodeInput): LaidOutNode {
395
- const x = node.location[0] ?? 0;
396
- const top = node.location[1] ?? 0;
293
+ const x = node.location[0];
294
+ const top = node.location[1];
397
295
  const width = node.width;
398
296
  const outputs = node.outputs.filter(socketDraws);
399
297
  const inputs = node.inputs.filter(socketDraws);
@@ -413,7 +311,6 @@ export function layoutNode(node: LayoutNodeInput): LaidOutNode {
413
311
  });
414
312
  };
415
313
  return {
416
- name: node.name,
417
314
  x,
418
315
  yTop,
419
316
  yBottom,
@@ -444,9 +341,7 @@ export function layoutNode(node: LayoutNodeInput): LaidOutNode {
444
341
  });
445
342
  if (outputs.length > 0 && inputs.length > 0) dy -= NODE_ITEM_SPACING_Y;
446
343
  inputs.forEach((socket, index) => {
447
- if (index > 0 || outputs.length > 0) {
448
- if (index > 0) dy -= NODE_ITEM_SPACING_Y;
449
- }
344
+ if (index > 0) dy -= NODE_ITEM_SPACING_Y;
450
345
  row(socket, false);
451
346
  });
452
347
  if (outputs.length > 0 || inputs.length > 0) {
@@ -456,7 +351,6 @@ export function layoutNode(node: LayoutNodeInput): LaidOutNode {
456
351
  }
457
352
 
458
353
  return {
459
- name: node.name,
460
354
  x,
461
355
  yTop: top,
462
356
  // `ymin = min_ff(dy, loc.y - 2 * NODE_DY)` — node_draw.cc:1311.
@@ -482,7 +376,6 @@ function socketAt(
482
376
  shape: socket.shape,
483
377
  output,
484
378
  linked: socket.linked,
485
- multiInput: socket.multiInput,
486
379
  hideValue: socket.hideValue,
487
380
  value: socket.value,
488
381
  ...(socket.softMin === undefined ? {} : { softMin: socket.softMin }),
@@ -513,14 +406,8 @@ function socketAt(
513
406
  export function linkHandles(
514
407
  from: readonly [number, number],
515
408
  to: readonly [number, number],
516
- curving: number = NODE_THEME.noodleCurving,
517
409
  ): [[number, number], [number, number]] {
518
- if (curving === 0) {
519
- return [
520
- [from[0] + (to[0] - from[0]) / 3, from[1] + (to[1] - from[1]) / 3],
521
- [from[0] + ((to[0] - from[0]) * 2) / 3, from[1] + ((to[1] - from[1]) * 2) / 3],
522
- ];
523
- }
410
+ const curving = NODE_THEME.noodleCurving;
524
411
  const distX = Math.abs(to[0] - from[0]);
525
412
  const distY = Math.abs(to[1] - from[1]);
526
413
  // `slope` divides by dist_x; Blender's own code does the same and relies on
@@ -562,18 +449,18 @@ export function nodeHeaderColor(node: {
562
449
  * (`node_draw.cc:3145-3149`). */
563
450
  export function nodeBodyColor(node: {
564
451
  readonly useCustomColor: boolean;
565
- readonly color: readonly number[];
452
+ readonly color: readonly [number, number, number];
566
453
  }): string {
567
454
  if (!node.useCustomColor) return NODE_THEME.body;
568
455
  return rgbFloatsToHex(node.color);
569
456
  }
570
457
 
571
- export function rgbFloatsToHex(channels: readonly number[]): string {
458
+ export function rgbFloatsToHex(channels: readonly [number, number, number, ...number[]]): string {
572
459
  const byte = (value: number): string =>
573
460
  Math.round(Math.min(1, Math.max(0, value)) * 255)
574
461
  .toString(16)
575
462
  .padStart(2, '0');
576
- return `#${byte(channels[0] ?? 0)}${byte(channels[1] ?? 0)}${byte(channels[2] ?? 0)}`;
463
+ return `#${byte(channels[0])}${byte(channels[1])}${byte(channels[2])}`;
577
464
  }
578
465
 
579
466
  /**
@@ -609,22 +496,9 @@ export function socketColor(type: string): string {
609
496
  /* SOCKET PANELS — `node_update_basis_from_declaration`'s other half */
610
497
  /* -------------------------------------------------------------------------- */
611
498
 
612
- /**
613
- * A PANEL HEADER'S ROW, in the layout: `NODE_DYS` tall, its centre exactly
614
- * half that below where the row starts — `locy -= h/2; center = locy;
615
- * locy -= h/2` (`node_draw.cc:1189-1195`). It is HALF a socket row, which is
616
- * the whole reason eight collapsed panels cost 160 px where their thirty
617
- * sockets cost 660.
618
- */
499
+ /** A panel-header row is NODE_DYS tall, half a socket row (node_draw.cc:1189-1195). */
619
500
  export const PANEL_HEADER_HEIGHT = NODE_DYS;
620
501
 
621
- /**
622
- * The header's DRAWN band is twice its laid-out height — `center ± NODE_DYS`
623
- * (`node_draw.cc:2005-2008`) — so two consecutive headers' bands abut exactly
624
- * at the 20-unit pitch they sit at.
625
- */
626
- export const PANEL_HEADER_BAND = 2 * NODE_DYS;
627
-
628
502
  /** `header_but_margin = NODE_MARGIN_X / 3` — where the triangle starts, and
629
503
  * the inset the whole header button takes (`node_draw.cc:2031-2038`). */
630
504
  export const PANEL_HEADER_MARGIN_X = NODE_MARGIN_X / 3;
@@ -635,34 +509,7 @@ export const PANEL_HEADER_BUT_PADDING = NODE_MARGIN_X / 4;
635
509
  * (`node_draw.cc:2059-2078`). */
636
510
  export const UI_UNIT_X = WIDGET_UNIT;
637
511
 
638
- /**
639
- * BLENDER'S PANEL TRIANGLE, as geometry rather than as a lookalike.
640
- *
641
- * `node_draw_panels` asks for `ICON_RIGHTARROW` when the panel is collapsed
642
- * and `ICON_DOWNARROW_HLT` when it is open, at `but_size = U.widget_unit *
643
- * 0.8` = {@link NODE_HEADER_ICON_SIZE} (`node_draw.cc:2035-2046`). Both marks
644
- * are single stroked chevrons, and these numbers are read straight off their
645
- * own path data in the checkout:
646
- *
647
- * - `release/datafiles/icons_svg/rightarrow.svg` —
648
- * `l3.64649 3.64648 -3.64649 3.64648` with a `.50005` round cap, under
649
- * `matrix(100 0 0 100 -40899.645 -60100.058)`: a chevron reaching **4**
650
- * units along its axis and **±3.646** across, stroked **1** unit wide, on
651
- * Blender's **8**-unit small-icon cell.
652
- * - `downarrow_hlt.svg` — `l4 4 4 -4`, the same chevron turned a quarter.
653
- *
654
- * DELIBERATELY NOT ADDED TO `blender.icons.traced.json`, and the reason is a
655
- * measured defect in that trace rather than a preference:
656
- * `traceBlenderIcon`'s one scaling rule is `16 / viewBox.width`, which is
657
- * right only for the 1600-wide artboards that carry a `matrix(100 …)`.
658
- * These two files are 700x1100 and 1100x700, and **13 glyphs already in that
659
- * set** come from non-1600 artboards and are scaled wrong —
660
- * `blender-mod-particle-instance` traces to a box at **[273, -60]**, off the
661
- * 16-unit grid entirely, and `rightarrow` would have traced to a 10.8x20.3
662
- * mark. Correcting that trace moves twelve shipped marks and belongs to
663
- * whoever owns the icon set; stating it here, with the numbers, is this
664
- * unit's honest half.
665
- */
512
+ /** Panel toggles draw the source rightarrow and downarrow_hlt chevrons in Blender’s 8-unit small-icon cell, scaled to NODE_HEADER_ICON_SIZE. */
666
513
  export const PANEL_TRIANGLE = {
667
514
  /** Blender's small-icon cell — the space the numbers below are on. */
668
515
  cell: 8,
@@ -686,21 +533,17 @@ export const PANEL_TRIANGLE = {
686
533
  export const PANEL_SUB_BACK = '#000000';
687
534
  export const PANEL_SUB_BACK_ALPHA = (0x1f / 255) * 1.5;
688
535
 
689
- /**
690
- * One item of a TRACED declaration — a row of `blender.node-panels.json`,
691
- * which is `blender-node-panels.source.mjs`'s reading of the node's C++
692
- * `declare()` body. The five kinds are `flat_item::Type`'s five
693
- * (`node_draw.cc:798-831`).
694
- */
536
+ interface NodeSocketDecl {
537
+ readonly name: string;
538
+ readonly identifier?: string;
539
+ readonly panelToggle?: boolean;
540
+ readonly alignWithPrevious?: boolean;
541
+ }
542
+
543
+ /** One item of the traced C++ declaration in blender.node-panels.json: inputs, outputs, panels, layouts and separators expand into the flat layout items. */
695
544
  export type NodeDeclItem =
696
- | {
697
- readonly kind: 'input' | 'output';
698
- readonly name: string;
699
- readonly identifier?: string;
700
- readonly panelToggle?: boolean;
701
- readonly conditional?: boolean;
702
- readonly alignWithPrevious?: boolean;
703
- }
545
+ | ({ readonly kind: 'input' } & NodeSocketDecl)
546
+ | ({ readonly kind: 'output' } & NodeSocketDecl)
704
547
  | {
705
548
  readonly kind: 'panel';
706
549
  readonly name: string;
@@ -712,7 +555,6 @@ export type NodeDeclItem =
712
555
 
713
556
  export interface NodeDeclaration {
714
557
  readonly customSocketOrder: boolean;
715
- readonly conditional?: boolean;
716
558
  readonly items: readonly NodeDeclItem[];
717
559
  }
718
560
 
@@ -722,23 +564,7 @@ export interface NodeDeclaration {
722
564
  const declIdentifier = (item: { readonly name: string; readonly identifier?: string }): string =>
723
565
  item.identifier ?? item.name;
724
566
 
725
- /**
726
- * DOES THE TRACED DECLARATION DESCRIBE THIS LIVE NODE?
727
- *
728
- * The ruling's own condition — "when the live socket list disagrees with the
729
- * traced declaration the view draws flat with the frame warning it draws
730
- * today". The check is STRICT and ordered: every socket the declaration
731
- * names, in declaration order, against `node.inputs`/`node.outputs` by
732
- * IDENTIFIER.
733
- *
734
- * Strict is the right strength because an UNAVAILABLE socket is still in the
735
- * live list: `.available(false)` sets `SOCK_UNAVAIL`, which RNA reports as
736
- * `enabled == False`, not as an absent socket. So a declaration that matches
737
- * the engine matches it exactly, and anything less is the engine having moved
738
- * out from under the trace — a different Blender, a `declare()` that took the
739
- * other arm of an `if`, a node group whose panels are runtime data. Each of
740
- * those must fall back, and be SEEN to.
741
- */
567
+ /** Match each traced socket identifier against the live inputs and outputs in order. Unavailable sockets remain in the live list. A mismatch or runtime group declaration uses the named flat-layout fallback. */
742
568
  export function declarationAgrees(
743
569
  declaration: NodeDeclaration,
744
570
  inputs: readonly { readonly identifier: string }[],
@@ -747,7 +573,7 @@ export function declarationAgrees(
747
573
  if (!declaration.customSocketOrder) {
748
574
  return {
749
575
  ok: false,
750
- why: 'its declaration does not set use_custom_socket_order, so Blender lays it out the legacy way',
576
+ why: 'its declaration does not set use_custom_socket_order, so Blender uses the default socket order',
751
577
  };
752
578
  }
753
579
  const declaredIn: string[] = [];
@@ -769,8 +595,8 @@ export function declarationAgrees(
769
595
  return `the declaration names ${declared.length} ${side}${declared.length === 1 ? '' : 's'} and the engine reports ${live.length}`;
770
596
  }
771
597
  for (let index = 0; index < declared.length; index++) {
772
- if (declared[index] !== live[index]?.identifier) {
773
- return `${side} ${index} is "${live[index]?.identifier}" in the engine and "${declared[index]}" in the declaration`;
598
+ if (declared[index] !== live[index]!.identifier) {
599
+ return `${side} ${index} is "${live[index]!.identifier}" in the engine and "${declared[index]}" in the declaration`;
774
600
  }
775
601
  }
776
602
  return null;
@@ -850,12 +676,10 @@ const MARGIN_BETWEEN: Partial<Record<FlatType, Partial<Record<FlatType, number>>
850
676
  PanelContentEnd: { Socket: 1, Separator: 1, Layout: 1, PanelHeader: 3, PanelContentEnd: 0 },
851
677
  };
852
678
 
853
- interface FlatItem {
854
- readonly type: FlatType;
855
- readonly input?: LayoutSocketInput | undefined;
856
- readonly output?: LayoutSocketInput | undefined;
857
- readonly panel?: PanelState | undefined;
858
- }
679
+ type FlatItem =
680
+ | { readonly type: 'Socket'; readonly input?: LayoutSocketInput | undefined; readonly output?: LayoutSocketInput | undefined }
681
+ | { readonly type: 'Layout' | 'Separator' }
682
+ | { readonly type: 'PanelHeader' | 'PanelContentBegin' | 'PanelContentEnd'; readonly panel: PanelState };
859
683
 
860
684
  interface PanelState {
861
685
  readonly name: string;
@@ -900,14 +724,13 @@ function flatten(
900
724
  }
901
725
  // `add_flat_items_for_socket`: an ALIGNED socket joins the previous
902
726
  // item's row instead of opening one (`node_draw.cc:703-717`).
903
- const last = out[out.length - 1];
904
- if (item.alignWithPrevious === true && previousVisible && last && last.type === 'Socket') {
727
+ if (item.alignWithPrevious === true && previousVisible) {
728
+ const last = out[out.length - 1] as Extract<FlatItem, { type: 'Socket' }>;
905
729
  out[out.length - 1] = {
906
730
  type: 'Socket',
907
731
  input: item.kind === 'input' ? socket : last.input,
908
732
  output: item.kind === 'output' ? socket : last.output,
909
733
  };
910
- previousVisible = true;
911
734
  continue;
912
735
  }
913
736
  out.push({
@@ -929,12 +752,6 @@ function flatten(
929
752
  if (showOptions) out.push({ type: 'Layout' });
930
753
  continue;
931
754
  }
932
- // A DISCRIMINANT THAT IS ITSELF A UNION (`kind: 'input' | 'output'`) does
933
- // not narrow away in the NEGATIVE branch of `a === 'input' || a ===
934
- // 'output'`, so without this guard `item` still carries that member here
935
- // and `item.items` does not typecheck. It is a compiler fact, not a real
936
- // branch.
937
- if (item.kind !== 'panel') continue;
938
755
  // A PANEL. `panel_input_decl()` (`node_declaration.cc:528-542`): the
939
756
  // panel's FIRST item, when it is a `panel_toggle` boolean input, is drawn
940
757
  // in the header and takes no row of its own.
@@ -1015,8 +832,8 @@ export interface LaidOutPanelledNode extends LaidOutNode {
1015
832
  * with.
1016
833
  */
1017
834
  export function layoutNodeWithPanels(node: PanelLayoutInput): LaidOutPanelledNode {
1018
- const x = node.location[0] ?? 0;
1019
- const top = node.location[1] ?? 0;
835
+ const x = node.location[0];
836
+ const top = node.location[1];
1020
837
  const width = node.width;
1021
838
  const sockets = new Map<string, LayoutSocketInput>();
1022
839
  for (const socket of node.inputs) sockets.set(`input:${socket.identifier}`, socket);
@@ -1043,13 +860,12 @@ export function layoutNodeWithPanels(node: PanelLayoutInput): LaidOutPanelledNod
1043
860
  dy -= NODE_DYS; // `get_margin_empty()` — node_draw.cc:833-836
1044
861
  }
1045
862
  for (let index = 0; index < items.length; index++) {
1046
- const item = items[index];
1047
- if (!item) continue;
863
+ const item = items[index]!;
1048
864
  const previous = items[index - 1];
1049
865
  const margin =
1050
866
  index === 0
1051
867
  ? (MARGIN_FROM_TOP[item.type] ?? 0)
1052
- : (MARGIN_BETWEEN[previous?.type ?? 'Socket']?.[item.type] ?? 0);
868
+ : (MARGIN_BETWEEN[previous!.type]?.[item.type] ?? 0);
1053
869
  dy -= margin * NODE_ITEM_SPACING_Y;
1054
870
 
1055
871
  if (item.type === 'Socket') {
@@ -1066,7 +882,7 @@ export function layoutNodeWithPanels(node: PanelLayoutInput): LaidOutPanelledNod
1066
882
  dy -= NODE_DY;
1067
883
  continue;
1068
884
  }
1069
- if (item.type === 'PanelHeader' && item.panel) {
885
+ if (item.type === 'PanelHeader') {
1070
886
  dy -= PANEL_HEADER_HEIGHT / 2;
1071
887
  item.panel.centerY = dy;
1072
888
  dy -= PANEL_HEADER_HEIGHT / 2;
@@ -1075,8 +891,8 @@ export function layoutNodeWithPanels(node: PanelLayoutInput): LaidOutPanelledNod
1075
891
  }
1076
892
  continue;
1077
893
  }
1078
- if (item.type === 'PanelContentBegin' && item.panel) item.panel.contentTop = dy;
1079
- if (item.type === 'PanelContentEnd' && item.panel) item.panel.contentBottom = dy;
894
+ if (item.type === 'PanelContentBegin') item.panel.contentTop = dy;
895
+ if (item.type === 'PanelContentEnd') item.panel.contentBottom = dy;
1080
896
  }
1081
897
 
1082
898
  const last = items[items.length - 1];
@@ -1097,16 +913,14 @@ export function layoutNodeWithPanels(node: PanelLayoutInput): LaidOutPanelledNod
1097
913
  }
1098
914
  }
1099
915
 
1100
- // `tag_final_panel` (`node_draw.cc:1063-1082`): walking back from the end,
1101
- // the innermost panel whose content runs to the node's very bottom.
916
+ // `tag_final_panel` (`node_draw.cc:1063-1082`): mark each closing panel at the node's bottom.
1102
917
  for (let index = items.length - 1; index >= 0; index--) {
1103
- const item = items[index];
1104
- if (!item || item.type !== 'PanelContentEnd') break;
1105
- if (item.panel) item.panel.fillsNodeEnd = true;
918
+ const item = items[index]!;
919
+ if (item.type !== 'PanelContentEnd') break;
920
+ item.panel.fillsNodeEnd = true;
1106
921
  }
1107
922
 
1108
923
  return {
1109
- name: node.name,
1110
924
  x,
1111
925
  yTop: top,
1112
926
  yBottom: Math.min(dy, top - 2 * NODE_DY),