@volter/editor-blender 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1409 -0
- package/README.md +17 -0
- package/contributions/blender-header-menus.tsx +483 -0
- package/contributions/blender-icon-trace.mjs +403 -0
- package/contributions/blender-icons.source.mjs +2925 -0
- package/contributions/blender-node-editor.document.tsx +1402 -0
- package/contributions/blender-node-geometry.ts +1138 -0
- package/contributions/blender-node-panels.source.mjs +485 -0
- package/contributions/blender-outliner-authoring.ts +1729 -0
- package/contributions/blender-outliner-model.ts +389 -0
- package/contributions/blender-palette.source.mjs +319 -0
- package/contributions/blender-properties-model.ts +351 -0
- package/contributions/blender-properties-tab.tsx +100 -0
- package/contributions/blender-properties-view.tsx +1191 -0
- package/contributions/blender-runtime-skin.ts +619 -0
- package/contributions/blender-runtime.document.tsx +232 -0
- package/contributions/blender-timeline-geometry.ts +323 -0
- package/contributions/blender-timeline.document.tsx +1056 -0
- package/contributions/blender-uv-editor.document.tsx +483 -0
- package/contributions/blender-uv-geometry.ts +305 -0
- package/contributions/blender-version.status.tsx +93 -0
- package/contributions/blender.command.ts +102 -0
- package/contributions/blender.icons.json +1247 -0
- package/contributions/blender.icons.traced.json +1561 -0
- package/contributions/blender.keymap.ts +39 -0
- package/contributions/blender.node-panels.json +2436 -0
- package/contributions/blender.palette.json +93 -0
- package/contributions/blender.status.tsx +263 -0
- package/contributions/blender.style.ts +271 -0
- package/contributions/model.layout.ts +53 -0
- package/contributions/models.finder.ts +59 -0
- package/contributions/properties-bone-constraints.inspector.tsx +50 -0
- package/contributions/properties-bone.inspector.tsx +184 -0
- package/contributions/properties-collection.inspector.tsx +96 -0
- package/contributions/properties-constraints.inspector.tsx +69 -0
- package/contributions/properties-data.inspector.tsx +229 -0
- package/contributions/properties-material.inspector.tsx +121 -0
- package/contributions/properties-modifiers.inspector.tsx +74 -0
- package/contributions/properties-object.inspector.tsx +215 -0
- package/contributions/properties-output.inspector.tsx +210 -0
- package/contributions/properties-particles.inspector.tsx +494 -0
- package/contributions/properties-physics.inspector.tsx +614 -0
- package/contributions/properties-render.inspector.tsx +446 -0
- package/contributions/properties-scene.inspector.tsx +174 -0
- package/contributions/properties-texture.inspector.tsx +300 -0
- package/contributions/properties-view-layer.inspector.tsx +145 -0
- package/contributions/properties-world.inspector.tsx +130 -0
- package/contributions/sculpt.layout.ts +25 -0
- package/contributions/shading.layout.ts +99 -0
- package/contributions/texture.layout.ts +16 -0
- package/contributions/uv-editing.layout.ts +93 -0
- package/host/blender-runtime-host.ts +1256 -0
- package/package.json +77 -0
- package/src/layouts.tsx +48 -0
- package/src/looks.ts +14 -0
- package/src/node-view-state.ts +125 -0
- package/src/timeline-view-state.ts +154 -0
- package/src/uv-view-state.ts +125 -0
|
@@ -0,0 +1,1138 @@
|
|
|
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
|
+
*/
|
|
29
|
+
|
|
30
|
+
/* -------------------------------------------------------------------------- */
|
|
31
|
+
/* Geometry — `space_node/node_intern.hh:332-344` */
|
|
32
|
+
/* -------------------------------------------------------------------------- */
|
|
33
|
+
|
|
34
|
+
/** `U.widget_unit` at scale 1 (`wm_window.cc:779`). */
|
|
35
|
+
export const WIDGET_UNIT = 20;
|
|
36
|
+
|
|
37
|
+
/** `U.pixelsize` at scale 1 — the node outline's width (`node_draw.cc:2922`). */
|
|
38
|
+
export const PIXEL_SIZE = 1;
|
|
39
|
+
|
|
40
|
+
/** `#define NODE_DY U.widget_unit` (`node_intern.hh:335`) — the header's
|
|
41
|
+
* height and a socket row's height, the same number. */
|
|
42
|
+
export const NODE_DY = WIDGET_UNIT;
|
|
43
|
+
|
|
44
|
+
/** `#define NODE_DYS (U.widget_unit / 2)` (`node_intern.hh:334`) — how far
|
|
45
|
+
* below a row's top its socket sits, and the body's side inset. */
|
|
46
|
+
export const NODE_DYS = WIDGET_UNIT / 2;
|
|
47
|
+
|
|
48
|
+
/** `#define BASIS_RAD (0.2f * U.widget_unit)` (`node_intern.hh:333`). */
|
|
49
|
+
export const BASIS_RAD = 0.2 * WIDGET_UNIT;
|
|
50
|
+
|
|
51
|
+
/** `#define NODE_SOCKSIZE (0.25f * U.widget_unit)` (`node_intern.hh:340`) —
|
|
52
|
+
* a socket's RADIUS: `node_draw_socket` builds a rect `location ± (5, 5)`
|
|
53
|
+
* (`node_draw.cc:1835`). */
|
|
54
|
+
export const NODE_SOCKSIZE = 0.25 * WIDGET_UNIT;
|
|
55
|
+
|
|
56
|
+
/** `#define NODE_MARGIN_X (1.2f * U.widget_unit)` (`node_intern.hh:339`) —
|
|
57
|
+
* where the header's label starts (`node_draw.cc:3117-3128`). */
|
|
58
|
+
export const NODE_MARGIN_X = 1.2 * WIDGET_UNIT;
|
|
59
|
+
|
|
60
|
+
/** `#define NODE_ITEM_SPACING_Y (0.1f * U.widget_unit)` (`node_intern.hh:336`)
|
|
61
|
+
* — the unit every inter-item margin of the declaration layout is a multiple
|
|
62
|
+
* of (`node_draw.cc:886-1000`). */
|
|
63
|
+
export const NODE_ITEM_SPACING_Y = 0.1 * WIDGET_UNIT;
|
|
64
|
+
|
|
65
|
+
/** `#define NODE_HEADER_ICON_SIZE (0.8f * U.widget_unit)` (`node_draw.cc:2143`). */
|
|
66
|
+
export const NODE_HEADER_ICON_SIZE = 0.8 * WIDGET_UNIT;
|
|
67
|
+
|
|
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
|
+
/** The 0.5 every filled rect is grown by "to prevent transparent gaps with the
|
|
73
|
+
* outline" (`node_draw.cc:2920`), and the radius that goes with it. */
|
|
74
|
+
export const NODE_FILL_PADDING = 0.5;
|
|
75
|
+
export const NODE_FILL_RADIUS = BASIS_RAD + NODE_FILL_PADDING;
|
|
76
|
+
/** The outline sits `U.pixelsize` outside the rect at `BASIS_RAD + 1`
|
|
77
|
+
* (`node_draw.cc:3184-3211`). */
|
|
78
|
+
export const NODE_OUTLINE_RADIUS = BASIS_RAD + PIXEL_SIZE;
|
|
79
|
+
|
|
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
|
+
/** `LINK_WIDTH` (`space_node/drawnode.cc:1959`) — a noodle's stroke at 100 %,
|
|
85
|
+
* clamped never to go below this as the view zooms out
|
|
86
|
+
* (`drawnode.cc:2299-2302`); a FIELD link draws at 0.7 of it. */
|
|
87
|
+
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
|
+
|
|
95
|
+
/** `NODE_TREE_SCALE_SMALL` (`node_draw.cc:1596`) — below `1/aspect > 0.2`,
|
|
96
|
+
* `draw_node_details` is false and Blender draws NO sockets, no reroute
|
|
97
|
+
* labels and no group marks at all (`node_draw.cc:1607-1610`). */
|
|
98
|
+
export const NODE_DETAIL_ZOOM_MIN = 0.2;
|
|
99
|
+
|
|
100
|
+
/** A collapsed node: `dy = NODE_DY * 0.5`, `height = dy * max(in, out, 2) +
|
|
101
|
+
* BASIS_RAD * 2`, and the whole bar is offset `NODE_DY * -0.5` below
|
|
102
|
+
* `location.y` (`node_update_collapsed`, `node_draw.cc:1345-1353`). */
|
|
103
|
+
export const COLLAPSED_SOCKET_PITCH = NODE_DY * 0.5;
|
|
104
|
+
export const COLLAPSED_OFFSET_Y = NODE_DY * -0.5;
|
|
105
|
+
|
|
106
|
+
/** A reroute is `NODE_SOCKSIZE` in every direction — a 10×10 box drawn as one
|
|
107
|
+
* socket (`reroute_node_prepare_for_draw`, `node_draw.cc:3579-3593`). */
|
|
108
|
+
export const REROUTE_RADIUS = NODE_SOCKSIZE;
|
|
109
|
+
|
|
110
|
+
/** A frame's margin is `1.5f * U.widget_unit` (`frame_node_layout`,
|
|
111
|
+
* `node_draw.cc:3472`); there is no `NODE_FRAME_MARGIN` macro in 5.2. The
|
|
112
|
+
* top margin makes room for the label: `max(margin, 1.5 * label_size + 0.2 *
|
|
113
|
+
* margin)`, and the baseline sits `0.5 * margin_top + 0.35 * label_size`
|
|
114
|
+
* below the top (`node_draw.cc:3474-3484`). */
|
|
115
|
+
export const FRAME_MARGIN = 1.5 * WIDGET_UNIT;
|
|
116
|
+
export const FRAME_LABEL_SIZE_DEFAULT = 20;
|
|
117
|
+
|
|
118
|
+
/** The label's point size — `UI_DEFAULT_TEXT_POINTS` (`UI_interface_c.hh:425`),
|
|
119
|
+
* which `style->widget` takes (`interface_style.cc:91`). A node's name is an
|
|
120
|
+
* ordinary `ButtonType::Label`: there is no `node_draw_label` in 5.2. */
|
|
121
|
+
export const UI_TEXT_POINTS = 11;
|
|
122
|
+
|
|
123
|
+
/* -------------------------------------------------------------------------- */
|
|
124
|
+
/* Colours — `release/datafiles/userdef/userdef_default_theme.c:652-698`, */
|
|
125
|
+
/* the `.space_node` block, mapped through `interface/resources.cc` */
|
|
126
|
+
/* -------------------------------------------------------------------------- */
|
|
127
|
+
|
|
128
|
+
/** Every value is the DEFAULT THEME's, by member and line. A member absent
|
|
129
|
+
* from the initializer takes the struct's zero and is called out. */
|
|
130
|
+
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. */
|
|
141
|
+
sliderFill: '#4671b2',
|
|
142
|
+
/** `.back` `0x1a1a1a00` (`:653`). The alpha is 0x00 and IRRELEVANT:
|
|
143
|
+
* `draw_background_color` clears with the RGB and a hard 1.0
|
|
144
|
+
* (`node_draw.cc:4713-4718`), so the canvas is opaque #1a1a1a. */
|
|
145
|
+
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
|
+
/** `.syntaxl` `0x303030ff` (`:673`), which `resources.cc:659-660` returns
|
|
156
|
+
* for `TH_NODE`. EVERY node's BODY is this colour whatever its class
|
|
157
|
+
* (`node_draw.cc:3149`); only the header carries the class hue. */
|
|
158
|
+
body: '#303030',
|
|
159
|
+
/** `.node_outline` `0xffffff26` (`:681`) → `TH_NODE_OUTLINE`
|
|
160
|
+
* (`resources.cc:662-663`): white at alpha 0x26 (`node_draw.cc:3206`). */
|
|
161
|
+
outline: '#ffffff',
|
|
162
|
+
outlineAlpha: 0x26 / 255,
|
|
163
|
+
/** `.select` `0xed5700ff` (`:663`) → `TH_SELECT` (`node_draw.cc:3196`). */
|
|
164
|
+
select: '#ed5700',
|
|
165
|
+
/** `.active` `0xffffffff` (`:664`) → `TH_ACTIVE`, the outline of a node that
|
|
166
|
+
* is selected AND active (`node_draw.cc:3196`). */
|
|
167
|
+
active: '#ffffff',
|
|
168
|
+
/** `.syntaxr` `0x8d8d8dff` (`:680`) → `TH_WIRE_INNER` (`resources.cc:364-365`)
|
|
169
|
+
* — a noodle's own colour when wire colours are off (`drawnode.cc:2312`). */
|
|
170
|
+
wireInner: '#8d8d8d',
|
|
171
|
+
/** `.wire` `0x1a1a1aff` (`:662`) → `TH_WIRE`: a noodle's OUTLINE
|
|
172
|
+
* (`drawnode.cc:2309`) and a socket's outline (`node_draw.cc:1502-1505`). */
|
|
173
|
+
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
|
+
/** `.noodle_curving` `4` (`:670`) → `TH_NODE_CURVING`
|
|
180
|
+
* (`resources.cc:713-714`). NOTE the divergence worth recording: RNA's
|
|
181
|
+
* property default is 5 (`rna_userdef.cc:3658-3663`), but the SHIPPED THEME
|
|
182
|
+
* initialises 4, and the theme is what a stock Blender runs with. */
|
|
183
|
+
noodleCurving: 4,
|
|
184
|
+
/** `.dash_alpha` `0.5f` (`:672`) — a muted/field noodle's dashes. */
|
|
185
|
+
dashAlpha: 0.5,
|
|
186
|
+
/** `.text` `0xe6e6e6ff` (`:655`) — the header label and socket names. */
|
|
187
|
+
text: '#e6e6e6',
|
|
188
|
+
/** `.movie` `0x0f0f0fcc` (`:694`), whose Python name is `frame_node`
|
|
189
|
+
* (`rna_userdef.cc:3640`) → `TH_NODE_FRAME` (`resources.cc:704-705`). */
|
|
190
|
+
frame: '#0f0f0f',
|
|
191
|
+
frameAlpha: 0xcc / 255,
|
|
192
|
+
} as const;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* THE HEADER COLOUR PER NODE CLASS, and it is a READ rather than a table of
|
|
196
|
+
* ours: `Node.color_tag` (`rna_nodetree.cc:9480-9484`) is RNA reporting
|
|
197
|
+
* `bke::node_color_tag(*node)`, which is the very value `node_get_colorid`
|
|
198
|
+
* (`node_draw.cc:1388-1434`) switches on. This map is that switch's targets,
|
|
199
|
+
* resolved through `resources.cc`'s `SPACE_NODE` branch to the theme members
|
|
200
|
+
* at `userdef_default_theme.c`'s stated lines.
|
|
201
|
+
*/
|
|
202
|
+
export const NODE_HEADER_COLOR: Readonly<Record<string, string>> = {
|
|
203
|
+
/** `TH_NODE_INPUT` ← `.syntaxn` (`:676`, `resources.cc:665-666`). */
|
|
204
|
+
INPUT: '#82354c',
|
|
205
|
+
/** `TH_NODE_OUTPUT` ← `.nodeclass_output` (`:682`, `resources.cc:668-669`).
|
|
206
|
+
* Only for the ACTIVE output — see {@link nodeHeaderColor}. */
|
|
207
|
+
OUTPUT: '#3e232a',
|
|
208
|
+
/** `TH_NODE_CONVERTER` ← `.syntaxv` (`:677`, `resources.cc:695-696`). */
|
|
209
|
+
CONVERTER: '#246283',
|
|
210
|
+
/** `TH_NODE_COLOR` ← `.syntaxb` (`:675`, `resources.cc:671-672`). */
|
|
211
|
+
COLOR: '#6e6e23',
|
|
212
|
+
/** `TH_NODE_VECTOR` ← `.nodeclass_vector` (`:684`, `resources.cc:677-678`). */
|
|
213
|
+
VECTOR: '#3c3c83',
|
|
214
|
+
/** `TH_NODE_FILTER` ← `.nodeclass_filter` (`:683`, `resources.cc:674-675`). */
|
|
215
|
+
FILTER: '#412b51',
|
|
216
|
+
/** `TH_NODE_GROUP` ← `.syntaxc` (`:678`, `resources.cc:698-699`). */
|
|
217
|
+
GROUP: '#374725',
|
|
218
|
+
/** `TH_NODE_INTERFACE` ← `.console_output` (`:666`, `resources.cc:701-702`). */
|
|
219
|
+
INTERFACE: '#1d1d1d',
|
|
220
|
+
/** `TH_NODE_MATTE` ← `.syntaxs` (`:674`, `resources.cc:707-708`). */
|
|
221
|
+
MATTE: '#5a3838',
|
|
222
|
+
/** `TH_NODE_DISTORT` ← `.syntaxd` (`:679`, `resources.cc:710-711`). */
|
|
223
|
+
DISTORT: '#3e5a5b',
|
|
224
|
+
/** `TH_NODE_TEXTURE` ← `.nodeclass_texture` (`:685`, `resources.cc:680-681`). */
|
|
225
|
+
TEXTURE: '#79461d',
|
|
226
|
+
/** `TH_NODE_SHADER` ← `.nodeclass_shader` (`:686`, `resources.cc:692-693`). */
|
|
227
|
+
SHADER: '#2b652b',
|
|
228
|
+
/** `TH_NODE_SCRIPT` ← `.nodeclass_script` (`:687`, `resources.cc:683-684`). */
|
|
229
|
+
SCRIPT: '#203c3c',
|
|
230
|
+
/** `TH_NODE_GEOMETRY` ← `.nodeclass_geometry` (`:688`, `resources.cc:686-687`). */
|
|
231
|
+
GEOMETRY: '#1d725e',
|
|
232
|
+
/** `TH_NODE_ATTRIBUTE` ← `.nodeclass_attribute` (`:689`, `resources.cc:689-690`). */
|
|
233
|
+
ATTRIBUTE: '#1d2546',
|
|
234
|
+
/** No class of its own in `node_get_colorid`: falls to `TH_NODE`. */
|
|
235
|
+
NONE: NODE_THEME.body,
|
|
236
|
+
PATTERN: NODE_THEME.body,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* SOCKET COLOUR BY `NodeSocket.type` — `std_node_socket_colors[][4]`
|
|
241
|
+
* (`space_node/drawnode.cc:987-1013`), indexed by the `SOCK_*` enum. The
|
|
242
|
+
* spellings are RNA's own (`NodeSocket.type`); each hex is that row's floats
|
|
243
|
+
* rounded to 8 bits.
|
|
244
|
+
*/
|
|
245
|
+
export const SOCKET_COLOR: Readonly<Record<string, string>> = {
|
|
246
|
+
VALUE: '#a1a1a1', // SOCK_FLOAT, drawnode.cc:988
|
|
247
|
+
VECTOR: '#6363c7', // :989
|
|
248
|
+
RGBA: '#c7c729', // :990
|
|
249
|
+
SHADER: '#63c763', // :991
|
|
250
|
+
BOOLEAN: '#cca6d6', // :992
|
|
251
|
+
INT: '#598c5c', // :994
|
|
252
|
+
STRING: '#70b3ff', // :995
|
|
253
|
+
OBJECT: '#ed9e5c', // :996
|
|
254
|
+
IMAGE: '#633863', // :997
|
|
255
|
+
GEOMETRY: '#00d6a3', // :998
|
|
256
|
+
COLLECTION: '#f5f5f5', // :999
|
|
257
|
+
TEXTURE: '#9e4fa3', // :1000
|
|
258
|
+
MATERIAL: '#eb7582', // :1001
|
|
259
|
+
ROTATION: '#a663c7', // :1002
|
|
260
|
+
MENU: '#666666', // :1003
|
|
261
|
+
MATRIX: '#b83385', // :1004
|
|
262
|
+
BUNDLE: '#4d8080', // :1005
|
|
263
|
+
CLOSURE: '#7d7d3b', // :1006
|
|
264
|
+
FONT: '#635742', // :1007
|
|
265
|
+
SOUND: '#635742', // :1011
|
|
266
|
+
INT_VECTOR: '#5c789c', // :1012
|
|
267
|
+
/** The virtual (reroute-through) socket: `virtual_node_socket_color`
|
|
268
|
+
* `{0.2, 0.2, 0.2, 1.0}` (`drawnode.cc:984`). */
|
|
269
|
+
CUSTOM: '#333333',
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/** A socket type Blender itself has no `draw_color` for falls back to magenta
|
|
273
|
+
* (`node_draw.cc:1514-1521`) — so an unrecognised type is LOUD here too,
|
|
274
|
+
* never quietly grey. */
|
|
275
|
+
export const SOCKET_COLOR_UNKNOWN = '#ff00ff';
|
|
276
|
+
|
|
277
|
+
/** A socket's outline: `TH_ACTIVE` when selected, `{0.5,0.5,0.5}` for a
|
|
278
|
+
* virtual socket, otherwise `TH_WIRE` with alpha forced to 1
|
|
279
|
+
* (`node_socket_outline_color_get`, `node_draw.cc:1485-1506`). */
|
|
280
|
+
export const SOCKET_OUTLINE = NODE_THEME.wire;
|
|
281
|
+
export const SOCKET_OUTLINE_VIRTUAL = '#808080';
|
|
282
|
+
/** `NODE_SOCKET_OUTLINE = U.pixelsize` (`node_draw.cc:1570`). */
|
|
283
|
+
export const SOCKET_OUTLINE_WIDTH = PIXEL_SIZE;
|
|
284
|
+
|
|
285
|
+
/* -------------------------------------------------------------------------- */
|
|
286
|
+
/* The layout */
|
|
287
|
+
/* -------------------------------------------------------------------------- */
|
|
288
|
+
|
|
289
|
+
export interface LaidOutSocket {
|
|
290
|
+
readonly identifier: string;
|
|
291
|
+
readonly name: string;
|
|
292
|
+
readonly type: string;
|
|
293
|
+
readonly shape: string;
|
|
294
|
+
readonly output: boolean;
|
|
295
|
+
readonly linked: boolean;
|
|
296
|
+
readonly multiInput: boolean;
|
|
297
|
+
readonly hideValue: boolean;
|
|
298
|
+
readonly value: number | string | boolean | readonly number[] | null;
|
|
299
|
+
/** See {@link LayoutSocketInput.softMin} — carried through so the inline
|
|
300
|
+
* widget can draw Blender's slider fill. */
|
|
301
|
+
readonly softMin?: number;
|
|
302
|
+
readonly softMax?: number;
|
|
303
|
+
/** Tree space. `x` is the node's own edge — a socket sits ON the edge, not
|
|
304
|
+
* inset (`node_update_basis_socket`, `node_draw.cc:533-541`). */
|
|
305
|
+
readonly x: number;
|
|
306
|
+
readonly y: number;
|
|
307
|
+
/** The top of the row this socket owns, for the inline value widget. */
|
|
308
|
+
readonly rowTop: number;
|
|
309
|
+
/** `SOCK_PANEL_COLLAPSED` (`node_draw.cc:1025`): this socket lives inside a
|
|
310
|
+
* COLLAPSED socket panel, so its MARK is drawn on that panel's header row
|
|
311
|
+
* and it draws no row of its own. Blender puts the mark there so a link
|
|
312
|
+
* into a hidden socket still lands somewhere visible. */
|
|
313
|
+
readonly panelCollapsed: boolean;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface LaidOutNode {
|
|
317
|
+
readonly name: string;
|
|
318
|
+
readonly x: number;
|
|
319
|
+
readonly yTop: number;
|
|
320
|
+
readonly yBottom: number;
|
|
321
|
+
readonly width: number;
|
|
322
|
+
readonly collapsed: boolean;
|
|
323
|
+
readonly sockets: readonly LaidOutSocket[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface LayoutSocketInput {
|
|
327
|
+
readonly identifier: string;
|
|
328
|
+
readonly name: string;
|
|
329
|
+
readonly label: string | null;
|
|
330
|
+
readonly type: string;
|
|
331
|
+
readonly shape: string;
|
|
332
|
+
readonly enabled: boolean;
|
|
333
|
+
readonly hide: boolean;
|
|
334
|
+
readonly hideValue: boolean;
|
|
335
|
+
readonly linked: boolean;
|
|
336
|
+
readonly multiInput: boolean;
|
|
337
|
+
readonly value: number | string | boolean | readonly number[] | null;
|
|
338
|
+
/** The `default_value` property's SOFT range, present only when the socket
|
|
339
|
+
* is a bounded scalar — which is exactly Blender's own test for drawing a
|
|
340
|
+
* NUMBER SLIDER rather than a flat number field. Absent means no fill. */
|
|
341
|
+
readonly softMin?: number;
|
|
342
|
+
readonly softMax?: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interface LayoutNodeInput {
|
|
346
|
+
readonly name: string;
|
|
347
|
+
readonly location: readonly number[];
|
|
348
|
+
readonly width: number;
|
|
349
|
+
readonly collapsed: boolean;
|
|
350
|
+
readonly inputs: readonly LayoutSocketInput[];
|
|
351
|
+
readonly outputs: readonly LayoutSocketInput[];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Blender's own availability test: a socket that is not `enabled`, or is
|
|
355
|
+
* `hide`, occupies no row and draws no mark. */
|
|
356
|
+
export function socketDraws(socket: LayoutSocketInput): boolean {
|
|
357
|
+
return socket.enabled && !socket.hide;
|
|
358
|
+
}
|
|
359
|
+
|
|
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
|
+
*/
|
|
394
|
+
export function layoutNode(node: LayoutNodeInput): LaidOutNode {
|
|
395
|
+
const x = node.location[0] ?? 0;
|
|
396
|
+
const top = node.location[1] ?? 0;
|
|
397
|
+
const width = node.width;
|
|
398
|
+
const outputs = node.outputs.filter(socketDraws);
|
|
399
|
+
const inputs = node.inputs.filter(socketDraws);
|
|
400
|
+
|
|
401
|
+
if (node.collapsed) {
|
|
402
|
+
// `node_update_collapsed`, node_draw.cc:1345-1378.
|
|
403
|
+
const pitch = COLLAPSED_SOCKET_PITCH;
|
|
404
|
+
const height = pitch * Math.max(inputs.length, outputs.length, 2) + BASIS_RAD * 2;
|
|
405
|
+
const yTop = top + height / 2 + COLLAPSED_OFFSET_Y;
|
|
406
|
+
const yBottom = top - height / 2 + COLLAPSED_OFFSET_Y;
|
|
407
|
+
const place = (list: readonly LayoutSocketInput[], output: boolean): LaidOutSocket[] => {
|
|
408
|
+
let y = top + pitch * (list.length - 1) * 0.5 + COLLAPSED_OFFSET_Y;
|
|
409
|
+
return list.map((socket) => {
|
|
410
|
+
const placed = socketAt(socket, output, output ? x + width : x, y, y);
|
|
411
|
+
y -= pitch;
|
|
412
|
+
return placed;
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
return {
|
|
416
|
+
name: node.name,
|
|
417
|
+
x,
|
|
418
|
+
yTop,
|
|
419
|
+
yBottom,
|
|
420
|
+
width,
|
|
421
|
+
collapsed: true,
|
|
422
|
+
sockets: [...place(outputs, true), ...place(inputs, false)],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let dy = top - NODE_DY; // the header — node_draw.cc:1300
|
|
427
|
+
const sockets: LaidOutSocket[] = [];
|
|
428
|
+
const row = (socket: LayoutSocketInput, output: boolean): void => {
|
|
429
|
+
const rowTop = dy;
|
|
430
|
+
// `input.location = (locx, locy - NODE_DYS)` — node_draw.cc:533-541.
|
|
431
|
+
sockets.push(socketAt(socket, output, output ? x + width : x, rowTop - NODE_DYS, rowTop));
|
|
432
|
+
// `buty = min(buty, topy - NODE_DY)` — node_draw.cc:567: a row is never
|
|
433
|
+
// shorter than one unit, and an inline value widget is exactly one
|
|
434
|
+
// `UI_UNIT_Y` (= NODE_DY) tall, so it never grows the row.
|
|
435
|
+
dy = rowTop - NODE_DY;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (outputs.length > 0 || inputs.length > 0) {
|
|
439
|
+
dy -= 2 * NODE_ITEM_SPACING_Y; // top margin before a Socket — node_draw.cc:841-859
|
|
440
|
+
}
|
|
441
|
+
outputs.forEach((socket, index) => {
|
|
442
|
+
if (index > 0) dy -= NODE_ITEM_SPACING_Y; // Socket→Socket — node_draw.cc:886-1000
|
|
443
|
+
row(socket, true);
|
|
444
|
+
});
|
|
445
|
+
if (outputs.length > 0 && inputs.length > 0) dy -= NODE_ITEM_SPACING_Y;
|
|
446
|
+
inputs.forEach((socket, index) => {
|
|
447
|
+
if (index > 0 || outputs.length > 0) {
|
|
448
|
+
if (index > 0) dy -= NODE_ITEM_SPACING_Y;
|
|
449
|
+
}
|
|
450
|
+
row(socket, false);
|
|
451
|
+
});
|
|
452
|
+
if (outputs.length > 0 || inputs.length > 0) {
|
|
453
|
+
dy -= 2 * NODE_ITEM_SPACING_Y; // bottom margin after a Socket — node_draw.cc:863-882
|
|
454
|
+
} else {
|
|
455
|
+
dy -= NODE_DYS; // `get_margin_empty()` — node_draw.cc:835-838
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
name: node.name,
|
|
460
|
+
x,
|
|
461
|
+
yTop: top,
|
|
462
|
+
// `ymin = min_ff(dy, loc.y - 2 * NODE_DY)` — node_draw.cc:1311.
|
|
463
|
+
yBottom: Math.min(dy, top - 2 * NODE_DY),
|
|
464
|
+
width,
|
|
465
|
+
collapsed: false,
|
|
466
|
+
sockets,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function socketAt(
|
|
471
|
+
socket: LayoutSocketInput,
|
|
472
|
+
output: boolean,
|
|
473
|
+
x: number,
|
|
474
|
+
y: number,
|
|
475
|
+
rowTop: number,
|
|
476
|
+
panelCollapsed = false,
|
|
477
|
+
): LaidOutSocket {
|
|
478
|
+
return {
|
|
479
|
+
identifier: socket.identifier,
|
|
480
|
+
name: socket.label ?? socket.name,
|
|
481
|
+
type: socket.type,
|
|
482
|
+
shape: socket.shape,
|
|
483
|
+
output,
|
|
484
|
+
linked: socket.linked,
|
|
485
|
+
multiInput: socket.multiInput,
|
|
486
|
+
hideValue: socket.hideValue,
|
|
487
|
+
value: socket.value,
|
|
488
|
+
...(socket.softMin === undefined ? {} : { softMin: socket.softMin }),
|
|
489
|
+
...(socket.softMax === undefined ? {} : { softMax: socket.softMax }),
|
|
490
|
+
x,
|
|
491
|
+
y,
|
|
492
|
+
rowTop,
|
|
493
|
+
panelCollapsed,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* A NOODLE'S TWO CONTROL POINTS — `calculate_inner_link_bezier_points`
|
|
499
|
+
* (`space_node/drawnode.cc:1747-1773`), verbatim:
|
|
500
|
+
*
|
|
501
|
+
* ```
|
|
502
|
+
* if (curving == 0) { P1 = lerp(P0,P3,1/3); P2 = lerp(P0,P3,2/3); }
|
|
503
|
+
* else {
|
|
504
|
+
* dist_x = |P3.x - P0.x| ; dist_y = |P3.y - P0.y| ; slope = dist_y / dist_x
|
|
505
|
+
* clamp_factor = min(1, slope * (4.5 - 0.25 * curving))
|
|
506
|
+
* handle_offset = curving * 0.1 * dist_x * clamp_factor
|
|
507
|
+
* P1 = (P0.x + handle_offset, P0.y) ; P2 = (P3.x - handle_offset, P3.y)
|
|
508
|
+
* }
|
|
509
|
+
* ```
|
|
510
|
+
* `curving` is the THEME's `TH_NODE_CURVING` (`resources.cc:713-714`), which
|
|
511
|
+
* the shipped theme sets to 4 (`userdef_default_theme.c:670`).
|
|
512
|
+
*/
|
|
513
|
+
export function linkHandles(
|
|
514
|
+
from: readonly [number, number],
|
|
515
|
+
to: readonly [number, number],
|
|
516
|
+
curving: number = NODE_THEME.noodleCurving,
|
|
517
|
+
): [[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
|
+
}
|
|
524
|
+
const distX = Math.abs(to[0] - from[0]);
|
|
525
|
+
const distY = Math.abs(to[1] - from[1]);
|
|
526
|
+
// `slope` divides by dist_x; Blender's own code does the same and relies on
|
|
527
|
+
// the clamp below, which `min(1, Infinity)` answers correctly for a
|
|
528
|
+
// perfectly vertical link.
|
|
529
|
+
const slope = distX === 0 ? Number.POSITIVE_INFINITY : distY / distX;
|
|
530
|
+
const clampFactor = Math.min(1, slope * (4.5 - 0.25 * curving));
|
|
531
|
+
const handle = curving * 0.1 * distX * clampFactor;
|
|
532
|
+
return [
|
|
533
|
+
[from[0] + handle, from[1]],
|
|
534
|
+
[to[0] - handle, to[1]],
|
|
535
|
+
];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* THE HEADER'S COLOUR for one node, following `node_get_colorid`'s two
|
|
540
|
+
* conditions that a colour tag alone does not answer:
|
|
541
|
+
*
|
|
542
|
+
* - an OUTPUT-class node takes the output colour only when it is the ACTIVE
|
|
543
|
+
* output, and otherwise `TH_NODE` (`node_draw.cc:1395-1401`);
|
|
544
|
+
* - a FRAME takes `TH_NODE_FRAME`, the `NODE_CLASS_LAYOUT` branch
|
|
545
|
+
* (`node_draw.cc:1429-1430`); `color_tag` has no member for it.
|
|
546
|
+
*
|
|
547
|
+
* A node with `use_custom_color` keeps the CLASS header and recolours its
|
|
548
|
+
* BODY (`node_draw.cc:2793-2795, 3145-3147`): the custom-colour header is the
|
|
549
|
+
* COLLAPSED path only, which {@link nodeBodyColor} and the view both follow.
|
|
550
|
+
*/
|
|
551
|
+
export function nodeHeaderColor(node: {
|
|
552
|
+
readonly colorTag: string;
|
|
553
|
+
readonly idname: string;
|
|
554
|
+
readonly activeOutput: boolean;
|
|
555
|
+
}): string {
|
|
556
|
+
if (node.idname === 'NodeFrame') return NODE_THEME.frame;
|
|
557
|
+
if (node.colorTag === 'OUTPUT' && !node.activeOutput) return NODE_THEME.body;
|
|
558
|
+
return NODE_HEADER_COLOR[node.colorTag] ?? NODE_THEME.body;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** `TH_NODE`, or the node's own colour when `NODE_CUSTOM_COLOR`
|
|
562
|
+
* (`node_draw.cc:3145-3149`). */
|
|
563
|
+
export function nodeBodyColor(node: {
|
|
564
|
+
readonly useCustomColor: boolean;
|
|
565
|
+
readonly color: readonly number[];
|
|
566
|
+
}): string {
|
|
567
|
+
if (!node.useCustomColor) return NODE_THEME.body;
|
|
568
|
+
return rgbFloatsToHex(node.color);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function rgbFloatsToHex(channels: readonly number[]): string {
|
|
572
|
+
const byte = (value: number): string =>
|
|
573
|
+
Math.round(Math.min(1, Math.max(0, value)) * 255)
|
|
574
|
+
.toString(16)
|
|
575
|
+
.padStart(2, '0');
|
|
576
|
+
return `#${byte(channels[0] ?? 0)}${byte(channels[1] ?? 0)}${byte(channels[2] ?? 0)}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* A MUTED NODE. `get_color_blend_alpha_4fv(color, TH_BACK, factor, -0.2)`:
|
|
581
|
+
* the header blends 0.6 toward the background and the body 0.8, and both lose
|
|
582
|
+
* 0.2 of alpha (`node_draw.cc:2806-2814` and `:3159-3163`). Two different
|
|
583
|
+
* factors on one node, which is why this takes one.
|
|
584
|
+
*/
|
|
585
|
+
export function mutedToward(color: string, factor: number): string {
|
|
586
|
+
const back = hexToRgb(NODE_THEME.background);
|
|
587
|
+
const own = hexToRgb(color);
|
|
588
|
+
return rgbFloatsToHex([
|
|
589
|
+
own[0] * (1 - factor) + back[0] * factor,
|
|
590
|
+
own[1] * (1 - factor) + back[1] * factor,
|
|
591
|
+
own[2] * (1 - factor) + back[2] * factor,
|
|
592
|
+
]);
|
|
593
|
+
}
|
|
594
|
+
export const MUTED_HEADER_FACTOR = 0.6;
|
|
595
|
+
export const MUTED_BODY_FACTOR = 0.8;
|
|
596
|
+
export const MUTED_ALPHA_DROP = 0.2;
|
|
597
|
+
|
|
598
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
599
|
+
const value = Number.parseInt(hex.slice(1), 16);
|
|
600
|
+
return [((value >> 16) & 255) / 255, ((value >> 8) & 255) / 255, (value & 255) / 255];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** The colour a socket's mark is filled with. */
|
|
604
|
+
export function socketColor(type: string): string {
|
|
605
|
+
return SOCKET_COLOR[type] ?? SOCKET_COLOR_UNKNOWN;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* -------------------------------------------------------------------------- */
|
|
609
|
+
/* SOCKET PANELS — `node_update_basis_from_declaration`'s other half */
|
|
610
|
+
/* -------------------------------------------------------------------------- */
|
|
611
|
+
|
|
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
|
+
*/
|
|
619
|
+
export const PANEL_HEADER_HEIGHT = NODE_DYS;
|
|
620
|
+
|
|
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
|
+
/** `header_but_margin = NODE_MARGIN_X / 3` — where the triangle starts, and
|
|
629
|
+
* the inset the whole header button takes (`node_draw.cc:2031-2038`). */
|
|
630
|
+
export const PANEL_HEADER_MARGIN_X = NODE_MARGIN_X / 3;
|
|
631
|
+
/** `but_padding = NODE_MARGIN_X / 4` — triangle to label (`node_draw.cc:2034`). */
|
|
632
|
+
export const PANEL_HEADER_BUT_PADDING = NODE_MARGIN_X / 4;
|
|
633
|
+
/** `UI_UNIT_X` (`UI_interface_c.hh:2346`) — the width a panel TOGGLE checkbox
|
|
634
|
+
* takes before the label, when the panel declares one
|
|
635
|
+
* (`node_draw.cc:2059-2078`). */
|
|
636
|
+
export const UI_UNIT_X = WIDGET_UNIT;
|
|
637
|
+
|
|
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
|
+
*/
|
|
666
|
+
export const PANEL_TRIANGLE = {
|
|
667
|
+
/** Blender's small-icon cell — the space the numbers below are on. */
|
|
668
|
+
cell: 8,
|
|
669
|
+
/** The chevron's reach along its axis, and across it. */
|
|
670
|
+
reach: 4,
|
|
671
|
+
spread: 3.64648,
|
|
672
|
+
/** `2 x .50005`, the round cap's diameter. */
|
|
673
|
+
stroke: 1,
|
|
674
|
+
} as const;
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* A PANEL'S CONTENT BACKGROUND — `TH_PANEL_SUB_BACK`
|
|
678
|
+
* (`resources.cc:298-300` ← `.tui.panel_sub_back` `0x0000001f`,
|
|
679
|
+
* `userdef_default_theme.c:283`), with its alpha multiplied by **1.5** to
|
|
680
|
+
* "increase contrast in nodes a bit" (`node_draw.cc:1918-1920`). The final
|
|
681
|
+
* panel — the one whose content runs to the node's bottom edge — is filled
|
|
682
|
+
* `depth + 1` TIMES over (`node_draw.cc:1951-1956`), so a nested final panel
|
|
683
|
+
* is visibly darker; that repetition is alpha compositing and is drawn as
|
|
684
|
+
* such rather than pre-multiplied into a second constant.
|
|
685
|
+
*/
|
|
686
|
+
export const PANEL_SUB_BACK = '#000000';
|
|
687
|
+
export const PANEL_SUB_BACK_ALPHA = (0x1f / 255) * 1.5;
|
|
688
|
+
|
|
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
|
+
*/
|
|
695
|
+
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
|
+
}
|
|
704
|
+
| {
|
|
705
|
+
readonly kind: 'panel';
|
|
706
|
+
readonly name: string;
|
|
707
|
+
readonly defaultClosed?: boolean;
|
|
708
|
+
readonly items: readonly NodeDeclItem[];
|
|
709
|
+
}
|
|
710
|
+
| { readonly kind: 'layout' }
|
|
711
|
+
| { readonly kind: 'separator' };
|
|
712
|
+
|
|
713
|
+
export interface NodeDeclaration {
|
|
714
|
+
readonly customSocketOrder: boolean;
|
|
715
|
+
readonly conditional?: boolean;
|
|
716
|
+
readonly items: readonly NodeDeclItem[];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** A socket's identifier defaults to its name
|
|
720
|
+
* (`DeclarationListBuilder::add_socket`), which is what the traced table
|
|
721
|
+
* omits when the two agree. */
|
|
722
|
+
const declIdentifier = (item: { readonly name: string; readonly identifier?: string }): string =>
|
|
723
|
+
item.identifier ?? item.name;
|
|
724
|
+
|
|
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
|
+
*/
|
|
742
|
+
export function declarationAgrees(
|
|
743
|
+
declaration: NodeDeclaration,
|
|
744
|
+
inputs: readonly { readonly identifier: string }[],
|
|
745
|
+
outputs: readonly { readonly identifier: string }[],
|
|
746
|
+
): { readonly ok: true } | { readonly ok: false; readonly why: string } {
|
|
747
|
+
if (!declaration.customSocketOrder) {
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
why: 'its declaration does not set use_custom_socket_order, so Blender lays it out the legacy way',
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const declaredIn: string[] = [];
|
|
754
|
+
const declaredOut: string[] = [];
|
|
755
|
+
const collect = (items: readonly NodeDeclItem[]): void => {
|
|
756
|
+
for (const item of items) {
|
|
757
|
+
if (item.kind === 'input') declaredIn.push(declIdentifier(item));
|
|
758
|
+
else if (item.kind === 'output') declaredOut.push(declIdentifier(item));
|
|
759
|
+
else if (item.kind === 'panel') collect(item.items);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
collect(declaration.items);
|
|
763
|
+
const compare = (
|
|
764
|
+
declared: readonly string[],
|
|
765
|
+
live: readonly { readonly identifier: string }[],
|
|
766
|
+
side: string,
|
|
767
|
+
): string | null => {
|
|
768
|
+
if (declared.length !== live.length) {
|
|
769
|
+
return `the declaration names ${declared.length} ${side}${declared.length === 1 ? '' : 's'} and the engine reports ${live.length}`;
|
|
770
|
+
}
|
|
771
|
+
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`;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
};
|
|
778
|
+
const why = compare(declaredOut, outputs, 'output') ?? compare(declaredIn, inputs, 'input');
|
|
779
|
+
return why === null ? { ok: true } : { ok: false, why };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** A panel as the view draws it: the band, the label, and the extent of the
|
|
783
|
+
* content beneath it while it is open. */
|
|
784
|
+
export interface LaidOutPanel {
|
|
785
|
+
readonly name: string;
|
|
786
|
+
readonly depth: number;
|
|
787
|
+
readonly collapsed: boolean;
|
|
788
|
+
/** `panel_runtime.header_center_y` (`node_draw.cc:1193`). */
|
|
789
|
+
readonly centerY: number;
|
|
790
|
+
/** `content_extent` (`node_draw.cc:1204-1210`) — absent while the panel is
|
|
791
|
+
* collapsed, because a collapsed panel appends no `PanelContentBegin`. */
|
|
792
|
+
readonly contentTop: number | null;
|
|
793
|
+
readonly contentBottom: number | null;
|
|
794
|
+
/** `content_extent->fill_node_end` (`tag_final_panel`,
|
|
795
|
+
* `node_draw.cc:1063-1082`): this panel's fill runs to the node's bottom
|
|
796
|
+
* edge, and is drawn `depth + 1` times. */
|
|
797
|
+
readonly fillsNodeEnd: boolean;
|
|
798
|
+
/** The panel's own header checkbox, when it declares one
|
|
799
|
+
* (`PanelDeclaration::panel_input_decl()`). */
|
|
800
|
+
readonly toggle: LaidOutSocket | null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** What a flattened item is, for the margin table. Named exactly as
|
|
804
|
+
* `flat_item::Type` names them (`node_draw.cc:798-831`). */
|
|
805
|
+
type FlatType =
|
|
806
|
+
| 'Socket'
|
|
807
|
+
| 'Layout'
|
|
808
|
+
| 'Separator'
|
|
809
|
+
| 'PanelHeader'
|
|
810
|
+
| 'PanelContentBegin'
|
|
811
|
+
| 'PanelContentEnd';
|
|
812
|
+
|
|
813
|
+
/** `get_margin_from_top` (`node_draw.cc:838-857`), in units of
|
|
814
|
+
* `NODE_ITEM_SPACING_Y`. */
|
|
815
|
+
const MARGIN_FROM_TOP: Partial<Record<FlatType, number>> = {
|
|
816
|
+
Socket: 2,
|
|
817
|
+
Separator: 0.5,
|
|
818
|
+
Layout: 3,
|
|
819
|
+
PanelHeader: 4,
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
/** `get_margin_to_bottom` (`node_draw.cc:860-882`). */
|
|
823
|
+
const MARGIN_TO_BOTTOM: Partial<Record<FlatType, number>> = {
|
|
824
|
+
Socket: 2,
|
|
825
|
+
Separator: 1,
|
|
826
|
+
Layout: 5,
|
|
827
|
+
PanelHeader: 4,
|
|
828
|
+
PanelContentEnd: 1,
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* `get_margin_between_elements` (`node_draw.cc:885-1000`) — Blender's own
|
|
833
|
+
* "handle all cases explicitly" table, transcribed whole, in units of
|
|
834
|
+
* `NODE_ITEM_SPACING_Y`. A pair the source marks `BLI_assert_unreachable` is
|
|
835
|
+
* absent here and answers 0, which is what that branch returns.
|
|
836
|
+
*/
|
|
837
|
+
const MARGIN_BETWEEN: Partial<Record<FlatType, Partial<Record<FlatType, number>>>> = {
|
|
838
|
+
Socket: { Socket: 1, Separator: 0, Layout: 2, PanelHeader: 3, PanelContentEnd: 2 },
|
|
839
|
+
Layout: { Socket: 2, Separator: 0, Layout: 1, PanelHeader: 3, PanelContentEnd: 2 },
|
|
840
|
+
Separator: { Socket: 2, Separator: 1, Layout: 1, PanelHeader: 1, PanelContentEnd: 1 },
|
|
841
|
+
PanelHeader: {
|
|
842
|
+
Socket: 4,
|
|
843
|
+
Separator: 3,
|
|
844
|
+
Layout: 3,
|
|
845
|
+
PanelHeader: 5,
|
|
846
|
+
PanelContentBegin: 3,
|
|
847
|
+
PanelContentEnd: 3,
|
|
848
|
+
},
|
|
849
|
+
PanelContentBegin: { Socket: 2, Separator: 1, Layout: 2, PanelHeader: 3, PanelContentEnd: 1 },
|
|
850
|
+
PanelContentEnd: { Socket: 1, Separator: 1, Layout: 1, PanelHeader: 3, PanelContentEnd: 0 },
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
interface FlatItem {
|
|
854
|
+
readonly type: FlatType;
|
|
855
|
+
readonly input?: LayoutSocketInput | undefined;
|
|
856
|
+
readonly output?: LayoutSocketInput | undefined;
|
|
857
|
+
readonly panel?: PanelState | undefined;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
interface PanelState {
|
|
861
|
+
readonly name: string;
|
|
862
|
+
readonly depth: number;
|
|
863
|
+
readonly collapsed: boolean;
|
|
864
|
+
readonly toggle: LayoutSocketInput | null;
|
|
865
|
+
/** Every socket the panel owns, at any depth — what
|
|
866
|
+
* `mark_sockets_collapsed_recursive` (`node_draw.cc:1007-1032`) walks to
|
|
867
|
+
* put a collapsed panel's sockets on its own header row. */
|
|
868
|
+
readonly owned: LayoutSocketInput[];
|
|
869
|
+
centerY: number;
|
|
870
|
+
contentTop: number | null;
|
|
871
|
+
contentBottom: number | null;
|
|
872
|
+
fillsNodeEnd: boolean;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* `make_flat_node_items` (`node_draw.cc:797-831`) over a TRACED declaration
|
|
877
|
+
* and the engine's LIVE sockets.
|
|
878
|
+
*
|
|
879
|
+
* `determine_visible_panels` (`node_draw.cc:691-703`) is folded in as the
|
|
880
|
+
* `owned.some(socketDraws)` test: a panel stands only when it contains a
|
|
881
|
+
* socket that is available and not hidden, and a sub-panel of a COLLAPSED
|
|
882
|
+
* panel never stands at all because `flatten` does not descend into one.
|
|
883
|
+
*/
|
|
884
|
+
function flatten(
|
|
885
|
+
items: readonly NodeDeclItem[],
|
|
886
|
+
sockets: Map<string, LayoutSocketInput>,
|
|
887
|
+
collapsedOf: (defaultClosed: boolean) => boolean,
|
|
888
|
+
showOptions: boolean,
|
|
889
|
+
depth: number,
|
|
890
|
+
out: FlatItem[],
|
|
891
|
+
panels: PanelState[],
|
|
892
|
+
): void {
|
|
893
|
+
let previousVisible = false;
|
|
894
|
+
for (const item of items) {
|
|
895
|
+
if (item.kind === 'input' || item.kind === 'output') {
|
|
896
|
+
const socket = sockets.get(`${item.kind}:${declIdentifier(item)}`);
|
|
897
|
+
if (!socket || !socketDraws(socket)) {
|
|
898
|
+
previousVisible = false;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
// `add_flat_items_for_socket`: an ALIGNED socket joins the previous
|
|
902
|
+
// 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') {
|
|
905
|
+
out[out.length - 1] = {
|
|
906
|
+
type: 'Socket',
|
|
907
|
+
input: item.kind === 'input' ? socket : last.input,
|
|
908
|
+
output: item.kind === 'output' ? socket : last.output,
|
|
909
|
+
};
|
|
910
|
+
previousVisible = true;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
out.push({
|
|
914
|
+
type: 'Socket',
|
|
915
|
+
input: item.kind === 'input' ? socket : undefined,
|
|
916
|
+
output: item.kind === 'output' ? socket : undefined,
|
|
917
|
+
});
|
|
918
|
+
previousVisible = true;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
previousVisible = false;
|
|
922
|
+
if (item.kind === 'separator') {
|
|
923
|
+
out.push({ type: 'Separator' });
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (item.kind === 'layout') {
|
|
927
|
+
// `add_flat_items_for_layout` returns early unless `NODE_OPTIONS` is set
|
|
928
|
+
// (`node_draw.cc:739-746`), which is `Node.show_options`.
|
|
929
|
+
if (showOptions) out.push({ type: 'Layout' });
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
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
|
+
// A PANEL. `panel_input_decl()` (`node_declaration.cc:528-542`): the
|
|
939
|
+
// panel's FIRST item, when it is a `panel_toggle` boolean input, is drawn
|
|
940
|
+
// in the header and takes no row of its own.
|
|
941
|
+
const first = item.items[0];
|
|
942
|
+
const toggleDecl = first && first.kind === 'input' && first.panelToggle === true ? first : null;
|
|
943
|
+
const toggle = toggleDecl ? (sockets.get(`input:${declIdentifier(toggleDecl)}`) ?? null) : null;
|
|
944
|
+
const owned: LayoutSocketInput[] = [];
|
|
945
|
+
const gather = (list: readonly NodeDeclItem[]): void => {
|
|
946
|
+
for (const child of list) {
|
|
947
|
+
if (child.kind === 'input' || child.kind === 'output') {
|
|
948
|
+
const socket = sockets.get(`${child.kind}:${declIdentifier(child)}`);
|
|
949
|
+
if (socket) owned.push(socket);
|
|
950
|
+
} else if (child.kind === 'panel') gather(child.items);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
gather(item.items);
|
|
954
|
+
// `determine_potentially_visible_panels` — no available socket, no panel.
|
|
955
|
+
if (!owned.some(socketDraws)) continue;
|
|
956
|
+
const collapsed = collapsedOf(item.defaultClosed === true);
|
|
957
|
+
const panel: PanelState = {
|
|
958
|
+
name: item.name,
|
|
959
|
+
depth,
|
|
960
|
+
collapsed,
|
|
961
|
+
toggle,
|
|
962
|
+
owned,
|
|
963
|
+
centerY: 0,
|
|
964
|
+
contentTop: null,
|
|
965
|
+
contentBottom: null,
|
|
966
|
+
fillsNodeEnd: false,
|
|
967
|
+
};
|
|
968
|
+
panels.push(panel);
|
|
969
|
+
out.push({ type: 'PanelHeader', panel });
|
|
970
|
+
if (collapsed) continue;
|
|
971
|
+
out.push({ type: 'PanelContentBegin', panel });
|
|
972
|
+
flatten(
|
|
973
|
+
item.items.filter((child) => child !== toggleDecl),
|
|
974
|
+
sockets,
|
|
975
|
+
collapsedOf,
|
|
976
|
+
showOptions,
|
|
977
|
+
depth + 1,
|
|
978
|
+
out,
|
|
979
|
+
panels,
|
|
980
|
+
);
|
|
981
|
+
out.push({ type: 'PanelContentEnd', panel });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export interface PanelLayoutInput extends LayoutNodeInput {
|
|
986
|
+
readonly declaration: NodeDeclaration;
|
|
987
|
+
/** `Node.panel_states`, in declaration order — `is_collapsed` per panel.
|
|
988
|
+
* Blender indexes `panel_states_array` by the declaration's panel INDEX
|
|
989
|
+
* (`node_draw.cc:1037`) and RNA's collection IS that array, so position is
|
|
990
|
+
* the join and no uid lookup is needed. */
|
|
991
|
+
readonly panelStates: readonly { readonly collapsed: boolean }[];
|
|
992
|
+
/** `Node.show_options` → `NODE_OPTIONS`, which gates a Layout item. */
|
|
993
|
+
readonly showOptions: boolean;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export interface LaidOutPanelledNode extends LaidOutNode {
|
|
997
|
+
readonly panels: readonly LaidOutPanel[];
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* ONE NODE LAID OUT **WITH ITS PANELS** —
|
|
1002
|
+
* `node_update_basis_from_declaration` (`node_draw.cc:1085-1218`) run over
|
|
1003
|
+
* the traced declaration and the engine's live sockets.
|
|
1004
|
+
*
|
|
1005
|
+
* The caller has already satisfied {@link declarationAgrees}; this does not
|
|
1006
|
+
* re-check, because a layout that silently half-applies a stale declaration
|
|
1007
|
+
* is exactly the failure that check exists to make loud.
|
|
1008
|
+
*
|
|
1009
|
+
* THE ONE HEIGHT THIS CANNOT READ is a `LayoutDeclaration`'s: its height is
|
|
1010
|
+
* whatever its C++ lambda's `block_layout_resolve` answers, and RNA exposes
|
|
1011
|
+
* neither the lambda nor its result. Blender's own node layouts here are a
|
|
1012
|
+
* single `layout.prop()` row, so ONE `NODE_DY` is reserved — stated rather
|
|
1013
|
+
* than assumed, and the divergence is bounded by it: a two-row layout leaves
|
|
1014
|
+
* the node 20 short. {@link layoutHasUnreadableRow} is what the view says so
|
|
1015
|
+
* with.
|
|
1016
|
+
*/
|
|
1017
|
+
export function layoutNodeWithPanels(node: PanelLayoutInput): LaidOutPanelledNode {
|
|
1018
|
+
const x = node.location[0] ?? 0;
|
|
1019
|
+
const top = node.location[1] ?? 0;
|
|
1020
|
+
const width = node.width;
|
|
1021
|
+
const sockets = new Map<string, LayoutSocketInput>();
|
|
1022
|
+
for (const socket of node.inputs) sockets.set(`input:${socket.identifier}`, socket);
|
|
1023
|
+
for (const socket of node.outputs) sockets.set(`output:${socket.identifier}`, socket);
|
|
1024
|
+
|
|
1025
|
+
let panelIndex = 0;
|
|
1026
|
+
const states = node.panelStates;
|
|
1027
|
+
const items: FlatItem[] = [];
|
|
1028
|
+
const panels: PanelState[] = [];
|
|
1029
|
+
flatten(
|
|
1030
|
+
node.declaration.items,
|
|
1031
|
+
sockets,
|
|
1032
|
+
(defaultClosed) => states[panelIndex++]?.collapsed ?? defaultClosed,
|
|
1033
|
+
node.showOptions,
|
|
1034
|
+
0,
|
|
1035
|
+
items,
|
|
1036
|
+
panels,
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
const laid: LaidOutSocket[] = [];
|
|
1040
|
+
let dy = top - NODE_DY; // the header — node_draw.cc:1300
|
|
1041
|
+
|
|
1042
|
+
if (items.length === 0) {
|
|
1043
|
+
dy -= NODE_DYS; // `get_margin_empty()` — node_draw.cc:833-836
|
|
1044
|
+
}
|
|
1045
|
+
for (let index = 0; index < items.length; index++) {
|
|
1046
|
+
const item = items[index];
|
|
1047
|
+
if (!item) continue;
|
|
1048
|
+
const previous = items[index - 1];
|
|
1049
|
+
const margin =
|
|
1050
|
+
index === 0
|
|
1051
|
+
? (MARGIN_FROM_TOP[item.type] ?? 0)
|
|
1052
|
+
: (MARGIN_BETWEEN[previous?.type ?? 'Socket']?.[item.type] ?? 0);
|
|
1053
|
+
dy -= margin * NODE_ITEM_SPACING_Y;
|
|
1054
|
+
|
|
1055
|
+
if (item.type === 'Socket') {
|
|
1056
|
+
const rowTop = dy;
|
|
1057
|
+
if (item.output) laid.push(socketAt(item.output, true, x + width, rowTop - NODE_DYS, rowTop));
|
|
1058
|
+
if (item.input) laid.push(socketAt(item.input, false, x, rowTop - NODE_DYS, rowTop));
|
|
1059
|
+
dy = rowTop - NODE_DY; // `buty = min(buty, topy - NODE_DY)` — node_draw.cc:567
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (item.type === 'Layout' || item.type === 'Separator') {
|
|
1063
|
+
// A separator is `layout.separator(1.0, Line)` in a `NODE_DY`-tall block
|
|
1064
|
+
// (`node_draw.cc:1176-1187`); the Layout's reservation is the header's
|
|
1065
|
+
// stated approximation.
|
|
1066
|
+
dy -= NODE_DY;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (item.type === 'PanelHeader' && item.panel) {
|
|
1070
|
+
dy -= PANEL_HEADER_HEIGHT / 2;
|
|
1071
|
+
item.panel.centerY = dy;
|
|
1072
|
+
dy -= PANEL_HEADER_HEIGHT / 2;
|
|
1073
|
+
if (item.panel.toggle) {
|
|
1074
|
+
laid.push(socketAt(item.panel.toggle, false, x, item.panel.centerY, item.panel.centerY));
|
|
1075
|
+
}
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (item.type === 'PanelContentBegin' && item.panel) item.panel.contentTop = dy;
|
|
1079
|
+
if (item.type === 'PanelContentEnd' && item.panel) item.panel.contentBottom = dy;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const last = items[items.length - 1];
|
|
1083
|
+
if (last) dy -= (MARGIN_TO_BOTTOM[last.type] ?? 0) * NODE_ITEM_SPACING_Y;
|
|
1084
|
+
|
|
1085
|
+
// `update_collapsed_sockets` (`node_draw.cc:1050-1060`): every socket inside
|
|
1086
|
+
// a COLLAPSED panel is placed on that panel's HEADER row at the node's own
|
|
1087
|
+
// edge and marked `SOCK_PANEL_COLLAPSED`, which is how a link into a hidden
|
|
1088
|
+
// socket still lands somewhere visible.
|
|
1089
|
+
for (const panel of panels) {
|
|
1090
|
+
if (!panel.collapsed) continue;
|
|
1091
|
+
for (const socket of panel.owned) {
|
|
1092
|
+
if (!socketDraws(socket)) continue;
|
|
1093
|
+
const output = node.outputs.includes(socket);
|
|
1094
|
+
laid.push(
|
|
1095
|
+
socketAt(socket, output, output ? x + width : x, panel.centerY, panel.centerY, true),
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
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.
|
|
1102
|
+
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;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return {
|
|
1109
|
+
name: node.name,
|
|
1110
|
+
x,
|
|
1111
|
+
yTop: top,
|
|
1112
|
+
yBottom: Math.min(dy, top - 2 * NODE_DY),
|
|
1113
|
+
width,
|
|
1114
|
+
collapsed: false,
|
|
1115
|
+
sockets: laid,
|
|
1116
|
+
panels: panels.map((panel) => ({
|
|
1117
|
+
name: panel.name,
|
|
1118
|
+
depth: panel.depth,
|
|
1119
|
+
collapsed: panel.collapsed,
|
|
1120
|
+
centerY: panel.centerY,
|
|
1121
|
+
contentTop: panel.contentTop,
|
|
1122
|
+
contentBottom: panel.contentBottom,
|
|
1123
|
+
fillsNodeEnd: panel.fillsNodeEnd,
|
|
1124
|
+
toggle: panel.toggle ? socketAt(panel.toggle, false, x, panel.centerY, panel.centerY) : null,
|
|
1125
|
+
})),
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/** Does this declaration contain a `LayoutDeclaration`, whose drawn height
|
|
1130
|
+
* cannot be read? The view names the node in its status line when it does,
|
|
1131
|
+
* rather than let a bounded approximation pass as a measurement. */
|
|
1132
|
+
export function layoutHasUnreadableRow(declaration: NodeDeclaration): boolean {
|
|
1133
|
+
const walk = (items: readonly NodeDeclItem[]): boolean =>
|
|
1134
|
+
items.some((item) =>
|
|
1135
|
+
item.kind === 'layout' ? true : item.kind === 'panel' ? walk(item.items) : false,
|
|
1136
|
+
);
|
|
1137
|
+
return walk(declaration.items);
|
|
1138
|
+
}
|