Skip to content

Commit 52ddebc

Browse files
fix detection of all inputs (#130)
Some were potentially hidden depending on intniial node setting configuration.
1 parent 2be8123 commit 52ddebc

7 files changed

Lines changed: 1219 additions & 72 deletions

File tree

docs/changelog.qmd

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
title: Changelog
33
---
44

5+
## v520.10.0 - 2026-08-03
6+
7+
### Fixes
8+
- **Asset generation exposes every interface input** — introspecting an asset node group skipped inputs that Blender's socket-usage inference marks inactive under the group's current node options (e.g. a Menu Switch selection deactivating the inputs of the branches not taken), so those parameters were silently missing from the generated `__init__`. All interface inputs are now generated; the bundled essentials APIs were regenerated and pick up the previously hidden menu-gated inputs (e.g. the compositor `ChromaticAberration` gains `axis`, `center`, `samples` and `fit`).
9+
10+
## v520.9.0 - 2026-07-22
11+
12+
### Enhancements
13+
- Generated asset classes now carry numpy-style docstrings (description, `Parameters`, `Inputs`, `Outputs`) built from the asset's own socket tooltips, so editors show documentation alongside the type hints. Pass `docstrings=False` to `generate_asset_api` (or `--no-docstrings` to `python -m nodebpy.assets`) for the terser output.
14+
- Menu sockets on generated asset classes are typed with the items they actually offer — `shape: InputMenu | Literal["Line", "Circle", "Curve", "Transform"]` — matching how menu sockets are already typed on the built-in nodes.
15+
516
## v520.8.0 - 2026-07-17
617

718
### Enhancements
@@ -42,8 +53,6 @@ python -m nodebpy.assets -b my_assets.blend -o my_addon/nodes/
4253

4354
### Enhancements
4455
- Changed the linking of asset node groups for the `_AssetGroupMixin` to be 'linked & packed' by default
45-
- Generated asset classes now carry numpy-style docstrings (description, `Parameters`, `Inputs`, `Outputs`) built from the asset's own socket tooltips, so editors show documentation alongside the type hints. Pass `docstrings=False` to `generate_asset_api` (or `--no-docstrings` to `python -m nodebpy.assets`) for the terser output.
46-
- Menu sockets on generated asset classes are typed with the items they actually offer — `shape: InputMenu | Literal["Line", "Circle", "Curve", "Transform"]` — matching how menu sockets are already typed on the built-in nodes.
4756

4857
## v520.4.0 - 2026-06-19
4958

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "nodebpy"
3-
version = "520.9.0"
3+
version = "520.10.0"
44
description = "Build nodes trees in Blender more elegantly with code"
55
readme = "README.md"
66
authors = [

src/nodebpy/assets/_codegen.py

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,10 @@ def _collect(
181181
group's *inputs*, whose parameters are typed from them.
182182
"""
183183
descriptions = descriptions or {}
184-
raw = [
185-
s
186-
for s in sockets
187-
if s.identifier != "__extend__" and not getattr(s, "is_inactive", False)
188-
]
184+
# Keep inactive sockets: socket-usage inference deactivates inputs that the
185+
# current node options (e.g. a menu selection) leave unused, but a caller
186+
# may set those options differently, so the API must expose every input.
187+
raw = [s for s in sockets if s.identifier != "__extend__"]
189188
# The accessor resolves attribute names by identifier first, then name, so a
190189
# group socket's readable name works as the attr/param when it's unambiguous;
191190
# fall back to the opaque-but-unique identifier only on a name collision.
@@ -213,6 +212,43 @@ def _collect(
213212
return out
214213

215214

215+
def _introspect_group(group, name: str, library_source: str) -> _AssetClass:
216+
"""Introspect the node group ``group`` (a ``bpy.types.NodeTree``) into an
217+
:class:`_AssetClass` record by instantiating it on a throwaway host node.
218+
219+
``group`` is deliberately unannotated: the stubs type interface items and
220+
``node_groups.new`` too narrowly for the runtime attributes used here.
221+
"""
222+
host = bpy.data.node_groups.new("_introspect_host", group.bl_idname)
223+
try:
224+
node_type = {
225+
"GeometryNodeTree": "GeometryNodeGroup",
226+
"ShaderNodeTree": "ShaderNodeGroup",
227+
"CompositorNodeTree": "CompositorNodeGroup",
228+
}[group.bl_idname]
229+
node = host.nodes.new(node_type)
230+
node.node_tree = group # ty: ignore[unresolved-attribute]
231+
# Tooltips live on the tree *interface* items, not on the node's
232+
# sockets — collect them by identifier so the generated docstrings
233+
# can use the asset author's own wording.
234+
descriptions = {
235+
item.identifier: item.description or ""
236+
for item in group.interface.items_tree
237+
if item.item_type == "SOCKET"
238+
}
239+
return _AssetClass(
240+
class_name=_class_name(name),
241+
asset_name=name,
242+
description=(group.description or name).strip(),
243+
library_source=library_source,
244+
tree_idname=group.bl_idname,
245+
inputs=_collect(node.inputs, descriptions, menus=True),
246+
outputs=_collect(node.outputs, descriptions),
247+
)
248+
finally:
249+
bpy.data.node_groups.remove(host)
250+
251+
216252
def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetClass]:
217253
"""Append each requested group from the library and introspect its
218254
interface into :class:`_AssetClass` records."""
@@ -236,37 +272,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
236272
):
237273
dst.node_groups = [name]
238274
group = dst.node_groups[0]
239-
240-
host = bpy.data.node_groups.new("_introspect_host", group.bl_idname)
241-
try:
242-
node_type = {
243-
"GeometryNodeTree": "GeometryNodeGroup",
244-
"ShaderNodeTree": "ShaderNodeGroup",
245-
"CompositorNodeTree": "CompositorNodeGroup",
246-
}[group.bl_idname]
247-
node = host.nodes.new(node_type)
248-
node.node_tree = group # ty: ignore[unresolved-attribute]
249-
# Tooltips live on the tree *interface* items, not on the node's
250-
# sockets — collect them by identifier so the generated docstrings
251-
# can use the asset author's own wording.
252-
descriptions = {
253-
item.identifier: item.description or ""
254-
for item in group.interface.items_tree
255-
if item.item_type == "SOCKET"
256-
}
257-
classes.append(
258-
_AssetClass(
259-
class_name=_class_name(name),
260-
asset_name=name,
261-
description=(group.description or name).strip(),
262-
library_source=library_source,
263-
tree_idname=group.bl_idname,
264-
inputs=_collect(node.inputs, descriptions, menus=True),
265-
outputs=_collect(node.outputs, descriptions),
266-
)
267-
)
268-
finally:
269-
bpy.data.node_groups.remove(host)
275+
classes.append(_introspect_group(group, name, library_source))
270276
return classes
271277

272278

src/nodebpy/nodes/compositor/assets.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,33 @@ class ChromaticAberration(AssetCompositorGroup):
3535
Image
3636
type : InputMenu | Literal["Offset", "Scale", "Directional Blur", "Lens Dispersion"]
3737
Different styles of aberrations
38+
axis : InputMenu | Literal["Vertical", "Horizontal"]
39+
Direction of the aberration effect
3840
factor : InputFloat
3941
The intensity of the aberration effect
42+
center : InputVector
43+
The position the transformations pivot around, in normalized coordinates. 0 means the lower left corner and 1 means the upper right corner of the image.
44+
samples : InputInteger
45+
The number of samples used to compute the blur. The more samples the smoother the result, at the expense of more compute time. The actual number of samples is two to the power of this input, so it increases exponentially.
46+
fit : InputBoolean
47+
Scale the image such that it fits entirely in the frame, leaving no empty spaces at the corners
4048
4149
Inputs
4250
------
4351
i.image : ColorSocket
4452
Image
4553
i.type : MenuSocket
4654
Different styles of aberrations
55+
i.axis : MenuSocket
56+
Direction of the aberration effect
4757
i.factor : FloatSocket
4858
The intensity of the aberration effect
59+
i.center : VectorSocket
60+
The position the transformations pivot around, in normalized coordinates. 0 means the lower left corner and 1 means the upper right corner of the image.
61+
i.samples : IntegerSocket
62+
The number of samples used to compute the blur. The more samples the smoother the result, at the expense of more compute time. The actual number of samples is two to the power of this input, so it increases exponentially.
63+
i.fit : BooleanSocket
64+
Scale the image such that it fits entirely in the frame, leaving no empty spaces at the corners
4965
5066
Outputs
5167
-------
@@ -62,8 +78,16 @@ class _Inputs(SocketAccessor):
6278
"""Image"""
6379
type: MenuSocket
6480
"""Different styles of aberrations"""
81+
axis: MenuSocket
82+
"""Direction of the aberration effect"""
6583
factor: FloatSocket
6684
"""The intensity of the aberration effect"""
85+
center: VectorSocket
86+
"""The position the transformations pivot around, in normalized coordinates. 0 means the lower left corner and 1 means the upper right corner of the image."""
87+
samples: IntegerSocket
88+
"""The number of samples used to compute the blur. The more samples the smoother the result, at the expense of more compute time. The actual number of samples is two to the power of this input, so it increases exponentially."""
89+
fit: BooleanSocket
90+
"""Scale the image such that it fits entirely in the frame, leaving no empty spaces at the corners"""
6791

6892
class _Outputs(SocketAccessor):
6993
image: ColorSocket
@@ -81,9 +105,23 @@ def __init__(
81105
image: InputColor = None,
82106
type: InputMenu
83107
| Literal["Offset", "Scale", "Directional Blur", "Lens Dispersion"] = "Scale",
108+
axis: InputMenu | Literal["Vertical", "Horizontal"] = "Horizontal",
84109
factor: InputFloat = 0.2,
110+
center: InputVector = None,
111+
samples: InputInteger = 3,
112+
fit: InputBoolean = False,
85113
):
86-
super().__init__(**{"Socket_1": image, "Socket_6": type, "Socket_2": factor})
114+
super().__init__(
115+
**{
116+
"Socket_1": image,
117+
"Socket_6": type,
118+
"Socket_8": axis,
119+
"Socket_2": factor,
120+
"Socket_3": center,
121+
"Socket_4": samples,
122+
"Socket_7": fit,
123+
}
124+
)
87125

88126

89127
class CombineCylindrical(AssetCompositorGroup):
@@ -218,8 +256,28 @@ class FilmGrain(AssetCompositorGroup):
218256
Amount of mixing between the effect and original image
219257
preset : InputMenu | Literal["8 mm Caffenol", "8 mm Home Movie", "Super 8 mm", "16 mm Broadcast", "16 mm Indie Cinema", "35 mm Portra 400", "Super 35 mm", "70 mm Cinema", "Custom"]
220258
Preset
259+
film_gauge : InputMenu | Literal["8 mm", "16 mm", "35 mm", "70 mm"]
260+
Film Gauge
261+
style : InputMenu | Literal["Xpro Sharp", "Cinematic Gritty", "Home Movie", "Studio Broadcast", "Pro Photography", "Cinematic Soft", "Custom Style"]
262+
Style
221263
animated : InputBoolean
222264
Should the noise change frame to frame
265+
iso : InputInteger
266+
ISO
267+
softness : InputFloat
268+
Halation-style softening: edges "bleed" and become smoother, adds bloom and noise that is somewhat pattern-aware
269+
acutance : InputFloat
270+
Sharpens large details and adds a halo around objects
271+
coarseness : InputFloat
272+
Emulsion roughness, also adds grain clamping
273+
patchiness : InputFloat
274+
Smoothness of the film substrate. High values contribute to a stroboscope effect when animating.
275+
saturation : InputFloat
276+
Noise colorfulness
277+
luma_bias : InputFloat
278+
Luma bias
279+
texture_scale : InputFloat
280+
Texture Scale
223281
224282
Inputs
225283
------
@@ -229,8 +287,28 @@ class FilmGrain(AssetCompositorGroup):
229287
Amount of mixing between the effect and original image
230288
i.preset : MenuSocket
231289
Preset
290+
i.film_gauge : MenuSocket
291+
Film Gauge
292+
i.style : MenuSocket
293+
Style
232294
i.animated : BooleanSocket
233295
Should the noise change frame to frame
296+
i.iso : IntegerSocket
297+
ISO
298+
i.softness : FloatSocket
299+
Halation-style softening: edges "bleed" and become smoother, adds bloom and noise that is somewhat pattern-aware
300+
i.acutance : FloatSocket
301+
Sharpens large details and adds a halo around objects
302+
i.coarseness : FloatSocket
303+
Emulsion roughness, also adds grain clamping
304+
i.patchiness : FloatSocket
305+
Smoothness of the film substrate. High values contribute to a stroboscope effect when animating.
306+
i.saturation : FloatSocket
307+
Noise colorfulness
308+
i.luma_bias : FloatSocket
309+
Luma bias
310+
i.texture_scale : FloatSocket
311+
Texture Scale
234312
235313
Outputs
236314
-------
@@ -249,8 +327,28 @@ class _Inputs(SocketAccessor):
249327
"""Amount of mixing between the effect and original image"""
250328
preset: MenuSocket
251329
"""Preset"""
330+
film_gauge: MenuSocket
331+
"""Film Gauge"""
332+
style: MenuSocket
333+
"""Style"""
252334
animated: BooleanSocket
253335
"""Should the noise change frame to frame"""
336+
iso: IntegerSocket
337+
"""ISO"""
338+
softness: FloatSocket
339+
"""Halation-style softening: edges "bleed" and become smoother, adds bloom and noise that is somewhat pattern-aware"""
340+
acutance: FloatSocket
341+
"""Sharpens large details and adds a halo around objects"""
342+
coarseness: FloatSocket
343+
"""Emulsion roughness, also adds grain clamping"""
344+
patchiness: FloatSocket
345+
"""Smoothness of the film substrate. High values contribute to a stroboscope effect when animating."""
346+
saturation: FloatSocket
347+
"""Noise colorfulness"""
348+
luma_bias: FloatSocket
349+
"""Luma bias"""
350+
texture_scale: FloatSocket
351+
"""Texture Scale"""
254352

255353
class _Outputs(SocketAccessor):
256354
result: ColorSocket
@@ -279,14 +377,43 @@ def __init__(
279377
"70 mm Cinema",
280378
"Custom",
281379
] = "Super 8 mm",
380+
film_gauge: InputMenu | Literal["8 mm", "16 mm", "35 mm", "70 mm"] = "16 mm",
381+
style: InputMenu
382+
| Literal[
383+
"Xpro Sharp",
384+
"Cinematic Gritty",
385+
"Home Movie",
386+
"Studio Broadcast",
387+
"Pro Photography",
388+
"Cinematic Soft",
389+
"Custom Style",
390+
] = "Studio Broadcast",
282391
animated: InputBoolean = False,
392+
iso: InputInteger = 400,
393+
softness: InputFloat = 0.5,
394+
acutance: InputFloat = 0.5,
395+
coarseness: InputFloat = 0.5,
396+
patchiness: InputFloat = 0.5,
397+
saturation: InputFloat = 0.5,
398+
luma_bias: InputFloat = 0.5,
399+
texture_scale: InputFloat = 0.5,
283400
):
284401
super().__init__(
285402
**{
286403
"Socket_1": input,
287404
"Socket_7": factor,
288405
"Socket_64": preset,
406+
"Socket_46": film_gauge,
407+
"Socket_47": style,
289408
"Socket_10": animated,
409+
"Socket_56": iso,
410+
"Socket_52": softness,
411+
"Socket_70": acutance,
412+
"Socket_53": coarseness,
413+
"Socket_54": patchiness,
414+
"Socket_55": saturation,
415+
"Socket_65": luma_bias,
416+
"Socket_73": texture_scale,
290417
}
291418
)
292419

@@ -395,6 +522,8 @@ class Retime(AssetCompositorGroup):
395522
Multiply the current scene frame by this factor
396523
cyclic : InputBoolean
397524
Restart from start frame when Cycle Length is reached
525+
cycle_length : InputInteger
526+
Number of frames to cycle through. Not affected by Speed.
398527
399528
Inputs
400529
------
@@ -406,6 +535,8 @@ class Retime(AssetCompositorGroup):
406535
Multiply the current scene frame by this factor
407536
i.cyclic : BooleanSocket
408537
Restart from start frame when Cycle Length is reached
538+
i.cycle_length : IntegerSocket
539+
Number of frames to cycle through. Not affected by Speed.
409540
410541
Outputs
411542
-------
@@ -426,6 +557,8 @@ class _Inputs(SocketAccessor):
426557
"""Multiply the current scene frame by this factor"""
427558
cyclic: BooleanSocket
428559
"""Restart from start frame when Cycle Length is reached"""
560+
cycle_length: IntegerSocket
561+
"""Number of frames to cycle through. Not affected by Speed."""
429562

430563
class _Outputs(SocketAccessor):
431564
frame: IntegerSocket
@@ -444,13 +577,15 @@ def __init__(
444577
step: InputInteger = 1,
445578
speed: InputFloat = 1.0,
446579
cyclic: InputBoolean = False,
580+
cycle_length: InputInteger = 10,
447581
):
448582
super().__init__(
449583
**{
450584
"Socket_4": offset,
451585
"Socket_13": step,
452586
"Socket_14": speed,
453587
"Socket_11": cyclic,
588+
"Socket_3": cycle_length,
454589
}
455590
)
456591

0 commit comments

Comments
 (0)