nodebpy 520.29.0__tar.gz → 520.30.1__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 (92) hide show
  1. {nodebpy-520.29.0 → nodebpy-520.30.1}/PKG-INFO +1 -1
  2. {nodebpy-520.29.0 → nodebpy-520.30.1}/pyproject.toml +1 -4
  3. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/__init__.py +2 -0
  4. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/assets/_codegen.py +81 -6
  5. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/_utils.py +3 -2
  6. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/asset.py +1 -1
  7. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/items.py +1 -1
  8. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/layout.py +9 -7
  9. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/mixins.py +37 -66
  10. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/node.py +11 -5
  11. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/socket.py +45 -11
  12. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/tree.py +44 -21
  13. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/export/codegen.py +122 -19
  14. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/export/diagram.py +6 -3
  15. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/export/parity.py +5 -5
  16. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/export/web_render.py +3 -3
  17. nodebpy-520.30.1/src/nodebpy/lib/__init__.py +0 -0
  18. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/VENDORED.md +8 -0
  19. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/graph.py +20 -4
  20. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/realize.py +5 -1
  21. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +4 -2
  22. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/config.py +4 -2
  23. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/live.py +2 -0
  24. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/_mixins.py +8 -5
  25. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/color.py +30 -15
  26. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/converter.py +97 -34
  27. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/distort.py +21 -21
  28. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/filter.py +53 -67
  29. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/group.py +1 -1
  30. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/input.py +10 -10
  31. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/interface.py +10 -10
  32. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/manual.py +22 -34
  33. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/matte.py +7 -7
  34. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/output.py +5 -5
  35. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/assets.py +28 -27
  36. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/attribute.py +9 -7
  37. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/color.py +1 -1
  38. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/converter.py +1964 -444
  39. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/geometry.py +419 -320
  40. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/grid.py +23 -21
  41. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/group.py +1 -1
  42. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/groups.py +7 -7
  43. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/input.py +74 -74
  44. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/interface.py +30 -30
  45. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/manual.py +172 -419
  46. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/output.py +18 -18
  47. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/texture.py +76 -51
  48. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/vector.py +47 -14
  49. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/zone.py +10 -7
  50. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/converter.py +127 -14
  51. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/grid.py +3 -3
  52. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/group.py +1 -1
  53. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/input.py +28 -28
  54. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/manual.py +7 -17
  55. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/output.py +14 -14
  56. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/script.py +22 -9
  57. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/shader.py +43 -37
  58. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/texture.py +114 -36
  59. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/vector.py +56 -21
  60. nodebpy-520.30.1/src/nodebpy/py.typed +0 -0
  61. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/types.py +62 -1
  62. {nodebpy-520.29.0 → nodebpy-520.30.1}/README.md +0 -0
  63. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/assets/__init__.py +0 -0
  64. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/assets/__main__.py +0 -0
  65. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/assets/_library.py +0 -0
  66. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/assets/_pipeline.py +0 -0
  67. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/__init__.py +0 -0
  68. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/_registry.py +0 -0
  69. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/_socket_order.py +0 -0
  70. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/builder/accessor.py +0 -0
  71. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/export/__init__.py +0 -0
  72. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/export/_floats.py +0 -0
  73. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/export/plot.py +0 -0
  74. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  75. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  76. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/balancing.py +0 -0
  77. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  78. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  79. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  80. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  81. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  82. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  83. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  84. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/__init__.py +0 -0
  85. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/__init__.py +1 -1
  86. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/assets.py +0 -0
  87. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/compositor/vector.py +0 -0
  88. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/__init__.py +1 -1
  89. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  90. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/__init__.py +0 -0
  91. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/assets.py +0 -0
  92. {nodebpy-520.29.0 → nodebpy-520.30.1}/src/nodebpy/nodes/shader/color.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.29.0
3
+ Version: 520.30.1
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.29.0"
3
+ version = "520.30.1"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -9,9 +9,6 @@ authors = [
9
9
  requires-python = ">=3.13"
10
10
  license = { text = "GPL-3.0-or-later" }
11
11
 
12
- [project.scripts]
13
- nodebpy = "nodebpy:main"
14
-
15
12
  [project.optional-dependencies]
16
13
  networkx = [
17
14
  "networkx>=3.6.1",
@@ -9,9 +9,11 @@ from .builder import (
9
9
  default_sugiyama_options,
10
10
  )
11
11
  from .nodes import compositor, geometry, shader
12
+ from .types import Default
12
13
 
13
14
  __all__ = [
14
15
  "ArrangeMethod",
16
+ "Default",
15
17
  "SimpleOptions",
16
18
  "SugiyamaOptions",
17
19
  "TreeBuilder",
@@ -24,6 +24,7 @@ import bpy
24
24
  from ..builder import AssetLibrary, BundledLibrary, asset_group_base
25
25
  from ..builder._utils import normalize_name, typed_param_names
26
26
  from ..export.codegen import GroupInterface, _fmt
27
+ from ..types import Default
27
28
 
28
29
  # bl_socket_type substring → (Socket accessor class, Input* parameter type).
29
30
  # Order matters: more specific keys (IntVector before Int) come first.
@@ -143,12 +144,29 @@ class _Socket:
143
144
  attr: str # normalized accessor/param name
144
145
  description: str = "" # interface tooltip, if the asset author set one
145
146
  menu_items: tuple[str, ...] = () # menu sockets only: the selectable items
147
+ fallback: str = "" # what an unconnected input reads, for the docstring
148
+ modifier_attribute: str = "" # attribute a modifier input defaults to reading
146
149
 
147
150
  @property
148
151
  def doc(self) -> str:
149
152
  """Documentation line for this socket — its tooltip, else its name."""
150
153
  return _clean_doc(self.description or self.name)
151
154
 
155
+ @property
156
+ def param_doc(self) -> str:
157
+ """The parameter's documentation line: the tooltip plus, for an input
158
+ with a fallback, what it reads when nothing is connected, and for one
159
+ with a default attribute, what a modifier input reads by default."""
160
+ doc = self.doc
161
+ if self.fallback:
162
+ doc = f"{doc.rstrip('.')}. When unconnected: {self.fallback}"
163
+ if self.modifier_attribute:
164
+ doc = (
165
+ f"{doc.rstrip('.')}. As a modifier input, reads the "
166
+ f'"{self.modifier_attribute}" attribute by default.'
167
+ )
168
+ return doc
169
+
152
170
  @property
153
171
  def param_type(self) -> str:
154
172
  """Type hint for the ``__init__`` parameter.
@@ -179,13 +197,22 @@ def _collect(
179
197
  sockets,
180
198
  descriptions: dict[str, str] | None = None,
181
199
  menus: bool = False,
200
+ fallbacks: dict[str, Default] | None = None,
201
+ modifier_attributes: dict[str, str] | None = None,
182
202
  ) -> list[_Socket]:
183
203
  """Introspect ``sockets`` into records.
184
204
 
185
205
  ``menus`` resolves menu sockets to their items — only worth doing for the
186
- group's *inputs*, whose parameters are typed from them.
206
+ group's *inputs*, whose parameters are typed from them. ``fallbacks`` maps
207
+ an input's identifier to what it reads when unconnected (the interface's
208
+ ``default_input`` field); such a parameter defaults to the matching
209
+ ``Default`` member instead of a stored value. ``modifier_attributes``
210
+ maps an input to its default attribute name, which only a Geometry Nodes
211
+ modifier uses, so it is documented but does not change the default.
187
212
  """
188
213
  descriptions = descriptions or {}
214
+ fallbacks = fallbacks or {}
215
+ modifier_attributes = modifier_attributes or {}
189
216
  # Keep inactive sockets: socket-usage inference deactivates inputs that the
190
217
  # current node options (e.g. a menu selection) leave unused, but a caller
191
218
  # may set those options differently, so the API must expose every input.
@@ -202,21 +229,58 @@ def _collect(
202
229
  attr = (
203
230
  norm_name if name_counts[norm_name] == 1 else normalize_name(s.identifier)
204
231
  )
232
+ fallback = fallbacks.get(s.identifier)
205
233
  out.append(
206
234
  _Socket(
207
235
  name=s.name,
208
236
  identifier=s.identifier,
209
237
  socket_class=socket_class,
210
238
  input_type=input_type,
211
- default=_format_default(s),
239
+ default=repr(fallback) if fallback else _format_default(s),
212
240
  attr=attr,
213
241
  description=descriptions.get(s.identifier, ""),
214
242
  menu_items=menu_items,
243
+ fallback=fallback.description if fallback else "",
244
+ modifier_attribute=modifier_attributes.get(s.identifier, ""),
215
245
  )
216
246
  )
217
247
  return out
218
248
 
219
249
 
250
+ def _interface_fallbacks(group) -> dict[str, Default]:
251
+ """What each group input reads when unconnected, by socket identifier.
252
+
253
+ An interface input with a ``default_input`` other than ``VALUE`` reads an
254
+ implicit field or context value instead of its stored default, whether
255
+ the group is a modifier or a node in another tree. Other inputs are left
256
+ out.
257
+ """
258
+ fallbacks: dict[str, Default] = {}
259
+ for item in group.interface.items_tree:
260
+ if item.item_type != "SOCKET" or item.in_out != "INPUT":
261
+ continue
262
+ default_input = getattr(item, "default_input", "VALUE")
263
+ if default_input != "VALUE":
264
+ fallbacks[item.identifier] = Default(default_input)
265
+ return fallbacks
266
+
267
+
268
+ def _interface_modifier_attributes(group) -> dict[str, str]:
269
+ """Each group input's default attribute name, by socket identifier.
270
+
271
+ Blender only applies it when the group is a Geometry Nodes modifier (the
272
+ input starts in attribute mode reading that name); as a node inside
273
+ another tree the input still uses its stored value.
274
+ """
275
+ return {
276
+ item.identifier: item.default_attribute_name
277
+ for item in group.interface.items_tree
278
+ if item.item_type == "SOCKET"
279
+ and item.in_out == "INPUT"
280
+ and getattr(item, "default_attribute_name", "")
281
+ }
282
+
283
+
220
284
  def _introspect_group(group, name: str, library_source: str) -> _AssetClass:
221
285
  """Introspect the node group ``group`` (a ``bpy.types.NodeTree``) into an
222
286
  :class:`_AssetClass` record by instantiating it on a throwaway host node.
@@ -253,7 +317,13 @@ def _introspect_group(group, name: str, library_source: str) -> _AssetClass:
253
317
  description=(group.description or name).strip(),
254
318
  library_source=library_source,
255
319
  tree_idname=group.bl_idname,
256
- inputs=_collect(node.inputs, descriptions, menus=True),
320
+ inputs=_collect(
321
+ node.inputs,
322
+ descriptions,
323
+ menus=True,
324
+ fallbacks=_interface_fallbacks(group),
325
+ modifier_attributes=_interface_modifier_attributes(group),
326
+ ),
257
327
  outputs=_collect(node.outputs, descriptions),
258
328
  )
259
329
  finally:
@@ -320,7 +390,7 @@ def _class_docstring(cls: _AssetClass) -> str:
320
390
  if cls.inputs:
321
391
  lines += ["Parameters", "----------"]
322
392
  for s in cls.inputs:
323
- lines += [f"{s.attr} : {s.param_type}", f" {s.doc}"]
393
+ lines += [f"{s.attr} : {s.param_type}", f" {s.param_doc}"]
324
394
  lines.append("")
325
395
  lines += ["Inputs", "------"]
326
396
  for s in cls.inputs:
@@ -369,7 +439,7 @@ def _render_class(cls: _AssetClass, docstrings: bool = False) -> str:
369
439
  @property
370
440
  def o(self) -> _Outputs: ...
371
441
 
372
- def __init__{signature}:
442
+ def __init__{signature}{" -> None" if signature == "(self)" else ""}:
373
443
  super().__init__(**{{{key_args}}})
374
444
  """
375
445
 
@@ -428,7 +498,8 @@ def interface_parts(
428
498
  args.append("**{" + ", ".join(keyed) + "}")
429
499
  if pairs:
430
500
  args.append("_named_links=[" + ", ".join(pairs) + "]")
431
- init = f" def __init__{signature}:\n super().__init__({', '.join(args)})"
501
+ init_return = " -> None" if signature == "(self)" else ""
502
+ init = f" def __init__{signature}{init_return}:\n super().__init__({', '.join(args)})"
432
503
 
433
504
  type_checking = (
434
505
  " if TYPE_CHECKING:\n"
@@ -469,6 +540,8 @@ def interface_parts(
469
540
  ]
470
541
  if any("math." in s.default for s in cls.inputs):
471
542
  import_lines.insert(0, "import math")
543
+ if any(s.default.startswith("Default.") for s in cls.inputs):
544
+ input_types.insert(0, "Default") # sorts before the Input* names
472
545
  if input_types:
473
546
  import_lines.append(f"from {nodebpy_pkg}.types import {', '.join(input_types)}")
474
547
 
@@ -489,6 +562,8 @@ def _render_module(
489
562
  {s.socket_class for c in classes for s in c.inputs + c.outputs}
490
563
  )
491
564
  input_types = sorted({s.input_type for c in classes for s in c.inputs})
565
+ if any(s.default.startswith("Default.") for c in classes for s in c.inputs):
566
+ input_types.insert(0, "Default")
492
567
  bases = sorted({asset_group_base(c.tree_idname).__name__ for c in classes})
493
568
  libraries = sorted(
494
569
  {
@@ -178,9 +178,10 @@ def _resolve_promotion(
178
178
 
179
179
  @runtime_checkable
180
180
  class _NodeLike(Protocol):
181
- """Protocol for objects that wrap a Blender node and expose an ``outputs`` accessor."""
181
+ """Protocol for objects that wrap a Blender node and expose an ``o`` output accessor."""
182
182
 
183
- outputs: Any # SocketAccessor at runtime; typed as Any to avoid circular import
183
+ @property
184
+ def o(self) -> Any: ... # SocketAccessor at runtime; Any avoids a circular import
184
185
 
185
186
 
186
187
  @runtime_checkable
@@ -111,7 +111,7 @@ class _AssetGroupMixin:
111
111
  return cls._build_group is not _AssetGroupMixin._build_group
112
112
 
113
113
  @classmethod
114
- def create_group(cls):
114
+ def create_group(cls) -> Any:
115
115
  """Append the named asset group from the library and return its tree,
116
116
  reusing a previously-appended group of the same name and tree type.
117
117
 
@@ -200,7 +200,7 @@ class ItemsMixin(DynamicInputsMixin):
200
200
 
201
201
  if TYPE_CHECKING:
202
202
  node: Node
203
- tree: TreeBuilder
203
+ tree: TreeBuilder[Any]
204
204
 
205
205
  def _establish_links(self, **kwargs: Any) -> None: ...
206
206
 
@@ -39,7 +39,9 @@ def _is_layoutable(node: bpy.types.Node) -> bool:
39
39
 
40
40
  def build_dependency_graph(
41
41
  tree: bpy.types.NodeTree,
42
- ) -> tuple[dict[bpy.types.Node, set[bpy.types.Node]], Counter]:
42
+ ) -> tuple[
43
+ dict[bpy.types.Node, set[bpy.types.Node]], Counter[bpy.types.NodeSocket | None]
44
+ ]:
43
45
  """Build a graph of node dependencies and count input connections.
44
46
 
45
47
  Only layoutable nodes (excluding frames and reroutes) are included.
@@ -48,7 +50,7 @@ def build_dependency_graph(
48
50
  dependency_graph: dict[bpy.types.Node, set[bpy.types.Node]] = {
49
51
  node: set() for node in layoutable
50
52
  }
51
- socket_input_connection_count: Counter = Counter()
53
+ socket_input_connection_count: Counter[bpy.types.NodeSocket | None] = Counter()
52
54
 
53
55
  for link in tree.links:
54
56
  if link.from_node in layoutable and link.to_node in layoutable:
@@ -181,7 +183,7 @@ def _socket_visible(socket: bpy.types.NodeSocket) -> bool:
181
183
 
182
184
  def _is_expanded_vector(
183
185
  socket: bpy.types.NodeSocket,
184
- socket_input_connection_count: Counter | None,
186
+ socket_input_connection_count: Counter[bpy.types.NodeSocket | None] | None,
185
187
  ) -> bool:
186
188
  """Whether an input draws the expanded 3-component vector widget: an
187
189
  unlinked vector whose value is shown (``hide_value`` sockets, such as
@@ -266,7 +268,7 @@ def _interface_panels(node: bpy.types.Node) -> tuple[list[Any], dict[int, bool]]
266
268
 
267
269
  def node_rows(
268
270
  node: bpy.types.Node,
269
- socket_input_connection_count: Counter | None = None,
271
+ socket_input_connection_count: Counter[bpy.types.NodeSocket | None] | None = None,
270
272
  ) -> list[NodeRow]:
271
273
  """The rows Blender draws below an expanded node's header, top to bottom.
272
274
 
@@ -376,7 +378,7 @@ def node_rows(
376
378
  def _rows_from_order(
377
379
  node: bpy.types.Node,
378
380
  order: tuple[tuple, ...],
379
- socket_input_connection_count: Counter | None,
381
+ socket_input_connection_count: Counter[bpy.types.NodeSocket | None] | None,
380
382
  ) -> list[NodeRow]:
381
383
  """Rows of a node drawn from its declaration (see ``_socket_order``)."""
382
384
  rows: list[NodeRow] = []
@@ -584,7 +586,7 @@ def _rows_from_order(
584
586
 
585
587
  def calculate_node_dimensions(
586
588
  node: bpy.types.Node,
587
- socket_input_connection_count: Counter | None = None,
589
+ socket_input_connection_count: Counter[bpy.types.NodeSocket | None] | None = None,
588
590
  interface_scale: float = 1.0,
589
591
  ) -> tuple[float, float]:
590
592
  """Calculate the visual dimensions of a node.
@@ -744,7 +746,7 @@ def _reduce_crossings(
744
746
 
745
747
  def position_nodes_in_columns(
746
748
  columns: list[list[bpy.types.Node]],
747
- connection_counts: Counter,
749
+ connection_counts: Counter[bpy.types.NodeSocket | None],
748
750
  spacing: tuple[float, float] = (50, 25),
749
751
  ) -> None:
750
752
  """Position nodes column-by-column with the given spacing.
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Self, TypeVar, cast, overload
5
5
 
6
6
  from bpy.types import NodeLink, NodeSocket
7
7
 
8
+ from ..types import Default
8
9
  from ._registry import _wrap_socket
9
10
  from ._utils import SocketError, _resolve_promotion, _SocketLike
10
11
 
@@ -12,13 +13,13 @@ _RShiftT = TypeVar("_RShiftT")
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from ..nodes.geometry import CombineTransform
16
+ from ..nodes.geometry.converter import BooleanMath
15
17
  from ..types import InputLinkable
16
18
  from .accessor import SocketAccessor
17
19
  from .node import BaseNode
18
20
  from .socket import (
19
21
  BooleanSocket,
20
22
  FloatSocket,
21
- IntegerSocket,
22
23
  MatrixSocket,
23
24
  Position,
24
25
  Socket,
@@ -44,74 +45,65 @@ class OperatorMixin:
44
45
 
45
46
  def _apply_math_operation(
46
47
  self, other: Any, operation: str, reverse: bool = False
47
- ) -> FloatSocket | VectorSocket | IntegerSocket:
48
+ ) -> Socket:
48
49
  socket, other, reverse = _resolve_promotion(
49
50
  self._default_output_socket,
50
51
  other,
51
52
  reverse,
52
53
  )
53
- return cast(
54
- "FloatSocket | VectorSocket | IntegerSocket",
55
- _wrap_socket(socket)._dispatch_math(other, operation, reverse),
56
- )
54
+ return _wrap_socket(socket)._dispatch_math(other, operation, reverse)
57
55
 
58
- def __mul__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
56
+ def __mul__(self, other: Any) -> Socket:
59
57
  return self._apply_math_operation(other, "multiply")
60
58
 
61
- def __rmul__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
59
+ def __rmul__(self, other: Any) -> Socket:
62
60
  return self._apply_math_operation(other, "multiply", reverse=True)
63
61
 
64
- def __truediv__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
62
+ def __truediv__(self, other: Any) -> Socket:
65
63
  return self._apply_math_operation(other, "divide")
66
64
 
67
- def __rtruediv__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
65
+ def __rtruediv__(self, other: Any) -> Socket:
68
66
  return self._apply_math_operation(other, "divide", reverse=True)
69
67
 
70
- def __add__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
68
+ def __add__(self, other: Any) -> Socket:
71
69
  return self._apply_math_operation(other, "add")
72
70
 
73
- def __radd__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
71
+ def __radd__(self, other: Any) -> Socket:
74
72
  return self._apply_math_operation(other, "add", reverse=True)
75
73
 
76
- def __sub__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
74
+ def __sub__(self, other: Any) -> Socket:
77
75
  return self._apply_math_operation(other, "subtract")
78
76
 
79
- def __rsub__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
77
+ def __rsub__(self, other: Any) -> Socket:
80
78
  return self._apply_math_operation(other, "subtract", reverse=True)
81
79
 
82
- def __pow__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
80
+ def __pow__(self, other: Any) -> Socket:
83
81
  return self._apply_math_operation(other, "power")
84
82
 
85
- def __rpow__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
83
+ def __rpow__(self, other: Any) -> Socket:
86
84
  return self._apply_math_operation(other, "power", reverse=True)
87
85
 
88
- def __mod__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
86
+ def __mod__(self, other: Any) -> Socket:
89
87
  return self._apply_math_operation(other, "modulo")
90
88
 
91
- def __rmod__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
89
+ def __rmod__(self, other: Any) -> Socket:
92
90
  return self._apply_math_operation(other, "modulo", reverse=True)
93
91
 
94
- def __floordiv__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
92
+ def __floordiv__(self, other: Any) -> Socket:
95
93
  socket, other, reverse = _resolve_promotion(
96
94
  self._default_output_socket,
97
95
  other,
98
96
  False,
99
97
  )
100
- return cast(
101
- "FloatSocket | VectorSocket | IntegerSocket",
102
- _wrap_socket(socket)._dispatch_floordiv(other, reverse),
103
- )
98
+ return _wrap_socket(socket)._dispatch_floordiv(other, reverse)
104
99
 
105
- def __rfloordiv__(self, other: Any) -> FloatSocket | VectorSocket | IntegerSocket:
100
+ def __rfloordiv__(self, other: Any) -> Socket:
106
101
  socket, other, reverse = _resolve_promotion(
107
102
  self._default_output_socket,
108
103
  other,
109
104
  True,
110
105
  )
111
- return cast(
112
- "FloatSocket | VectorSocket | IntegerSocket",
113
- _wrap_socket(socket)._dispatch_floordiv(other, reverse),
114
- )
106
+ return _wrap_socket(socket)._dispatch_floordiv(other, reverse)
115
107
 
116
108
  def __matmul__(
117
109
  self, other: Position | CombineTransform | VectorSocket | MatrixSocket
@@ -143,36 +135,11 @@ class OperatorMixin:
143
135
 
144
136
  return MultiplyMatrices(other, self).o.matrix # ty: ignore[invalid-argument-type]
145
137
 
146
- def __neg__(self) -> FloatSocket | VectorSocket | IntegerSocket:
147
- return cast(
148
- "FloatSocket | VectorSocket | IntegerSocket",
149
- _wrap_socket(self._default_output_socket)._dispatch_unary("negate"),
150
- )
151
-
152
- def __abs__(self) -> FloatSocket | VectorSocket | IntegerSocket:
153
- return cast(
154
- "FloatSocket | VectorSocket | IntegerSocket",
155
- _wrap_socket(self._default_output_socket)._dispatch_unary("absolute"),
156
- )
157
-
158
- if TYPE_CHECKING:
138
+ def __neg__(self) -> Socket:
139
+ return _wrap_socket(self._default_output_socket)._dispatch_unary("negate")
159
140
 
160
- def __mul__(self, other: Any) -> Self: ...
161
- def __rmul__(self, other: Any) -> Self: ...
162
- def __truediv__(self, other: Any) -> Self: ...
163
- def __rtruediv__(self, other: Any) -> Self: ...
164
- def __add__(self, other: Any) -> Self: ...
165
- def __radd__(self, other: Any) -> Self: ...
166
- def __sub__(self, other: Any) -> Self: ...
167
- def __rsub__(self, other: Any) -> Self: ...
168
- def __pow__(self, other: Any) -> Self: ...
169
- def __rpow__(self, other: Any) -> Self: ...
170
- def __mod__(self, other: Any) -> Self: ...
171
- def __rmod__(self, other: Any) -> Self: ...
172
- def __floordiv__(self, other: Any) -> Self: ...
173
- def __rfloordiv__(self, other: Any) -> Self: ...
174
- def __neg__(self) -> Self: ...
175
- def __abs__(self) -> Self: ...
141
+ def __abs__(self) -> Socket:
142
+ return _wrap_socket(self._default_output_socket)._dispatch_unary("absolute")
176
143
 
177
144
  def _apply_compare_operation(
178
145
  self, other: Any, operation: str
@@ -202,36 +169,36 @@ class OperatorMixin:
202
169
  def __ne__(self, other: object) -> FloatSocket | BooleanSocket: # type: ignore
203
170
  return self._apply_compare_operation(other, "not_equal")
204
171
 
205
- def _apply_boolean_operation(self, other: Any, operation: str):
172
+ def _apply_boolean_operation(self, other: Any, operation: str) -> BooleanMath:
206
173
  from ..nodes.geometry.converter import BooleanMath
207
174
 
208
175
  return getattr(BooleanMath, operation)(self, other)
209
176
 
210
- def __and__(self, other: Any):
177
+ def __and__(self, other: Any) -> BooleanMath | Socket:
211
178
  return self._apply_boolean_operation(other, "l_and")
212
179
 
213
- def __rand__(self, other: Any):
180
+ def __rand__(self, other: Any) -> BooleanMath | Socket:
214
181
  from ..nodes.geometry.converter import BooleanMath
215
182
 
216
183
  return BooleanMath.l_and(other, cast(Any, self))
217
184
 
218
- def __or__(self, other: Any):
185
+ def __or__(self, other: Any) -> BooleanMath | Socket:
219
186
  return self._apply_boolean_operation(other, "l_or")
220
187
 
221
- def __ror__(self, other: Any):
188
+ def __ror__(self, other: Any) -> BooleanMath | Socket:
222
189
  from ..nodes.geometry.converter import BooleanMath
223
190
 
224
191
  return BooleanMath.l_or(other, cast(Any, self))
225
192
 
226
- def __xor__(self, other: Any):
193
+ def __xor__(self, other: Any) -> BooleanMath | Socket:
227
194
  return self._apply_boolean_operation(other, "not_equal")
228
195
 
229
- def __rxor__(self, other: Any):
196
+ def __rxor__(self, other: Any) -> BooleanMath | Socket:
230
197
  from ..nodes.geometry.converter import BooleanMath
231
198
 
232
199
  return BooleanMath.not_equal(other, cast(Any, self))
233
200
 
234
- def __invert__(self):
201
+ def __invert__(self) -> BooleanMath | Socket:
235
202
  from ..nodes.geometry.converter import BooleanMath
236
203
 
237
204
  return BooleanMath.l_not(cast(Any, self))
@@ -244,7 +211,7 @@ class LinkingMixin:
244
211
  and ``_default_input_socket`` on the concrete class.
245
212
  """
246
213
 
247
- tree: TreeBuilder
214
+ tree: TreeBuilder[Any]
248
215
 
249
216
  if TYPE_CHECKING:
250
217
  import bpy
@@ -262,6 +229,8 @@ class LinkingMixin:
262
229
 
263
230
  def _source_socket(self, node: InputLinkable | Socket | NodeSocket) -> NodeSocket:
264
231
  assert node is not None
232
+ if isinstance(node, Default):
233
+ raise TypeError(f"{node} marks an input's fallback, it cannot be linked")
265
234
  if isinstance(node, NodeSocket):
266
235
  return node
267
236
  elif hasattr(node, "_default_output_socket"):
@@ -271,6 +240,8 @@ class LinkingMixin:
271
240
 
272
241
  def _target_socket(self, node: InputLinkable | Socket | NodeSocket) -> NodeSocket:
273
242
  assert node is not None
243
+ if isinstance(node, Default):
244
+ raise TypeError(f"{node} marks an input's fallback, it cannot be linked")
274
245
  if isinstance(node, NodeSocket):
275
246
  return node
276
247
  elif hasattr(node, "_default_input_socket"):
@@ -25,7 +25,7 @@ from bpy.types import (
25
25
  ShaderNodeTree,
26
26
  )
27
27
 
28
- from ..types import SOCKET_COMPATIBILITY, InputAny
28
+ from ..types import SOCKET_COMPATIBILITY, Default, InputAny
29
29
  from ._utils import SocketError, _NodeLike, _SocketLike
30
30
  from .accessor import SocketAccessor
31
31
  from .mixins import LinkingMixin, OperatorMixin
@@ -91,7 +91,8 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
91
91
  """Base class for all node wrappers."""
92
92
 
93
93
  _bl_idname: str
94
- _tree: TreeBuilder
94
+ _tree: TreeBuilder[Any]
95
+ node: Node
95
96
  _default_input_id: str | None = None
96
97
  _default_output_id: str | None = None
97
98
  _placeholder_inputs: list[str]
@@ -113,7 +114,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
113
114
  self.node = node if node else self._tree.add(self.__class__._bl_idname)
114
115
 
115
116
  @property
116
- def tree(self) -> TreeBuilder:
117
+ def tree(self) -> TreeBuilder[Any]:
117
118
  """The `TreeBuilder` instance this node belongs to and is being built within."""
118
119
  return self._tree
119
120
 
@@ -207,6 +208,11 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
207
208
  lets callers address one of several same-named sockets unambiguously.
208
209
  """
209
210
  named = isinstance(target, str)
211
+ # A ``Default`` member stands for the socket's own fallback (an implicit
212
+ # field or context value Blender reads when nothing is linked): nothing to
213
+ # set or link, the same as ``None``.
214
+ if isinstance(value, Default):
215
+ return
210
216
  # TODO: don't like these manual overrides for particular nodes, but best I can do for now
211
217
  if value is None or (
212
218
  named
@@ -231,7 +237,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
231
237
  self._link_from(value, target)
232
238
  elif isinstance(value, _NodeLike):
233
239
  target_type = target.type if not named else self.i._get(target).type
234
- self._link_from(value.o._best_match(target_type), target) # type: ignore
240
+ self._link_from(value.o._best_match(target_type), target)
235
241
  else:
236
242
  # TODO: explicitly skipping the sockets for BooleanMath as they are default false,
237
243
  # but this needs to be a more generic solution for sockets which aren't available
@@ -422,7 +428,7 @@ class NodeGroupBuilder[T: bpy.types.NodeTree](BaseNode, ABC):
422
428
  # fresh tree's defaults belong here; codegen fills it when exporting.
423
429
  _tree_properties: ClassVar[dict[str, Any]] = {}
424
430
 
425
- def __init__(self, **kwargs):
431
+ def __init__(self, **kwargs: Any):
426
432
  super().__init__()
427
433
  self._setup_node_group()
428
434
  self.node.show_options = False