@volter/editor-blender 0.1.150 → 0.1.152

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 +26 -56
  13. package/contributions/blender-node-editor.document.tsx +30 -181
  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 +99 -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 +59 -91
  23. package/contributions/blender-properties-tab.tsx +3 -50
  24. package/contributions/blender-properties-view.tsx +39 -181
  25. package/contributions/blender-remedy.ts +14 -0
  26. package/contributions/blender-runtime-skin.ts +103 -99
  27. package/contributions/blender-runtime.document.tsx +170 -618
  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 +145 -333
  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 +45 -31
  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 +64 -0
  40. package/contributions/blender.status.tsx +8 -150
  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 +3 -30
  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 -32
  64. package/host/blender-runtime-host.ts +795 -412
  65. package/host/scene-sound.ts +143 -0
  66. package/node/blender-mcp.ts +52 -95
  67. package/package.json +1221 -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 -48
  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/node-view-state.ts +3 -25
  78. package/src/play-mode.ts +13 -42
  79. package/src/play-tool.ts +44 -0
  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,49 +1,11 @@
1
- /**
2
- * THE NODE EDITOR — Blender's Shader Editor, READ-ONLY, over the active
3
- * material's RNA node tree (WORK.md §Blender in the tab is Blender,
4
- * "Inspection parity", I5; ARCHITECTURE-CORE §Blender north star).
5
- *
6
- * OUR PANEL, BLENDER'S DRAWING. Blender's own UI layer — `bl_ui`, the node
7
- * editor's C++ drawing code — is never run, ported as a UI system, or
8
- * recorded here. It is READ as the specification of what to draw, and every
9
- * number it specifies lives in `./blender-node-geometry.ts` with its file and
10
- * line beside it. This module is an ordinary React component over that data.
11
- *
12
- * INSPECTION PARITY, NOT EDITING PARITY. Nothing here writes. A gesture that
13
- * WOULD edit — dragging a node, dragging a link, typing into a socket's value
14
- * — is refused BY NAME in the frame's status line rather than quietly doing
15
- * nothing, which is the ruling's own "never a silent degrade". The only state
16
- * this view owns is INSPECTION state: which node is looked at, and where the
17
- * view is panned. Selecting a node here does not write `Node.select` in the
18
- * engine (that would be a mutation the document saves); the ENGINE's own
19
- * selection is still drawn, in Blender's own selected outline.
20
- *
21
- * WHY SVG AND NOT CANVAS 2D. The tree is a few dozen rounded rects, circles
22
- * and cubics, and SVG gives three things a 2D context would have to
23
- * reimplement: the browser's own `C` path segment for `node_link_bezier`'s
24
- * cubic, real text layout and measurement for the header labels, and hit
25
- * testing for free on every node and socket. The measured tree this unit was
26
- * built against is 2 nodes and 37 sockets; a pathological 200-node tree is
27
- * ~1,200 elements, which is inside what a browser lays out in one frame. The
28
- * moment a tree is measured slow, the answer is a canvas with the same
29
- * geometry module behind it — which is why the geometry lives in its own
30
- * module and this file holds no constants.
31
- *
32
- * WHY IT IS THE DRAWER AND NOT A SECOND DOCUMENT. Blender's Shading workspace
33
- * puts the Shader Editor in the AREA BELOW the 3D viewport, and this host's
34
- * bottom group (`volter:bottom-center`) is that area: the Model workspace hides
35
- * it for exactly this reason, recorded in `workspace-regions.ts` — "Blender's
36
- * modeling workspace has no timeline strip". The Shading workspace shows it
37
- * and puts this in it.
38
- */
1
+
39
2
 
40
3
  import type { BlenderNode, BlenderNodeTree } from '@volter/blender-engine/browser/rna';
41
- import { registerViewVerbs } from '@volter/sdk/views';
42
4
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
5
+ import { StateSurface } from '@volter/sdk/widgets';
43
6
  import {
44
7
  blenderNodeTree,
45
8
  blenderRnaVersion,
46
- NODE_VIEW_VERBS,
47
9
  subscribeBlenderRna,
48
10
  } from '../host/blender-runtime-host';
49
11
  import type { NodeViewTransform } from '../src/node-view-state';
@@ -107,26 +69,7 @@ export const point = 'workspace.document';
107
69
  * the editor-type menu): "Shader Editor" for a `ShaderNodeTree`. */
108
70
  export const title = 'Shader Editor';
109
71
 
110
- // THE VIEW'S PRODUCT DOOR (WORK.md §The core is Code-OSS U8, ruling 1). This
111
- // view's verbs are published ONCE, here, where the view itself is contributed:
112
- // under the Code-OSS frame each becomes a `volter.blender-node-view.<verb>`
113
- // command the bridge dispatches into the view, and standalone `cyclotron edit` —
114
- // which has no command service — reaches the SAME table through the session's
115
- // `blender-node-view` verb. One table, two doors, which is why the remaining
116
- // read-only editors (UV Editing, Animation, Texture Paint) add no session verb
117
- // of their own: they register here instead.
118
- // THIS LINE RUNS ONCE PER EVALUATION, and a contribution is evaluated more
119
- // than once per session by design — the tool loader imports it as
120
- // `?t=<version>`, so a save re-evaluates it against a registry that already
121
- // holds the view. `registerViewVerbs` recognises the re-evaluation and the
122
- // newer table wins; measured live 2026-09-19, before it did, the first reload
123
- // threw and the loader reported "editor app failed to start", taking the whole
124
- // editor down with the view.
125
- registerViewVerbs(NODE_VIEW_VERBS);
126
-
127
- /** The N-panel's width. `bl_ui` sizes a sidebar in `UI_UNIT_X`; the node
128
- * editor's sidebar opens at the region default, measured in Blender 5.2 at
129
- * ~16 units. */
72
+ /** The node sidebar opens at about 16 UI_UNIT_X units in Blender 5.2. */
130
73
  const SIDEBAR_WIDTH = 16 * 20;
131
74
 
132
75
  /** One unit of tree space per CSS px is Blender's zoom 1 — the node editor
@@ -137,23 +80,13 @@ const ZOOM_MAX = 4;
137
80
  type View = NodeViewTransform;
138
81
 
139
82
  export default function BlenderNodeEditor() {
140
- // THE TREE'S FRESHNESS IS THE TREE'S, NOT THE PRESENTED FRAME'S (ruling 3,
141
- // 2026-09-19). This used to read the properties model's version, which moves
142
- // on a presented frame — and collapsing a socket panel writes
143
- // `Node.panel_states[n].is_collapsed`, which presents NOTHING, so the view
144
- // kept drawing the old tree until the document was reopened (measured by the
145
- // socket-panel step that shipped the panels). `blenderRnaVersion` is the RNA
146
- // door's own, bumped by the write, and a frame bumps it too.
83
+ // Use the RNA door’s version: every write and presented frame bumps it, including panel-state writes that change no rendered geometry.
147
84
  const version = useSyncExternalStore(subscribeBlenderRna, blenderRnaVersion, blenderRnaVersion);
148
85
  const [tree, setTree] = useState<BlenderNodeTree | null>(null);
149
86
  const [error, setError] = useState<string | null>(null);
150
87
  const canvas = useRef<HTMLDivElement | null>(null);
151
88
  const framed = useRef(false);
152
- // THE VIEW'S STATE IS THE STORE'S (`../src/node-view-state.ts`), not this
153
- // component's, because the session verb `blender-node-view` has to reach it:
154
- // `editor.document.*` is scoped to the active CENTER document and a drawer
155
- // utility is not one, so a `useState` here would be a surface nothing in the
156
- // product could read or drive.
89
+ // Share view state with the blender-node-view command through node-view-state.ts.
157
90
  useSyncExternalStore(subscribeNodeView, nodeViewVersion, nodeViewVersion);
158
91
  const { transform: view, looked, refusal, size } = nodeViewState();
159
92
  const setView = useCallback((next: View | ((current: View) => View)): void => {
@@ -272,7 +205,7 @@ export default function BlenderNodeEditor() {
272
205
  // `VIEW2D_OT_pan`, bound to `MIDDLEMOUSE` in the default keymap).
273
206
  if (event.button !== 1) return;
274
207
  event.preventDefault();
275
- (event.target as Element).setPointerCapture?.(event.pointerId);
208
+ (event.target as Element).setPointerCapture(event.pointerId);
276
209
  panning.current = { x: event.clientX, y: event.clientY, cx: view.cx, cy: view.cy };
277
210
  };
278
211
  const onPointerMove = (event: React.PointerEvent<SVGSVGElement>) => {
@@ -310,11 +243,7 @@ export default function BlenderNodeEditor() {
310
243
  >
311
244
  <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0, minHeight: 0 }}>
312
245
  <NodeEditorHeader tree={tree} zoom={view.zoom} onViewAll={viewAll} onRefuse={refuse} />
313
- {/* THE CANVAS BOX. Its height comes from FLEX, never from its own
314
- content: a `height: 100%` SVG inside an auto-height parent makes
315
- the parent's height a function of the SVG's, which is a function
316
- of the parent's — measured live 2026-09-19, that loop settled at
317
- ~30 px and drew the whole tree in a sliver at 31 % zoom. */}
246
+ {/* Flex supplies the canvas height; a percentage-height SVG inside an auto-height parent would size the parent from its own content. */}
318
247
  <div ref={canvas} style={{ position: 'relative', flex: 1, minHeight: 0 }}>
319
248
  <svg
320
249
  role="presentation"
@@ -383,8 +312,6 @@ interface LaidLink {
383
312
  readonly key: string;
384
313
  readonly from: readonly [number, number];
385
314
  readonly to: readonly [number, number];
386
- readonly fromColor: string;
387
- readonly toColor: string;
388
315
  readonly muted: boolean;
389
316
  readonly valid: boolean;
390
317
  }
@@ -419,7 +346,6 @@ function layoutTree(tree: BlenderNodeTree | null): {
419
346
  const placed = new Map<string, LaidOutNode>();
420
347
  const entries = tree.nodes.map((node): LaidEntry => {
421
348
  const flat = {
422
- name: node.name,
423
349
  location: node.location,
424
350
  width: node.width,
425
351
  collapsed: node.collapsed,
@@ -469,9 +395,8 @@ function layoutTree(tree: BlenderNodeTree | null): {
469
395
  return { node, laid, panels: [], fallback: null, approximate: false };
470
396
  });
471
397
  const links: LaidLink[] = tree.links.flatMap((link, index) => {
472
- const from = placed.get(link.fromNode);
473
- const to = placed.get(link.toNode);
474
- if (!from || !to) return [];
398
+ const from = placed.get(link.fromNode)!;
399
+ const to = placed.get(link.toNode)!;
475
400
  const fromSocket = from.sockets.find((s) => s.output && s.identifier === link.fromSocket);
476
401
  const toSocket = to.sockets.find((s) => !s.output && s.identifier === link.toSocket);
477
402
  if (!fromSocket || !toSocket) return [];
@@ -480,8 +405,6 @@ function layoutTree(tree: BlenderNodeTree | null): {
480
405
  key: `${link.fromNode}.${link.fromSocket}→${link.toNode}.${link.toSocket}#${index}`,
481
406
  from: [fromSocket.x, fromSocket.y] as const,
482
407
  to: [toSocket.x, toSocket.y] as const,
483
- fromColor: socketColor(fromSocket.type),
484
- toColor: socketColor(toSocket.type),
485
408
  muted: link.muted,
486
409
  valid: link.valid,
487
410
  },
@@ -837,11 +760,7 @@ function PanelHeader({
837
760
  );
838
761
  }
839
762
 
840
- /** Blender's socket mark: a 10×10 box (radius `NODE_SOCKSIZE`) in the socket
841
- * TYPE's colour with a 1 px `TH_WIRE` outline, shaped by `display_shape`
842
- * (`node_draw.cc:1824-1857`; the shapes themselves are SDFs in
843
- * `gpu_shader_2D_node_socket*.glsl`, which is outside the sparse checkout —
844
- * the silhouettes below are drawn from the enum's own names). */
763
+ /** Socket marks use Blender's type colours, TH_WIRE outlines and display_shape silhouettes. */
845
764
  function Socket({
846
765
  socket,
847
766
  node,
@@ -925,12 +844,8 @@ function Socket({
925
844
  * `:529`), which is why an output's name is right-aligned against the node's
926
845
  * edge and an input's widget fills the row.
927
846
  *
928
- * WHY THE WIDGET IS DRAWN HERE AND NOT BORROWED FROM THE PROPERTIES RAIL.
929
- * This surface is a ZOOMING tree-space canvas: a DOM control would need a
930
- * `foreignObject` per socket and would not scale with the view transform, so
931
- * it would stop being the same drawing at any zoom but 1. The Properties
932
- * rail's widgets are reused where they belong — the N-panel, which is
933
- * ordinary unzoomed DOM.
847
+ * SVG widgets use the node canvas's tree-space transform. The N-panel uses
848
+ * ordinary unzoomed DOM controls.
934
849
  */
935
850
  function SocketRow({
936
851
  socket,
@@ -965,6 +880,7 @@ function SocketRow({
965
880
  }
966
881
 
967
882
  const isColour = Array.isArray(socket.value) && socket.value.length >= 3;
883
+ const fraction = sliderFraction(socket);
968
884
  return (
969
885
  <g
970
886
  onPointerDown={(event) => {
@@ -989,15 +905,8 @@ function SocketRow({
989
905
  fill="#545454"
990
906
  transform="scale(1 -1)"
991
907
  />
992
- {/* THE SLIDER'S FILL. A scalar socket with a bounded SOFT range is drawn
993
- by Blender as `UI_BTYPE_NUM_SLIDER`, whose back is filled to the
994
- value's proportion of that range (`widget_numslider`,
995
- `interface_widgets.cc`) — which is why Roughness at 0.5 reads as
996
- half-filled at a glance and ours read as a text box (sighted read,
997
- 2026-09-20). No range means no fill, which is also what Blender does
998
- for an unbounded number. The fill is clipped to the field's own
999
- rounded rect so a full-value slider does not square off its corners. */}
1000
- {sliderFraction(socket) !== null && (
908
+ {/* A bounded scalar socket fills its rounded field to its fraction of the soft range (widget_numslider). Unbounded values have no fill. */}
909
+ {fraction !== null && (
1001
910
  <>
1002
911
  <clipPath id={`sl-${socket.identifier}`}>
1003
912
  <rect
@@ -1011,7 +920,7 @@ function SocketRow({
1011
920
  <rect
1012
921
  x={left}
1013
922
  y={centre - NODE_DY / 2 + 1}
1014
- width={(right - left) * (sliderFraction(socket) ?? 0)}
923
+ width={(right - left) * fraction}
1015
924
  height={NODE_DY - 2}
1016
925
  fill={NODE_THEME.sliderFill}
1017
926
  clipPath={`url(#sl-${socket.identifier})`}
@@ -1037,7 +946,7 @@ function SocketRow({
1037
946
  width={30}
1038
947
  height={NODE_DY - 6}
1039
948
  rx={2}
1040
- fill={rgbFloatsToHex(socket.value as readonly number[])}
949
+ fill={rgbFloatsToHex(socket.value as unknown as readonly [number, number, number, ...number[]])}
1041
950
  transform="scale(1 -1)"
1042
951
  />
1043
952
  ) : (
@@ -1059,15 +968,14 @@ function SocketRow({
1059
968
  }
1060
969
 
1061
970
  /**
1062
- * How far along its soft range a scalar socket sits, or `null` when it is not
1063
- * a slider at all. Blender's own test is the property's soft range being
1064
- * bounded (`ui_but_is_slider` → `UI_BTYPE_NUM_SLIDER`); the engine reports the
1065
- * pair only for a bounded scalar, so the presence of the pair IS the test.
971
+ * A scalar factor socket's fraction of its soft range, or null without a
972
+ * numeric value and range. The engine reports only finite, increasing soft
973
+ * ranges for factor scalars.
1066
974
  */
1067
975
  function sliderFraction(socket: LaidOutSocket): number | null {
1068
976
  const { softMin, softMax, value } = socket;
1069
977
  if (softMin === undefined || softMax === undefined) return null;
1070
- if (typeof value !== 'number' || softMax <= softMin) return null;
978
+ if (typeof value !== 'number') return null;
1071
979
  return Math.min(1, Math.max(0, (value - softMin) / (softMax - softMin)));
1072
980
  }
1073
981
 
@@ -1104,10 +1012,8 @@ function Noodle({ link }: { readonly link: LaidLink }) {
1104
1012
  );
1105
1013
  }
1106
1014
 
1107
- /** A frame: its own colour at `TH_NODE_FRAME`'s alpha, radius `BASIS_RAD` with
1108
- * no 0.5 padding (`frame_node_draw_background`, `node_draw.cc:3749-3772`),
1109
- * and a centred label `0.5 * margin_top + 0.35 * label_size` below the top
1110
- * (`node_draw.cc:3474-3484, 3699-3703`). */
1015
+ /** A frame with its own colour at TH_NODE_FRAME's alpha, BASIS_RAD corners and
1016
+ * a centred 20-pixel label 25 tree units below the top. */
1111
1017
  function FrameNode({ node, laid }: { readonly node: BlenderNode; readonly laid: LaidOutNode }) {
1112
1018
  const height = Math.max(laid.yTop - laid.yBottom, FRAME_MARGIN * 2);
1113
1019
  return (
@@ -1140,21 +1046,7 @@ function FrameNode({ node, laid }: { readonly node: BlenderNode; readonly laid:
1140
1046
 
1141
1047
  /* -------------------------------------------------------------------------- */
1142
1048
 
1143
- /**
1144
- * THE HEADER, per `bl_ui/space_node.py:41-260` — read as a specification and
1145
- * drawn READ-ONLY, which is where it diverges from Blender by design: a
1146
- * control that would WRITE is drawn as what it reads and refuses the click by
1147
- * name. What the shader path's header carries, in order: the tree-type well
1148
- * (`template_header`, `:55`), the shader-type selector (`snode.shader_type`,
1149
- * `:62`), the material slot popover and the material ID (`:79-93`), the pin
1150
- * (`:222-224`), the breadcrumb parent (`:228-230`), snapping
1151
- * (`tool_settings.use_snap_node`, `:249-252`) and the overlays popover
1152
- * (`:254-260`).
1153
- *
1154
- * There is deliberately NO "Use Nodes" checkbox: measured in the source, 5.2
1155
- * draws it only for Line Style (`:119`) and Texture (`:133`) trees, never for
1156
- * a material's.
1157
- */
1049
+ /** Read-only tree/material labels, zoom and View All; Use Nodes reports its write refusal. */
1158
1050
  function NodeEditorHeader({
1159
1051
  tree,
1160
1052
  zoom,
@@ -1213,20 +1105,7 @@ function NodeEditorHeader({
1213
1105
  );
1214
1106
  }
1215
1107
 
1216
- /**
1217
- * THE STATUS LINE — where a refusal lands, and where anything this view
1218
- * cannot draw is named rather than silently skipped.
1219
- *
1220
- * THE PANEL GAP IS CLOSED, and what is left of it is stated here rather than
1221
- * hidden. A node's socket-panel MEMBERSHIP is traced from Blender's own
1222
- * `declare()` bodies (`blender.node-panels.json`) and its COLLAPSED STATE is
1223
- * read live from `Node.panel_states`, so a Principled BSDF draws the eight
1224
- * collapsed panel rows Blender draws. Two things still fall back, and both
1225
- * say so by name: a node whose LIVE socket list disagrees with the traced
1226
- * declaration (the ruling's own condition), and a node the trace does not
1227
- * cover at all — a node GROUP, whose panels are its tree interface's runtime
1228
- * data rather than a declaration.
1229
- */
1108
+ /** The status line names refusals and node-layout limitations. Socket panels use traced declarations and live panel_states; mismatched socket lists and runtime node groups draw flat with a named warning. */
1230
1109
  function StatusLine({
1231
1110
  entries,
1232
1111
  error,
@@ -1272,20 +1151,7 @@ function StatusLine({
1272
1151
  );
1273
1152
  }
1274
1153
 
1275
- /**
1276
- * THE N-PANEL'S NODE TAB — `NODE_PT_active_node_generic`
1277
- * (`bl_ui/space_node.py:800-841`), in its own order: `name` (`:819`), `label`
1278
- * (`:820`), the Color heading with `use_custom_color` and `color`
1279
- * (`:822-834`), then `show_options` (`:837`) and `mute` (`:838`). Blender
1280
- * 5.2 has no separate `NODE_PT_active_node_color`; the colour controls are
1281
- * that panel's own sub-column.
1282
- *
1283
- * `NODE_PT_active_node_properties` (`:844-857`) is ONE call —
1284
- * `layout.template_node_inputs(node)` — and that template is C: it draws the
1285
- * same per-socket widgets the node body draws. The node's own inputs are
1286
- * listed here with their values for exactly that reason, which is the same
1287
- * fact through the same door.
1288
- */
1154
+ /** Read-only looked-at node name, label, colour, mute and visible socket values. */
1289
1155
  function NodePanel({
1290
1156
  node,
1291
1157
  onRefuse,
@@ -1301,15 +1167,9 @@ function NodePanel({
1301
1167
  flex: '0 0 auto',
1302
1168
  overflow: 'auto',
1303
1169
  // THE SIDEBAR IS PART OF THE NODE EDITOR'S AREA, so it wears the
1304
- // space's own `TH_BACK` like the header and the canvas do. It used to
1305
- // paint `--volter-color-surface-panel`, which is the DOCK's grey
1306
- // (#303030 under the Blender look): Blender fills every region of a
1307
- // node editor area with `TH_BACK` and draws panels ON it
1308
- // (`ED_region_panels`), so the grey was this view wearing its
1309
- // container's colour. MEASURED 2026-09-19 (the frame walk, beat 5):
1310
- // at (600,500) — inside the Shader Editor's pane, which a 320px
1311
- // sidebar had squeezed the canvas out of — the read was #303030 where
1312
- // the beat named #1a1a1a. The frame's `volter.nodeEditor.background`
1170
+ // space's own `TH_BACK` like the header and the canvas do, not the
1171
+ // DOCK's grey: Blender fills every region of a node editor area with
1172
+ // `TH_BACK` and draws panels ON it (`ED_region_panels`). The frame's `volter.nodeEditor.background`
1313
1173
  // theme colour carries the same traced value for the workbench's own
1314
1174
  // painting; this is the same number, from the same trace, for the
1315
1175
  // pixels we paint ourselves.
@@ -1322,9 +1182,7 @@ function NodePanel({
1322
1182
  >
1323
1183
  <div style={{ opacity: 0.6, marginBottom: 'var(--volter-space-2)' }}>Node</div>
1324
1184
  {!node ? (
1325
- // Blender's panel `poll` is `context.active_node is not None`
1326
- // (`space_node.py:806-808`): with no active node the tab draws nothing.
1327
- <div style={{ opacity: 0.5 }}>No node looked at.</div>
1185
+ <StateSurface inline title="No node looked at." />
1328
1186
  ) : (
1329
1187
  <>
1330
1188
  <Row label="Name" value={node.name} onRefuse={onRefuse} />
@@ -1391,12 +1249,3 @@ function Row({
1391
1249
  </div>
1392
1250
  );
1393
1251
  }
1394
-
1395
- // THERE IS DELIBERATELY NO `available` GATE. A `workspace.utility` may gate
1396
- // its tab on session state, and the Network utility does because a game may or
1397
- // may not expose a networking adapter. This tab's condition is that the
1398
- // BLENDER BUILD is loaded, and a contribution of this package cannot be
1399
- // registered unless it is — the same reason `model.layout.ts` gates on the
1400
- // project declaring a `model` document and nothing else. With no material the
1401
- // view draws what Blender draws with no tree: the bare background, and the
1402
- // header saying so.