@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,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRACING A NODE'S SOCKET PANELS FROM BLENDER'S OWN DECLARATIONS — the table
|
|
3
|
+
* that closes I5's one named gap (WORK.md §Blender in the tab is Blender,
|
|
4
|
+
* "Inspection parity", I5; RULED 2026-09-19: "socket panels are traced from
|
|
5
|
+
* Blender's source, like the icons and the palette").
|
|
6
|
+
*
|
|
7
|
+
* ## Why a trace and not a read
|
|
8
|
+
*
|
|
9
|
+
* `Node.panel_states` is RNA's whole answer about a node's socket panels: a
|
|
10
|
+
* `persistent_uid` and `is_collapsed`, and nothing else
|
|
11
|
+
* (`rna_nodetree.cc:9289-9299`, `:9430-9434`). Which SOCKETS belong to a
|
|
12
|
+
* panel, what a panel is CALLED, and where a panel sits among the node's
|
|
13
|
+
* items all live in the node's C++ declaration — the `root_items` list that
|
|
14
|
+
* `node_update_basis_from_declaration` (`node_draw.cc:1086-1218`) walks — and
|
|
15
|
+
* bpy exposes no part of it. Measured on the engine: a
|
|
16
|
+
* `ShaderNodeBsdfPrincipled` answers 8 panel states and not one of its
|
|
17
|
+
* `NodeSocket`s carries a panel field.
|
|
18
|
+
*
|
|
19
|
+
* So the membership is READ FROM THE SOURCE, exactly as the icons are traced
|
|
20
|
+
* out of `release/datafiles/icons_svg/*.svg` (`blender-icon-trace.mjs`) and
|
|
21
|
+
* the palette out of `userdef_default_theme.c` (`blender-palette.source.mjs`).
|
|
22
|
+
* The reference is Blender's SOURCE as well as its frames
|
|
23
|
+
* (ARCHITECTURE-CORE §Blender north star), and a declaration is source.
|
|
24
|
+
*
|
|
25
|
+
* ## What is traced, and what deliberately is not
|
|
26
|
+
*
|
|
27
|
+
* ONLY node types that DECLARE A PANEL. A node with no panel is already laid
|
|
28
|
+
* out correctly by the flat path — its flattened item list is its socket list
|
|
29
|
+
* — so a table row for it would carry no information and would have to be
|
|
30
|
+
* kept in step with 558 files instead of 36. Measured at Blender 5.2.0
|
|
31
|
+
* (`fbe6228777e7`): 558 `node_*.cc` files, **36** of which call `add_panel`
|
|
32
|
+
* in a declaration (17 composite, 11 geometry, 5 shader, 3 function).
|
|
33
|
+
*
|
|
34
|
+
* TWO `add_panel` CALL SITES ARE EXCLUDED AND THE REASON IS STATED.
|
|
35
|
+
* `intern/node_declaration.cc:474` is the builder method itself, not a
|
|
36
|
+
* declaration. `intern/node_common.cc:485` is the NODE GROUP's declare, which
|
|
37
|
+
* builds its panels in a LOOP over `ntree->tree_interface` — a group's panels
|
|
38
|
+
* are runtime data, not a static declaration, and RNA *can* answer them
|
|
39
|
+
* (`NodeTree.interface`), so tracing them would be tracing the wrong thing.
|
|
40
|
+
* A group node therefore has no row here and keeps the flat fallback.
|
|
41
|
+
*
|
|
42
|
+
* ## The grammar this reads
|
|
43
|
+
*
|
|
44
|
+
* A declaration body is a straight-line sequence of builder calls. Measured
|
|
45
|
+
* across the 36 files, a panel is introduced in exactly two spellings —
|
|
46
|
+
* `PanelDeclarationBuilder &<var> = <recv>.add_panel("Name")` (49 sites) and
|
|
47
|
+
* `auto &<var> = <recv>.add_panel("Name")` (17 sites) — and everything else
|
|
48
|
+
* is `<recv>.add_input<…>("Name"[, "Identifier"])`,
|
|
49
|
+
* `<recv>.add_output<…>(…)`, `<recv>.add_layout(…)`,
|
|
50
|
+
* `<recv>.add_default_layout()` and `<recv>.add_separator()`, where `<recv>`
|
|
51
|
+
* is either the root builder's own parameter or a bound panel variable. That
|
|
52
|
+
* is the whole grammar, and this file parses that and nothing more.
|
|
53
|
+
*
|
|
54
|
+
* A body with CONTROL FLOW is parsed straight through — both arms of an
|
|
55
|
+
* `if`/`else` contribute items, in source order — and the entry is marked
|
|
56
|
+
* `conditional`. That is deliberately a LOUD approximation rather than a
|
|
57
|
+
* clever one: the consumer checks the traced item list against the engine's
|
|
58
|
+
* LIVE socket list before it uses it, and a node whose real declaration took
|
|
59
|
+
* the other arm simply disagrees and falls back to flat with its frame
|
|
60
|
+
* warning. A static parser cannot evaluate `b.tree_or_null()`, and pretending
|
|
61
|
+
* otherwise is how a drawing quietly stops being a measurement.
|
|
62
|
+
*
|
|
63
|
+
* ## Running it
|
|
64
|
+
*
|
|
65
|
+
* ```
|
|
66
|
+
* node packages/blender/contributions/blender-node-panels.source.mjs \
|
|
67
|
+
* [--checkout <path>] [--check]
|
|
68
|
+
* ```
|
|
69
|
+
* writes (or, with `--check`, verifies) `blender.node-panels.json` beside
|
|
70
|
+
* this file. The checkout defaults to `~/volter/blender-src`, the sparse
|
|
71
|
+
* checkout pinned at the engine's own commit; `source/blender/nodes` is one
|
|
72
|
+
* of the paths it must carry (see the checkout recipe's file map).
|
|
73
|
+
*
|
|
74
|
+
* ## Licence
|
|
75
|
+
*
|
|
76
|
+
* What lands in the JSON is a table of PANEL NAMES and SOCKET NAMES read out
|
|
77
|
+
* of GPL-2.0-or-later sources. `@vgai/blender` is AGPL-3.0-only, which that
|
|
78
|
+
* is compatible with (relicense to GPL-3.0, then §13), and the package
|
|
79
|
+
* already carries Blender itself — the same standing this file's sibling
|
|
80
|
+
* `blender-icon-trace.mjs` records for the icon paths. Nothing traced here
|
|
81
|
+
* may be copied into an Apache-2.0 or MIT part of this repo.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
85
|
+
import { homedir } from 'node:os';
|
|
86
|
+
import { join, relative } from 'node:path';
|
|
87
|
+
import { fileURLToPath } from 'node:url';
|
|
88
|
+
|
|
89
|
+
const DEFAULT_CHECKOUT = join(homedir(), 'volter', 'blender-src');
|
|
90
|
+
const NODES_ROOT = join('source', 'blender', 'nodes');
|
|
91
|
+
const OUT = fileURLToPath(new URL('./blender.node-panels.json', import.meta.url));
|
|
92
|
+
|
|
93
|
+
/** The two `add_panel` call sites that are not a node's declaration — see the
|
|
94
|
+
* header. Both are named rather than filtered by a shape rule, so a third one
|
|
95
|
+
* appearing upstream shows up as a new row instead of vanishing. */
|
|
96
|
+
const NOT_A_DECLARATION = new Set([
|
|
97
|
+
join(NODES_ROOT, 'intern', 'node_common.cc'),
|
|
98
|
+
join(NODES_ROOT, 'intern', 'node_declaration.cc'),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
/* ----------------------------------------------------------------- scanning */
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Comments out, strings kept. A C++ scanner rather than a regex because a
|
|
105
|
+
* `//` inside a string literal (`"http://…"`) and a `"` inside a comment both
|
|
106
|
+
* occur in this tree, and either one defeats the regex form.
|
|
107
|
+
*/
|
|
108
|
+
function stripComments(source) {
|
|
109
|
+
let out = '';
|
|
110
|
+
for (let index = 0; index < source.length; index++) {
|
|
111
|
+
const two = source.slice(index, index + 2);
|
|
112
|
+
if (two === '//') {
|
|
113
|
+
while (index < source.length && source[index] !== '\n') index++;
|
|
114
|
+
out += '\n';
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (two === '/*') {
|
|
118
|
+
index += 2;
|
|
119
|
+
while (index < source.length && source.slice(index, index + 2) !== '*/') {
|
|
120
|
+
if (source[index] === '\n') out += '\n';
|
|
121
|
+
index++;
|
|
122
|
+
}
|
|
123
|
+
index += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (source[index] === '"') {
|
|
127
|
+
out += '"';
|
|
128
|
+
index++;
|
|
129
|
+
while (index < source.length && source[index] !== '"') {
|
|
130
|
+
if (source[index] === '\\') {
|
|
131
|
+
out += source[index] + (source[index + 1] ?? '');
|
|
132
|
+
index += 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
out += source[index];
|
|
136
|
+
index++;
|
|
137
|
+
}
|
|
138
|
+
out += '"';
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
out += source[index];
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The body of the block whose `{` is at `open`, without the braces. */
|
|
147
|
+
function blockAt(source, open) {
|
|
148
|
+
let depth = 0;
|
|
149
|
+
for (let index = open; index < source.length; index++) {
|
|
150
|
+
if (source[index] === '{') depth++;
|
|
151
|
+
else if (source[index] === '}') {
|
|
152
|
+
depth--;
|
|
153
|
+
if (depth === 0) return source.slice(open + 1, index);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw new Error('unbalanced block');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A block split into STATEMENTS, with control-flow blocks flattened in place.
|
|
161
|
+
*
|
|
162
|
+
* `;` at nesting depth 0 ends a statement; a `{ … }` that closes back to
|
|
163
|
+
* depth 0 is spliced in as its own statements, which is what makes an
|
|
164
|
+
* `if`/`else` body contribute its items in source order (and what makes the
|
|
165
|
+
* entry `conditional` — see the header).
|
|
166
|
+
*/
|
|
167
|
+
function statementsOf(block) {
|
|
168
|
+
const out = [];
|
|
169
|
+
let current = '';
|
|
170
|
+
let depth = 0;
|
|
171
|
+
let sawControl = false;
|
|
172
|
+
for (let index = 0; index < block.length; index++) {
|
|
173
|
+
const char = block[index];
|
|
174
|
+
if (char === '"') {
|
|
175
|
+
const end = block.indexOf('"', index + 1);
|
|
176
|
+
current += block.slice(index, end + 1);
|
|
177
|
+
index = end;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (char === '(' || char === '[') depth++;
|
|
181
|
+
else if (char === ')' || char === ']') depth--;
|
|
182
|
+
else if (char === '{') {
|
|
183
|
+
if (depth === 0) {
|
|
184
|
+
// A control-flow or scope block at statement level. Its HEAD (the
|
|
185
|
+
// `if (…)`, the `for (…)`) is discarded and its body is flattened in.
|
|
186
|
+
const inner = blockAt(block, index);
|
|
187
|
+
if (/\b(if|else|for|while|switch|do)\b/.test(current)) sawControl = true;
|
|
188
|
+
const nested = statementsOf(inner);
|
|
189
|
+
out.push(...nested.statements);
|
|
190
|
+
sawControl ||= nested.sawControl;
|
|
191
|
+
index += inner.length + 1;
|
|
192
|
+
current = '';
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
depth++;
|
|
196
|
+
} else if (char === '}') depth--;
|
|
197
|
+
else if (char === ';' && depth === 0) {
|
|
198
|
+
out.push(current);
|
|
199
|
+
current = '';
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
current += char;
|
|
203
|
+
}
|
|
204
|
+
if (current.trim() !== '') out.push(current);
|
|
205
|
+
return { statements: out, sawControl };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ----------------------------------------------------------------- the walk */
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The string literals in the FIRST call of a chain, and only that call.
|
|
212
|
+
*
|
|
213
|
+
* `open` is the index of its `(`. Taking the balanced argument list rather
|
|
214
|
+
* than "every quoted run in the statement" is not a nicety: the very first
|
|
215
|
+
* node measured, `Base Color`, chains
|
|
216
|
+
* `.description("Color of the material used for …")`, and a whole-statement
|
|
217
|
+
* scan reads that sentence as the socket's identifier.
|
|
218
|
+
*/
|
|
219
|
+
function firstStrings(call, open) {
|
|
220
|
+
let depth = 0;
|
|
221
|
+
let end = call.length;
|
|
222
|
+
for (let index = open; index < call.length; index++) {
|
|
223
|
+
const char = call[index];
|
|
224
|
+
if (char === '"') {
|
|
225
|
+
index = call.indexOf('"', index + 1);
|
|
226
|
+
if (index < 0) break;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (char === '(') depth++;
|
|
230
|
+
else if (char === ')') {
|
|
231
|
+
depth--;
|
|
232
|
+
if (depth === 0) {
|
|
233
|
+
end = index;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const args = call.slice(open + 1, end);
|
|
239
|
+
const found = [];
|
|
240
|
+
const each = /"((?:[^"\\]|\\.)*)"/g;
|
|
241
|
+
for (let hit = each.exec(args); hit !== null; hit = each.exec(args)) found.push(hit[1]);
|
|
242
|
+
return found;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* ONE DECLARATION BODY → the `root_items` tree Blender's own
|
|
247
|
+
* `NodeDeclaration` holds, in declaration order.
|
|
248
|
+
*
|
|
249
|
+
* Each item is `{kind}` where kind is `input` | `output` | `panel` |
|
|
250
|
+
* `layout` | `separator`, matching the five `flat_item::Type`s
|
|
251
|
+
* `make_flat_node_items` produces (`node_draw.cc:798-831`). A panel carries
|
|
252
|
+
* its `name`, its `defaultClosed` and its own `items`.
|
|
253
|
+
*/
|
|
254
|
+
function parseDeclaration(body, rootParam) {
|
|
255
|
+
const root = { items: [] };
|
|
256
|
+
/** Builder variable → the item list it appends to. The root builder's own
|
|
257
|
+
* parameter is bound first, which is how `b.add_input(…)` lands at root. */
|
|
258
|
+
const lists = new Map([[rootParam, root]]);
|
|
259
|
+
const { statements, sawControl } = statementsOf(body);
|
|
260
|
+
let customSocketOrder = false;
|
|
261
|
+
const unknown = [];
|
|
262
|
+
|
|
263
|
+
for (const statement of statements) {
|
|
264
|
+
// PREPROCESSOR LINES ARE NOT STATEMENTS and they carry no `;`, so a
|
|
265
|
+
// `#define SOCK_BASE_COLOR_ID 0` between two `add_input` calls otherwise
|
|
266
|
+
// glues itself to the front of the next one and defeats the anchored
|
|
267
|
+
// match below. Principled has one after every socket.
|
|
268
|
+
const text = statement
|
|
269
|
+
.split('\n')
|
|
270
|
+
.filter((line) => !/^\s*#/.test(line))
|
|
271
|
+
.join('\n')
|
|
272
|
+
.replace(/\s+/g, ' ')
|
|
273
|
+
.trim();
|
|
274
|
+
if (text === '') continue;
|
|
275
|
+
if (new RegExp(`\\b${rootParam}\\.use_custom_socket_order\\s*\\(`).test(text)) {
|
|
276
|
+
customSocketOrder = true;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// `PanelDeclarationBuilder &name = recv.add_panel(…)` / `auto &name = …`.
|
|
280
|
+
const bind = /^(?:const\s+)?(?:PanelDeclarationBuilder|auto)\s*&\s*(\w+)\s*=\s*([\s\S]*)$/.exec(
|
|
281
|
+
text,
|
|
282
|
+
);
|
|
283
|
+
const binding = bind ? bind[1] : null;
|
|
284
|
+
const call = bind ? bind[2] : text;
|
|
285
|
+
const head = /^(\w+)\s*\.\s*(\w+)\s*(?:<[^(]*>)?\s*\(/.exec(call);
|
|
286
|
+
if (!head) continue;
|
|
287
|
+
const [, receiver, method] = head;
|
|
288
|
+
const open = call.indexOf('(', head[0].length - 1);
|
|
289
|
+
const owner = lists.get(receiver);
|
|
290
|
+
if (!owner) continue;
|
|
291
|
+
|
|
292
|
+
if (method === 'add_panel') {
|
|
293
|
+
const names = firstStrings(call, open);
|
|
294
|
+
const panel = { kind: 'panel', name: names[0] ?? '', items: [] };
|
|
295
|
+
// `PanelDeclaration::default_collapsed` (`NOD_node_declaration.hh:505`),
|
|
296
|
+
// set by `.default_closed(bool)`. Absent means false.
|
|
297
|
+
if (/\.default_closed\s*\(\s*true\s*\)/.test(call)) panel.defaultClosed = true;
|
|
298
|
+
owner.items.push(panel);
|
|
299
|
+
if (binding) lists.set(binding, panel);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (method === 'add_input' || method === 'add_output') {
|
|
303
|
+
const names = firstStrings(call, open);
|
|
304
|
+
if (names.length === 0) continue;
|
|
305
|
+
// EVERY OPTIONAL FLAG IS OMITTED WHEN IT IS FALSE, and `identifier` when
|
|
306
|
+
// it equals the name. The table is 36 node types and is meant to be
|
|
307
|
+
// READ in review; spelling four `false`s per socket triples it for no
|
|
308
|
+
// information (105 KB against 47 KB, measured).
|
|
309
|
+
const socket = {
|
|
310
|
+
kind: method === 'add_input' ? 'input' : 'output',
|
|
311
|
+
name: names[0],
|
|
312
|
+
};
|
|
313
|
+
// `add_input(name, identifier)` — the identifier defaults to the name
|
|
314
|
+
// (`DeclarationListBuilder::add_socket`), and the identifier is what a
|
|
315
|
+
// live `NodeSocket` is matched on.
|
|
316
|
+
if (names[1] && names[1] !== names[0]) socket.identifier = names[1];
|
|
317
|
+
// `.panel_toggle()` makes this the panel's HEADER checkbox rather than a
|
|
318
|
+
// row of its own — `PanelDeclaration::panel_input_decl()`
|
|
319
|
+
// (`node_declaration.cc:528-542`) takes the panel's FIRST item when it
|
|
320
|
+
// is a panel-toggle boolean input.
|
|
321
|
+
if (/\.panel_toggle\s*\(/.test(call)) socket.panelToggle = true;
|
|
322
|
+
// `.available(<expr>)` — a socket whose availability is COMPUTED. It is
|
|
323
|
+
// recorded rather than resolved: the consumer matches against the LIVE
|
|
324
|
+
// socket list, where the expression has already been evaluated.
|
|
325
|
+
if (/\.available\s*\(/.test(call)) socket.conditional = true;
|
|
326
|
+
// `SocketDeclaration::align_with_previous_socket` (`:196`): this socket
|
|
327
|
+
// shares the previous socket's ROW instead of taking one
|
|
328
|
+
// (`add_flat_items_for_socket`, `node_draw.cc:704-717`).
|
|
329
|
+
if (/\.align_with_previous\s*\(\s*(?:true\s*)?\)/.test(call)) {
|
|
330
|
+
socket.alignWithPrevious = true;
|
|
331
|
+
}
|
|
332
|
+
owner.items.push(socket);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (method === 'add_layout' || method === 'add_default_layout') {
|
|
336
|
+
owner.items.push({ kind: 'layout' });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (method === 'add_separator') {
|
|
340
|
+
owner.items.push({ kind: 'separator' });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (method.startsWith('add_')) unknown.push(`${receiver}.${method}`);
|
|
344
|
+
}
|
|
345
|
+
return { items: root.items, customSocketOrder, conditional: sawControl, unknown };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ------------------------------------------------------------ the registry */
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The node types a file registers, each with the declaration function it
|
|
352
|
+
* points `ntype.declare` at.
|
|
353
|
+
*
|
|
354
|
+
* `<x>_node_type_base(&ntype, "IDNAME"_ustr, …)` names the type and
|
|
355
|
+
* `ntype.declare = <ns>::<fn>` names the body. Where one file registers two
|
|
356
|
+
* types from two nested namespaces with the SAME function name
|
|
357
|
+
* (`node_geo_foreach_geometry_element.cc` is the measured case), the body is
|
|
358
|
+
* the definition of that name nearest ABOVE the register function — which is
|
|
359
|
+
* what C++ name lookup resolves to from inside the namespace.
|
|
360
|
+
*/
|
|
361
|
+
function registrationsOf(source) {
|
|
362
|
+
const declares = [];
|
|
363
|
+
const declareEach =
|
|
364
|
+
/(?:static\s+)?void\s+(\w+)\s*\(\s*(?:const\s+)?NodeDeclarationBuilder\s*&\s*(\w+)\s*\)\s*\{/g;
|
|
365
|
+
for (let hit = declareEach.exec(source); hit !== null; hit = declareEach.exec(source)) {
|
|
366
|
+
declares.push({
|
|
367
|
+
name: hit[1],
|
|
368
|
+
param: hit[2],
|
|
369
|
+
at: hit.index,
|
|
370
|
+
body: blockAt(source, hit.index + hit[0].length - 1),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const out = [];
|
|
374
|
+
// THREE SPELLINGS, measured across the tree: `static void node_register()`
|
|
375
|
+
// with `NOD_REGISTER_NODE(node_register)` (411 sites, the modern one),
|
|
376
|
+
// `static void register_node()` (5) and the legacy
|
|
377
|
+
// `void register_node_type_sh_*()` (the shader tree's). A function is a
|
|
378
|
+
// registration when its body calls `<x>_node_type_base(&ntype, "IDNAME")`,
|
|
379
|
+
// which is the test below; these names are only how the body is FOUND.
|
|
380
|
+
const registerEach =
|
|
381
|
+
/(?:static\s+)?void\s+(register_node_type_\w+|node_register|register_node)\s*\(\s*\)\s*\{/g;
|
|
382
|
+
for (let hit = registerEach.exec(source); hit !== null; hit = registerEach.exec(source)) {
|
|
383
|
+
const body = blockAt(source, hit.index + hit[0].length - 1);
|
|
384
|
+
const idname = /_node_type_base\s*\(\s*&\s*ntype\s*,\s*"([^"]+)"/.exec(body);
|
|
385
|
+
const declare = /ntype\.declare\s*=\s*(?:[\w:]+::)?(\w+)\s*;/.exec(body);
|
|
386
|
+
if (!idname || !declare) continue;
|
|
387
|
+
const candidates = declares.filter(
|
|
388
|
+
(entry) => entry.name === declare[1] && entry.at < hit.index,
|
|
389
|
+
);
|
|
390
|
+
const chosen = candidates[candidates.length - 1];
|
|
391
|
+
if (!chosen) continue;
|
|
392
|
+
out.push({ idname: idname[1], declare: chosen });
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* -------------------------------------------------------------------- main */
|
|
398
|
+
|
|
399
|
+
function* walk(directory) {
|
|
400
|
+
for (const entry of readdirSync(directory)) {
|
|
401
|
+
const path = join(directory, entry);
|
|
402
|
+
if (statSync(path).isDirectory()) yield* walk(path);
|
|
403
|
+
else if (/^node_.*\.cc$/.test(entry)) yield path;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function build(checkout) {
|
|
408
|
+
const root = join(checkout, NODES_ROOT);
|
|
409
|
+
const table = {};
|
|
410
|
+
const report = { files: 0, withPanels: 0, types: 0, conditional: [], unknown: new Set() };
|
|
411
|
+
for (const path of [...walk(root)].sort()) {
|
|
412
|
+
report.files++;
|
|
413
|
+
const key = relative(checkout, path);
|
|
414
|
+
if (NOT_A_DECLARATION.has(key)) continue;
|
|
415
|
+
const raw = readFileSync(path, 'utf8');
|
|
416
|
+
if (!raw.includes('add_panel')) continue;
|
|
417
|
+
const source = stripComments(raw);
|
|
418
|
+
let traced = 0;
|
|
419
|
+
for (const { idname, declare } of registrationsOf(source)) {
|
|
420
|
+
const parsed = parseDeclaration(declare.body, declare.param);
|
|
421
|
+
if (!parsed.items.some((item) => item.kind === 'panel')) continue;
|
|
422
|
+
traced++;
|
|
423
|
+
report.types++;
|
|
424
|
+
if (parsed.conditional) report.conditional.push(idname);
|
|
425
|
+
for (const name of parsed.unknown) report.unknown.add(name);
|
|
426
|
+
table[idname] = {
|
|
427
|
+
// `is_node_panels_supported` is `decl->use_custom_socket_order`
|
|
428
|
+
// (`node_draw.cc:343-346`), which is what puts the node on the
|
|
429
|
+
// DECLARATION layout path at all. A panel-declaring node that does not
|
|
430
|
+
// set it is a contradiction worth carrying rather than hiding.
|
|
431
|
+
customSocketOrder: parsed.customSocketOrder,
|
|
432
|
+
...(parsed.conditional ? { conditional: true } : {}),
|
|
433
|
+
items: parsed.items,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (traced > 0) report.withPanels++;
|
|
437
|
+
}
|
|
438
|
+
return { table, report };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function main() {
|
|
442
|
+
const argv = process.argv.slice(2);
|
|
443
|
+
const checkoutFlag = argv.indexOf('--checkout');
|
|
444
|
+
const checkout = checkoutFlag >= 0 ? argv[checkoutFlag + 1] : DEFAULT_CHECKOUT;
|
|
445
|
+
const { table, report } = build(checkout);
|
|
446
|
+
const ordered = Object.fromEntries(Object.entries(table).sort(([a], [b]) => (a < b ? -1 : 1)));
|
|
447
|
+
const text = `${JSON.stringify(ordered, null, 2)}\n`;
|
|
448
|
+
if (argv.includes('--check')) {
|
|
449
|
+
const have = readFileSync(OUT, 'utf8');
|
|
450
|
+
if (have !== text) {
|
|
451
|
+
console.error('blender.node-panels.json is STALE — re-run this script without --check.');
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
console.log(
|
|
455
|
+
`blender.node-panels.json — up to date (${Object.keys(ordered).length} node types)`,
|
|
456
|
+
);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
writeFileSync(OUT, text);
|
|
460
|
+
const panels = Object.values(ordered).reduce((sum, entry) => sum + countPanels(entry.items), 0);
|
|
461
|
+
console.log(
|
|
462
|
+
`blender.node-panels.json — ${Object.keys(ordered).length} node types, ${panels} panels, ` +
|
|
463
|
+
`from ${report.withPanels} of ${report.files} node_*.cc files`,
|
|
464
|
+
);
|
|
465
|
+
if (report.conditional.length > 0) {
|
|
466
|
+
console.log(
|
|
467
|
+
` conditional declarations (parsed straight through): ${report.conditional.join(', ')}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (report.unknown.size > 0) {
|
|
471
|
+
console.log(` builder calls this grammar does not know: ${[...report.unknown].join(', ')}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function countPanels(items) {
|
|
476
|
+
let count = 0;
|
|
477
|
+
for (const item of items) {
|
|
478
|
+
if (item.kind === 'panel') count += 1 + countPanels(item.items);
|
|
479
|
+
}
|
|
480
|
+
return count;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
|
|
484
|
+
|
|
485
|
+
export { build, parseDeclaration, registrationsOf, statementsOf, stripComments };
|