nodebpy 520.21.0__tar.gz → 520.23.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {nodebpy-520.21.0 → nodebpy-520.23.0}/PKG-INFO +1 -1
  2. {nodebpy-520.21.0 → nodebpy-520.23.0}/pyproject.toml +1 -1
  3. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/__init__.py +2 -0
  4. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/assets/_library.py +24 -4
  5. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/assets/_pipeline.py +5 -0
  6. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/__init__.py +2 -0
  7. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/layout.py +26 -0
  8. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/tree.py +49 -17
  9. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/export/codegen.py +14 -3
  10. {nodebpy-520.21.0 → nodebpy-520.23.0}/README.md +0 -0
  11. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/assets/__init__.py +0 -0
  12. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/assets/__main__.py +0 -0
  13. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/assets/_codegen.py +0 -0
  14. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/_registry.py +0 -0
  15. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/_utils.py +0 -0
  16. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/accessor.py +0 -0
  17. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/asset.py +0 -0
  18. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/items.py +0 -0
  19. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/mixins.py +0 -0
  20. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/node.py +0 -0
  21. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/builder/socket.py +0 -0
  22. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/export/__init__.py +0 -0
  23. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/export/diagram.py +0 -0
  24. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/export/parity.py +0 -0
  25. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/export/plot.py +0 -0
  26. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/export/web_render.py +0 -0
  27. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/VENDORED.md +0 -0
  28. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  29. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  30. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  31. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  32. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  33. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  34. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  35. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  36. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  37. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  38. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  39. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  40. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  41. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/__init__.py +0 -0
  42. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/_mixins.py +0 -0
  43. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  44. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
  45. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  46. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  47. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  48. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  49. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  50. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  51. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  52. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
  53. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  54. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  55. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  56. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  57. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
  58. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  59. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  60. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  61. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
  62. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  63. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  64. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  65. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  66. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  67. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
  68. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  69. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  70. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  71. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  72. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
  73. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  74. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/assets.py +0 -0
  75. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/color.py +0 -0
  76. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  77. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  78. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/group.py +0 -0
  79. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/input.py +0 -0
  80. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  81. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/output.py +0 -0
  82. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/script.py +0 -0
  83. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  84. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  85. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  86. {nodebpy-520.21.0 → nodebpy-520.23.0}/src/nodebpy/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.21.0
3
+ Version: 520.23.0
4
4
  Summary: Build nodes trees in Blender more elegantly with code
5
5
  Author: Brady Johnston
6
6
  Author-email: Brady Johnston <brady.johnston@me.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "520.21.0"
3
+ version = "520.23.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -5,6 +5,7 @@ from .builder import (
5
5
  SugiyamaOptions,
6
6
  TreeBuilder,
7
7
  arrange,
8
+ default_split_inputs,
8
9
  default_sugiyama_options,
9
10
  )
10
11
  from .nodes import compositor, geometry, shader
@@ -17,6 +18,7 @@ __all__ = [
17
18
  "arrange",
18
19
  "builder",
19
20
  "compositor",
21
+ "default_split_inputs",
20
22
  "default_sugiyama_options",
21
23
  "export",
22
24
  "geometry",
@@ -88,6 +88,7 @@ from ..builder import (
88
88
  SugiyamaOptions,
89
89
  TreeBuilder,
90
90
  build_from_source,
91
+ default_split_inputs,
91
92
  default_sugiyama_options,
92
93
  )
93
94
  from ..builder._utils import normalize_name
@@ -1062,6 +1063,7 @@ def build_library(
1062
1063
  on_missing: str = "error",
1063
1064
  add_reroutes: bool = False,
1064
1065
  arrange: SugiyamaOptions | None = None,
1066
+ split_inputs: bool = False,
1065
1067
  ) -> list[str]:
1066
1068
  """Rebuild a ``.blend`` asset library from sources written by
1067
1069
  :func:`dump_library`.
@@ -1094,9 +1096,14 @@ def build_library(
1094
1096
  :func:`nodebpy.builder.default_sugiyama_options`. ``add_reroutes=True``
1095
1097
  additionally inserts reroute nodes to route long links around nodes
1096
1098
  (the node-arrange addon's behaviour); it composes with ``arrange``.
1097
- Either only affects modules that leave the arrangement at its default —
1098
- sources dumped with ``snapshot_positions`` disable arrangement and keep
1099
- their authored layout.
1099
+ ``split_inputs=True`` gives each consumer node its own Group Input
1100
+ instance — named and labelled after the interface sockets it carries,
1101
+ with unused sockets hidden — instead of a single Group Input trailing
1102
+ long noodles (scoped via
1103
+ :func:`nodebpy.builder.default_split_inputs`). All three only affect
1104
+ modules that leave the arrangement at its default — sources dumped with
1105
+ ``snapshot_positions`` disable arrangement and keep their authored
1106
+ layout, authored Group Input splits included.
1100
1107
 
1101
1108
  The built trees stay in the current session afterwards. Because
1102
1109
  ``create_group()`` reuses an existing tree of the same name (that is what
@@ -1193,7 +1200,8 @@ def build_library(
1193
1200
  arrange_override = (
1194
1201
  default_sugiyama_options(options) if options is not None else nullcontext()
1195
1202
  )
1196
- with arrange_override:
1203
+ split_override = default_split_inputs() if split_inputs else nullcontext()
1204
+ with arrange_override, split_override:
1197
1205
  # Materials first: asset trees look them up by name while building.
1198
1206
  built_materials = []
1199
1207
  for file, module in material_modules:
@@ -1521,6 +1529,17 @@ def _add_build_flags(parser) -> None: # pragma: no cover
1521
1529
  "defaults apply where unset). Sources dumped with "
1522
1530
  "--snapshot-positions keep their authored layout regardless.",
1523
1531
  )
1532
+ parser.add_argument(
1533
+ "--split-inputs",
1534
+ action="store_true",
1535
+ help=(
1536
+ "Give each consumer node its own Group Input instance, named "
1537
+ "after the interface sockets it uses (unused sockets hidden), "
1538
+ "instead of one Group Input trailing long noodles. Sources "
1539
+ "dumped with --snapshot-positions keep their authored layout "
1540
+ "regardless."
1541
+ ),
1542
+ )
1524
1543
  parser.add_argument(
1525
1544
  "--drop-missing",
1526
1545
  dest="on_missing",
@@ -1784,6 +1803,7 @@ def _build_command(args) -> None:
1784
1803
  on_missing=args.on_missing,
1785
1804
  add_reroutes=args.add_reroutes,
1786
1805
  arrange=_arrange_options_from_args(args),
1806
+ split_inputs=args.split_inputs,
1787
1807
  )
1788
1808
  print(f"Built {args.blend} with {len(names)} assets: {', '.join(names)}")
1789
1809
  write_stamp(args.blend, args.source, args.resources, stamp_options(args))
@@ -88,6 +88,7 @@ _CONFIG_KEYS: dict[str, tuple[str | None, str]] = {
88
88
  "no-compress": ("compress", "off"),
89
89
  "drop-missing": ("on_missing", "drop"),
90
90
  "add-reroutes": ("add_reroutes", "flag"),
91
+ "split-inputs": ("split_inputs", "flag"),
91
92
  "spacing": ("spacing", "value"),
92
93
  "iterations": ("iterations", "value"),
93
94
  "direction": ("direction", "value"),
@@ -122,6 +123,7 @@ _OPTION_DEFAULTS: dict[str, object] = {
122
123
  "compress": True,
123
124
  "on_missing": "error",
124
125
  "add_reroutes": False,
126
+ "split_inputs": False,
125
127
  "spacing": None,
126
128
  "iterations": None,
127
129
  "direction": None,
@@ -150,6 +152,7 @@ _STAMP_DESTS = (
150
152
  "snapshot_positions",
151
153
  "socket_alignment",
152
154
  "spacing",
155
+ "split_inputs",
153
156
  "stack_collapsed",
154
157
  "stack_margin_y_fac",
155
158
  "typed_api",
@@ -345,6 +348,8 @@ def _build_stage_flags(args: Namespace) -> list[str]:
345
348
  flags.append("--no-compress")
346
349
  if args.add_reroutes:
347
350
  flags.append("--add-reroutes")
351
+ if args.split_inputs:
352
+ flags.append("--split-inputs")
348
353
  if args.spacing is not None:
349
354
  flags += ["--spacing", *(str(value) for value in args.spacing)]
350
355
  if args.iterations is not None:
@@ -17,6 +17,7 @@ from .layout import (
17
17
  SugiyamaOptions,
18
18
  arrange,
19
19
  arrange_tree,
20
+ default_split_inputs,
20
21
  default_sugiyama_options,
21
22
  )
22
23
  from .mixins import LinkingMixin, OperatorMixin
@@ -160,6 +161,7 @@ __all__ = [
160
161
  "arrange_tree",
161
162
  "asset_group_base",
162
163
  "build_from_source",
164
+ "default_split_inputs",
163
165
  "default_sugiyama_options",
164
166
  "denormalize_name",
165
167
  "normalize_name",
@@ -512,6 +512,32 @@ def default_sugiyama_options(options: SugiyamaOptions) -> Iterator[None]:
512
512
  _DEFAULT_SUGIYAMA.reset(token)
513
513
 
514
514
 
515
+ # What TreeBuilder's split_inputs=None resolves to. Overridable per scope so
516
+ # a batch build can split the Group Input of trees whose recipes leave
517
+ # TreeBuilder at its default — see :func:`default_split_inputs`.
518
+ _DEFAULT_SPLIT_INPUTS: ContextVar[bool] = ContextVar(
519
+ "nodebpy_default_split_inputs", default=False
520
+ )
521
+
522
+
523
+ @contextmanager
524
+ def default_split_inputs(split: bool = True) -> Iterator[None]:
525
+ """Scope in which every ``TreeBuilder`` left at its default
526
+ ``split_inputs`` splits the Group Input node into one instance per
527
+ consumer node (with unused sockets hidden) on context exit.
528
+
529
+ An explicit ``split_inputs=True/False`` is unaffected, and so are trees
530
+ that disable auto-arrangement (as ``snapshot_positions`` dumps do) —
531
+ their authored layout, including any authored Group Input splits, must
532
+ survive untouched.
533
+ """
534
+ token = _DEFAULT_SPLIT_INPUTS.set(split)
535
+ try:
536
+ yield
537
+ finally:
538
+ _DEFAULT_SPLIT_INPUTS.reset(token)
539
+
540
+
515
541
  def _arrange_sugiyama(tree: bpy.types.NodeTree, options: SugiyamaOptions) -> None:
516
542
  from mathutils import Vector
517
543
 
@@ -31,7 +31,7 @@ from ._utils import (
31
31
  resolve_socket_key,
32
32
  socket_key,
33
33
  )
34
- from .layout import ArrangeMethod
34
+ from .layout import _DEFAULT_SPLIT_INPUTS, ArrangeMethod
35
35
  from .layout import arrange as _arrange_nodes
36
36
  from .socket import (
37
37
  BooleanSocket,
@@ -1106,7 +1106,7 @@ class TreeBuilder[TreeT: NodeTree]:
1106
1106
  arrange: ArrangeMethod = "sugiyama",
1107
1107
  fake_user: bool = False,
1108
1108
  ignore_visibility: bool = False,
1109
- split_inputs: bool = False,
1109
+ split_inputs: bool | None = None,
1110
1110
  ):
1111
1111
  if isinstance(tree, str):
1112
1112
  self.tree = bpy.data.node_groups.new(tree, tree_type) # ty: ignore[invalid-assignment]
@@ -1131,7 +1131,7 @@ class TreeBuilder[TreeT: NodeTree]:
1131
1131
  collapse: bool = False,
1132
1132
  arrange: ArrangeMethod = "sugiyama",
1133
1133
  fake_user: bool = False,
1134
- split_inputs: bool = False,
1134
+ split_inputs: bool | None = None,
1135
1135
  ) -> TreeBuilder[GeometryNodeTree]:
1136
1136
  """Create a geometry node tree."""
1137
1137
  return cast(
@@ -1154,7 +1154,7 @@ class TreeBuilder[TreeT: NodeTree]:
1154
1154
  collapse: bool = False,
1155
1155
  arrange: ArrangeMethod = "sugiyama",
1156
1156
  fake_user: bool = False,
1157
- split_inputs: bool = False,
1157
+ split_inputs: bool | None = None,
1158
1158
  ) -> TreeBuilder[ShaderNodeTree]:
1159
1159
  """Create a shader node tree."""
1160
1160
  return cast(
@@ -1177,7 +1177,7 @@ class TreeBuilder[TreeT: NodeTree]:
1177
1177
  collapse: bool = False,
1178
1178
  arrange: ArrangeMethod = "sugiyama",
1179
1179
  fake_user: bool = False,
1180
- split_inputs: bool = False,
1180
+ split_inputs: bool | None = None,
1181
1181
  ) -> TreeBuilder[CompositorNodeTree]:
1182
1182
  """Create a compositor node tree."""
1183
1183
  return cast(
@@ -1268,7 +1268,14 @@ class TreeBuilder[TreeT: NodeTree]:
1268
1268
  def __exit__(self, *args):
1269
1269
  # Split before auto-layout, so the created instances get arranged
1270
1270
  # next to their consumers.
1271
- if self._split_inputs:
1271
+ split = self._split_inputs
1272
+ if split is None:
1273
+ # The ambient default (see default_split_inputs) only reaches
1274
+ # auto-arranged trees: snapshot-positions dumps disable
1275
+ # arrangement and their authored layout — including any authored
1276
+ # Group Input splits — must survive untouched.
1277
+ split = self._arrange is not None and _DEFAULT_SPLIT_INPUTS.get()
1278
+ if split:
1272
1279
  self.split_group_inputs()
1273
1280
  if self._arrange is not None:
1274
1281
  self.arrange()
@@ -1425,10 +1432,11 @@ class TreeBuilder[TreeT: NodeTree]:
1425
1432
  @property
1426
1433
  def group_input_splits(self) -> list[dict]:
1427
1434
  """The extra Group Input instances beyond the primary one, each as
1428
- ``{"name": ..., "links": [(interface input name, consumer node name,
1429
- consumer socket key), ...], "location": ..., "parent": ...}`` — the
1430
- editor convention of several input nodes near their consumers
1431
- instead of one node trailing long noodles. See the setter."""
1435
+ ``{"name": ..., "label": ..., "links": [(interface input name,
1436
+ consumer node name, consumer socket key), ...], "location": ...,
1437
+ "parent": ...}`` — the editor convention of several input nodes near
1438
+ their consumers instead of one node trailing long noodles. See the
1439
+ setter."""
1432
1440
  splits: list[dict] = []
1433
1441
  for node in self.tree.nodes:
1434
1442
  if node.bl_idname != "NodeGroupInput" or node.name == "Group Input":
@@ -1442,6 +1450,7 @@ class TreeBuilder[TreeT: NodeTree]:
1442
1450
  splits.append(
1443
1451
  {
1444
1452
  "name": node.name,
1453
+ "label": node.label,
1445
1454
  "links": links,
1446
1455
  "location": (node.location.x, node.location.y),
1447
1456
  "parent": node.parent.name if node.parent is not None else None,
@@ -1469,6 +1478,8 @@ class TreeBuilder[TreeT: NodeTree]:
1469
1478
  instance = self.tree.nodes.new("NodeGroupInput")
1470
1479
  assert instance is not None
1471
1480
  instance.name = split["name"]
1481
+ if label := split.get("label"):
1482
+ instance.label = label
1472
1483
  # Authored placement (parent before location — locations are
1473
1484
  # parent-relative); older snapshots without these keys leave the
1474
1485
  # instance where auto-layout or the caller puts it.
@@ -1533,10 +1544,15 @@ class TreeBuilder[TreeT: NodeTree]:
1533
1544
  def split_group_inputs(self) -> None:
1534
1545
  """Split the Group Input node into one instance per consumer node,
1535
1546
  with unused sockets hidden — regenerating the editor style that
1536
- avoids a single input node trailing long noodles. Runs automatically
1537
- on context exit (before auto-layout, so the instances are arranged
1538
- next to their consumers) when the builder was created with
1539
- ``split_inputs=True``."""
1547
+ avoids a single input node trailing long noodles. Each instance is
1548
+ named (and labelled, so the editor header shows it) after the
1549
+ interface sockets it carries, making it easy to spot when scanning
1550
+ the tree (Blender de-duplicates repeated names with ``.001``
1551
+ suffixes as usual), and parented into its consumer's frame so the
1552
+ arranger's frame clustering keeps the pair together. Runs automatically on context exit (before auto-layout, so
1553
+ the instances are arranged next to their consumers) when the builder
1554
+ was created with ``split_inputs=True``, or inside a
1555
+ :func:`~nodebpy.builder.default_split_inputs` scope."""
1540
1556
  primary = self.tree.nodes.get("Group Input")
1541
1557
  if primary is None:
1542
1558
  return
@@ -1546,11 +1562,27 @@ class TreeBuilder[TreeT: NodeTree]:
1546
1562
  by_consumer.setdefault(link.to_node.name, []).append(
1547
1563
  (socket.name, link.to_node.name, socket_key(link.to_socket))
1548
1564
  )
1565
+
1566
+ def socket_names(links: list[tuple[str, str, str]]) -> str:
1567
+ return ", ".join(dict.fromkeys(name for name, _, _ in links))
1568
+
1569
+ def parent_frame(consumer: str) -> str | None:
1570
+ parent = self.tree.nodes[consumer].parent
1571
+ return parent.name if parent is not None else None
1572
+
1549
1573
  # The first consumer keeps the primary node; each further consumer
1550
- # gets its own instance (named like Blender would on duplication).
1574
+ # gets its own instance, named after the sockets it carries and
1575
+ # parented into the consumer's frame — frame clustering would
1576
+ # otherwise push an unparented instance away from its consumer when
1577
+ # the tree is arranged.
1551
1578
  self.group_input_splits = [
1552
- {"name": f"Group Input.{index:03d}", "links": by_consumer[consumer]}
1553
- for index, consumer in enumerate(list(by_consumer)[1:], start=1)
1579
+ {
1580
+ "name": socket_names(by_consumer[consumer]),
1581
+ "label": socket_names(by_consumer[consumer]),
1582
+ "parent": parent_frame(consumer),
1583
+ "links": by_consumer[consumer],
1584
+ }
1585
+ for consumer in list(by_consumer)[1:]
1554
1586
  ]
1555
1587
  self._hide_unused_input_sockets()
1556
1588
 
@@ -812,6 +812,15 @@ def _make_var(label: str, counter: dict[str, int]) -> str:
812
812
  return f"{base}_{counter[base]}"
813
813
 
814
814
 
815
+ def _node_var_label(node) -> str:
816
+ """The stem for a node's generated variable name. A group node's
817
+ ``bl_label`` is just "Group", so it reads as its group's name instead
818
+ (``inverse_mass = InverseMass()``)."""
819
+ if node.bl_idname in _GROUP_BASES and node.node_tree is not None:
820
+ return node.node_tree.name
821
+ return node.bl_label or "node"
822
+
823
+
815
824
  # ---------------------------------------------------------------------------
816
825
  # Graph utilities
817
826
  # ---------------------------------------------------------------------------
@@ -1203,7 +1212,7 @@ class EmitContext:
1203
1212
  vector feeding ``vec.x`` / ``vec.y``). The assignment is queued on
1204
1213
  ``pending_lines`` and flushed before the consuming statement.
1205
1214
  """
1206
- label = link.from_socket.name or link.from_node.bl_label or "value"
1215
+ label = link.from_socket.name or _node_var_label(link.from_node)
1207
1216
  var = _make_var(label, self.counter)
1208
1217
  self.pending_lines.append(f" {var} = {expr.render()}")
1209
1218
  ref = Ref(var)
@@ -3930,6 +3939,8 @@ def _group_input_split_lines(
3930
3939
  parent = _fmt(node.parent.name) if node.parent is not None else "None"
3931
3940
  lines.append(f"{indent} {{")
3932
3941
  lines.append(f'{indent} "name": {_fmt(node.name)},')
3942
+ if node.label:
3943
+ lines.append(f'{indent} "label": {_fmt(node.label)},')
3933
3944
  lines.append(f'{indent} "location": {_fmt(loc)},')
3934
3945
  lines.append(f'{indent} "parent": {parent},')
3935
3946
  lines.append(f'{indent} "links": [')
@@ -4489,7 +4500,7 @@ def _emit_tree(node_tree, collector: _GroupCollector) -> _TreeEmission:
4489
4500
  f"Cannot generate code: {message}. Register an emitter "
4490
4501
  "with register_emitter() or pass strict=False."
4491
4502
  )
4492
- var = _make_var(node.bl_label or "node", ctx.counter)
4503
+ var = _make_var(_node_var_label(node), ctx.counter)
4493
4504
  if not ctx.outgoing.get(name):
4494
4505
  var = "_" + var
4495
4506
  ctx.var_map[name] = _Val(Ref(var))
@@ -4575,7 +4586,7 @@ def _emit_tree(node_tree, collector: _GroupCollector) -> _TreeEmission:
4575
4586
  consumed_out_links.add(id(link))
4576
4587
  continue
4577
4588
 
4578
- var = _make_var(node.bl_label or "node", ctx.counter)
4589
+ var = _make_var(_node_var_label(node), ctx.counter)
4579
4590
  # A node created purely for its side effect (a gizmo, a dangling
4580
4591
  # node) is never referenced again — prefix its variable so the
4581
4592
  # generated module passes lint (F841).
File without changes