nodebpy 520.2.0__tar.gz → 520.4.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 (81) hide show
  1. {nodebpy-520.2.0 → nodebpy-520.4.0}/PKG-INFO +1 -1
  2. {nodebpy-520.2.0 → nodebpy-520.4.0}/pyproject.toml +2 -2
  3. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/__init__.py +3 -1
  4. nodebpy-520.4.0/src/nodebpy/assets/__main__.py +113 -0
  5. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/assets/_codegen.py +21 -8
  6. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/_utils.py +16 -2
  7. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/asset.py +1 -1
  8. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/mixins.py +12 -2
  9. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/socket.py +115 -4
  10. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/tree.py +3 -1
  11. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/export/codegen.py +332 -64
  12. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +1 -1
  13. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/_mixins.py +1 -1
  14. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/assets.py +2 -2
  15. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/assets.py +2 -2
  16. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/converter.py +3 -3
  17. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/groups.py +8 -9
  18. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/manual.py +4 -5
  19. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/zone.py +2 -2
  20. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/assets.py +2 -2
  21. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/types.py +2 -3
  22. nodebpy-520.2.0/src/nodebpy/assets/__main__.py +0 -57
  23. {nodebpy-520.2.0 → nodebpy-520.4.0}/README.md +0 -0
  24. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/assets/__init__.py +0 -0
  25. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/__init__.py +0 -0
  26. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/_registry.py +0 -0
  27. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/accessor.py +0 -0
  28. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/arrange.py +0 -0
  29. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/items.py +0 -0
  30. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/builder/node.py +0 -0
  31. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/export/__init__.py +0 -0
  32. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/export/diagram.py +0 -0
  33. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  34. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  35. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  36. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  37. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  38. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  39. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  40. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  41. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  42. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  43. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  44. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  45. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/__init__.py +0 -0
  46. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  47. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  48. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  49. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  50. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  51. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  52. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  53. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  54. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
  55. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  56. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  57. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  58. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/__init__.py +208 -208
  59. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  60. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  61. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
  62. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  63. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  64. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  65. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  66. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  67. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  68. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  69. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  70. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  71. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/color.py +0 -0
  72. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  73. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  74. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/group.py +0 -0
  75. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/input.py +0 -0
  76. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  77. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/output.py +0 -0
  78. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/script.py +0 -0
  79. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  80. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  81. {nodebpy-520.2.0 → nodebpy-520.4.0}/src/nodebpy/nodes/shader/vector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.2.0
3
+ Version: 520.4.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.2.0"
3
+ version = "520.4.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -42,5 +42,5 @@ dev = [
42
42
  "syrupy>=5.0.0",
43
43
  "pytest-xdist>=3.8.0",
44
44
  "griffe<2.0.0",
45
- "ty>=0.0.35",
45
+ "ty==0.0.51",
46
46
  ]
@@ -1,4 +1,4 @@
1
- from . import export, nodes
1
+ from . import builder, export, nodes, types
2
2
  from .builder import TreeBuilder
3
3
  from .nodes import compositor, geometry, shader
4
4
 
@@ -8,5 +8,7 @@ __all__ = [
8
8
  "geometry",
9
9
  "shader",
10
10
  "export",
11
+ "types",
12
+ "builder",
11
13
  "TreeBuilder",
12
14
  ]
@@ -0,0 +1,113 @@
1
+ """Regenerate the bundled-essentials asset APIs.
2
+
3
+ ``python -m nodebpy.assets`` introspects Blender's bundled node-group asset
4
+ libraries and writes typed classes to ``nodebpy/nodes/{geometry,shader,
5
+ compositor}/assets.py``, where ``python -m gen`` re-exports them so they are
6
+ available alongside the built-in nodes (e.g. ``g.SmoothByAngle()``). Run *before*
7
+ ``python -m gen`` and through the ruff/ty post-processing (see the Makefile).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import os
14
+ from pathlib import Path
15
+
16
+ from ..builder import BundledLibrary, PackageLibrary
17
+ from ._codegen import generate_asset_api
18
+
19
+ # Bundled libraries shipped with Blender, grouped by output module. Each library
20
+ # holds node groups of a single tree type.
21
+ _ESSENTIALS: dict[str, tuple[str, ...]] = {
22
+ "geometry": (
23
+ "geometry_nodes_essentials.blend",
24
+ "geometry_nodes_dynamics_assets.blend",
25
+ "procedural_hair_node_assets.blend",
26
+ "principal_components.blend",
27
+ ),
28
+ "shader": ("shading_nodes_essentials.blend",),
29
+ "compositor": ("compositing_nodes_essentials.blend",),
30
+ }
31
+
32
+
33
+ def generate_essentials(
34
+ nodes_dir: Path, nodebpy_pkg: str = ".."
35
+ ) -> dict[str, list[str]]:
36
+ """Generate the bundled-essentials asset modules into
37
+ ``<nodes_dir>/<tree>/assets.py``; returns the class names written per tree
38
+ (libraries not present in this Blender install are skipped)."""
39
+ written: dict[str, list[str]] = {}
40
+ for tree, filenames in _ESSENTIALS.items():
41
+ libraries = [
42
+ BundledLibrary(f)
43
+ for f in filenames
44
+ if os.path.exists(BundledLibrary(f).path())
45
+ ]
46
+ if not libraries: # pragma: no cover - depends on the Blender install
47
+ print(f" {tree}: no bundled libraries present, skipping")
48
+ continue
49
+ names = generate_asset_api(
50
+ libraries,
51
+ Path(nodes_dir) / tree / "assets.py",
52
+ nodebpy_pkg=nodebpy_pkg,
53
+ )
54
+ written[tree] = names
55
+ print(f" nodes/{tree}/assets.py: {len(names)} asset classes")
56
+ return written
57
+
58
+
59
+ def parse_args() -> argparse.Namespace:
60
+ parser = argparse.ArgumentParser()
61
+ parser.add_argument(
62
+ "--blend-file",
63
+ "-b",
64
+ type=Path,
65
+ help="Optional custom .blend asset library to generate from.",
66
+ )
67
+ parser.add_argument(
68
+ "--output",
69
+ "--output-dir",
70
+ "-o",
71
+ dest="output",
72
+ type=Path,
73
+ help="Path of the assets.py module to write.",
74
+ )
75
+ parser.add_argument(
76
+ "--nodebpy-pkg",
77
+ default="nodebpy",
78
+ help=(
79
+ "Import anchor for nodebpy in the generated module. Defaults to the "
80
+ "absolute 'nodebpy'. When nodebpy is vendored inside another package, "
81
+ "pass the path that reaches it relative to the generated module's "
82
+ "package — e.g. '..lib.nodebpy'."
83
+ ),
84
+ )
85
+ return parser.parse_args()
86
+
87
+
88
+ def main() -> None: # pragma: no cover - CLI wrapper
89
+ args = parse_args()
90
+ output = (
91
+ args.output
92
+ if args.output
93
+ else Path(__file__).parent.parent / "nodes" / "custom" / "assets.py"
94
+ )
95
+
96
+ if args.blend_file is not None:
97
+ # PackageLibrary resolves ``relative`` against the generated module's
98
+ # directory (``__file__``), so express the .blend relative to the output
99
+ # module — not the CWD the command happened to run from.
100
+ blend = args.blend_file.resolve()
101
+ relative = Path(os.path.relpath(blend, output.resolve().parent)).as_posix()
102
+ generate_asset_api(
103
+ [PackageLibrary(str(output), relative)],
104
+ output,
105
+ nodebpy_pkg=args.nodebpy_pkg,
106
+ )
107
+ return
108
+
109
+ generate_essentials(Path(__file__).parent.parent / "nodes")
110
+
111
+
112
+ if __name__ == "__main__": # pragma: no cover
113
+ main()
@@ -144,7 +144,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
144
144
  path = library.path()
145
145
  library_source = _library_source(library)
146
146
 
147
- with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, _):
147
+ with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, _): # ty: ignore[invalid-context-manager]
148
148
  available = list(src.node_groups)
149
149
  wanted = [n for n in available if names is None or n in names]
150
150
 
@@ -155,7 +155,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
155
155
  # compositor "Combine Spherical" both exist) and we'd introspect the
156
156
  # wrong one. Blender renames the appended copy on a clash; that's fine,
157
157
  # we only read its interface (the emitted _asset_name uses the original).
158
- with bpy.data.libraries.load(path, link=False, assets_only=True) as (
158
+ with bpy.data.libraries.load(path, link=False, assets_only=True) as ( # ty: ignore[invalid-context-manager]
159
159
  src,
160
160
  dst,
161
161
  ):
@@ -170,7 +170,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
170
170
  "CompositorNodeTree": "CompositorNodeGroup",
171
171
  }[group.bl_idname]
172
172
  node = host.nodes.new(node_type)
173
- node.node_tree = group
173
+ node.node_tree = group # ty: ignore[unresolved-attribute]
174
174
  classes.append(
175
175
  _AssetClass(
176
176
  class_name=_class_name(name),
@@ -194,7 +194,11 @@ def _library_source(library: AssetLibrary) -> str:
194
194
  from ..builder import PackageLibrary
195
195
 
196
196
  if isinstance(library, PackageLibrary):
197
- return f"PackageLibrary(__file__, {library.relative!r})"
197
+ # Emit a plain forward-slash string literal (never ``PosixPath(...)``),
198
+ # so the generated module imports cleanly and stays cross-platform even
199
+ # when ``relative`` was passed as a ``Path``.
200
+ relative = Path(library.relative).as_posix()
201
+ return f"PackageLibrary(__file__, {relative!r})"
198
202
  raise TypeError(f"Cannot serialise asset library: {library!r}")
199
203
 
200
204
 
@@ -245,7 +249,7 @@ def _render_class(cls: _AssetClass) -> str:
245
249
  '''
246
250
 
247
251
 
248
- def _render_module(classes: list[_AssetClass]) -> str:
252
+ def _render_module(classes: list[_AssetClass], nodebpy_pkg: str = "nodebpy") -> str:
249
253
  socket_classes = sorted(
250
254
  {s.socket_class for c in classes for s in c.inputs + c.outputs}
251
255
  )
@@ -268,8 +272,8 @@ def _render_module(classes: list[_AssetClass]) -> str:
268
272
  "# Auto-generated by nodebpy.assets.generate_asset_api — do not edit manually.",
269
273
  "from typing import TYPE_CHECKING",
270
274
  "",
271
- f"from nodebpy.builder import (\n {',\n '.join(builder_imports)},\n)",
272
- f"from nodebpy.types import (\n {',\n '.join(input_types)},\n)"
275
+ f"from {nodebpy_pkg}.builder import (\n {',\n '.join(builder_imports)},\n)",
276
+ f"from {nodebpy_pkg}.types import (\n {',\n '.join(input_types)},\n)"
273
277
  if input_types
274
278
  else "",
275
279
  ]
@@ -288,6 +292,7 @@ def generate_asset_api(
288
292
  output_path: str | Path,
289
293
  *,
290
294
  names: set[str] | None = None,
295
+ nodebpy_pkg: str = "nodebpy",
291
296
  ) -> list[str]:
292
297
  """Generate typed asset classes for ``libraries`` into ``output_path``.
293
298
 
@@ -302,6 +307,12 @@ def generate_asset_api(
302
307
  The ``.py`` file to write.
303
308
  names:
304
309
  Restrict generation to these asset (node-group) names; defaults to all.
310
+ nodebpy_pkg:
311
+ Import anchor for nodebpy in the generated module. Defaults to the
312
+ absolute ``"nodebpy"``. When nodebpy is vendored inside another package,
313
+ pass the path that reaches it *relative to the generated module's
314
+ package* — e.g. ``"..vendor.nodebpy"`` — so the emitted imports stay
315
+ relative to the install/vendor location.
305
316
 
306
317
  Returns the list of generated class names.
307
318
  """
@@ -314,5 +325,7 @@ def generate_asset_api(
314
325
 
315
326
  output_path = Path(output_path)
316
327
  output_path.parent.mkdir(parents=True, exist_ok=True)
317
- output_path.write_text(_render_module(classes), encoding="utf-8")
328
+ output_path.write_text(
329
+ _render_module(classes, nodebpy_pkg=nodebpy_pkg), encoding="utf-8"
330
+ )
318
331
  return [c.class_name for c in classes]
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import keyword
4
+ import re
3
5
  from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
4
6
 
5
7
  import bpy
@@ -34,9 +36,21 @@ GEO_NODE_NAMES = (
34
36
  )
35
37
 
36
38
 
39
+ _NON_IDENTIFIER = re.compile(r"[^0-9a-z]+")
40
+
41
+
37
42
  def normalize_name(name: str) -> str:
38
- """Convert 'Geometry' or 'My Socket' to 'geometry' or 'my_socket'."""
39
- return name.lower().replace(" ", "_").replace("é", "e")
43
+ """Convert 'Geometry' or 'My Socket' to a valid lower-case Python identifier
44
+ ('geometry', 'my_socket'). Spaces, punctuation and other non-identifier
45
+ characters (e.g. the '⟂'/'(' in 'BA⟂(BC)') collapse to underscores so the
46
+ result is always usable as an attribute or parameter name."""
47
+ text = name.lower().replace("é", "e")
48
+ cleaned = _NON_IDENTIFIER.sub("_", text).strip("_")
49
+ if cleaned and cleaned[0].isdigit():
50
+ cleaned = "_" + cleaned
51
+ if keyword.iskeyword(cleaned):
52
+ cleaned += "_"
53
+ return cleaned or "_"
40
54
 
41
55
 
42
56
  def denormalize_name(attr_name: str) -> str:
@@ -93,7 +93,7 @@ class _AssetGroupMixin:
93
93
  path = cls._library.path()
94
94
  if not os.path.exists(path):
95
95
  raise FileNotFoundError(f"Asset library not found: {path}")
96
- with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, dst):
96
+ with bpy.data.libraries.load(path, link=False, assets_only=True) as (src, dst): # ty: ignore[invalid-context-manager]
97
97
  if cls._asset_name not in src.node_groups:
98
98
  raise KeyError(f"Node group {cls._asset_name!r} not found in {path}")
99
99
  dst.node_groups = [cls._asset_name]
@@ -367,7 +367,11 @@ class LinkingMixin:
367
367
  else:
368
368
  self._link(source, input)
369
369
 
370
- def __rshift__(self, other: _RShiftT) -> _RShiftT:
370
+ @overload
371
+ def __rshift__(self, other: None) -> Self: ...
372
+ @overload
373
+ def __rshift__(self, other: _RShiftT) -> _RShiftT: ...
374
+ def __rshift__(self, other: _RShiftT | None) -> _RShiftT | Self:
371
375
  """Chain nodes using >> operator. Links output to input.
372
376
 
373
377
  Usage:
@@ -377,8 +381,14 @@ class LinkingMixin:
377
381
  If the target node has an ellipsis placeholder (...), links to that specific input.
378
382
  Otherwise, finds the best compatible socket pair based on type compatibility.
379
383
 
380
- Returns the right-hand node to enable continued chaining.
384
+ Returns the right-hand node to enable continued chaining. A ``None``
385
+ target is a no-op passthrough — nothing is linked and ``self`` is
386
+ returned so an optional node can be conditionally skipped::
387
+
388
+ src >> SetPosition() >> (Transform() if trans else None) >> out
381
389
  """
390
+ if other is None:
391
+ return self
382
392
  if isinstance(other, _SocketLike):
383
393
  source = self._default_output_socket
384
394
  target = other.socket
@@ -472,7 +472,9 @@ class _FloatGridOperatorMixin(Socket):
472
472
  """Generate a mesh on the "surface" of a volume grid."""
473
473
  from ..nodes.geometry import GridToMesh
474
474
 
475
- return GridToMesh(self.socket).o.mesh
475
+ return GridToMesh(
476
+ self.socket, threshold=threshold, adaptivity=adaptivity
477
+ ).o.mesh
476
478
 
477
479
 
478
480
  class _VectorGridOperatorMixin(Socket):
@@ -675,7 +677,6 @@ class _AccumulateField(_EvaluateField[_T]):
675
677
  def _accumulate(
676
678
  self, output: Literal["leading", "trailing", "total"], group_index: InputInteger
677
679
  ) -> "_T":
678
-
679
680
  from ..nodes.geometry import AccumulateField
680
681
 
681
682
  node = getattr(
@@ -1504,16 +1505,91 @@ class _FloatMixin(BaseSocket, Generic[_IntegerResult]):
1504
1505
 
1505
1506
  return Clamp.min_max(self.socket, min, max).o.result # ty: ignore[invalid-return-type]
1506
1507
 
1508
+ def min(self, value: InputFloat = 0.0) -> Self:
1509
+ """Create Math with operation 'Minimum'. The minimum from self and value"""
1510
+ self._assert_output("min")
1511
+ return self._math.minimum(self.socket, value).o.value # ty: ignore[invalid-return-type]
1512
+
1513
+ def max(self, value: InputFloat = 1.0) -> Self:
1514
+ """Create Math with operation 'Maximum'. The maximum from self and value"""
1515
+ self._assert_output("max")
1516
+ return self._math.maximum(self.socket, value).o.value # ty: ignore[invalid-return-type]
1517
+
1518
+ def sin(self) -> Self:
1519
+ "Create a Math node with operation 'Sine'. The sine of self"
1520
+ self._assert_output("sin")
1521
+ return self._math.sine(self.socket).o.value # ty: ignore[invalid-return-type]
1522
+
1523
+ def cos(self) -> Self:
1524
+ "Create a Math node with operation 'Cosine'. The cosine of self"
1525
+ self._assert_output("cos")
1526
+ return self._math.cosine(self.socket).o.value # ty: ignore[invalid-return-type]
1527
+
1528
+ def tan(self) -> Self:
1529
+ "Create a Math node with operation 'Tangent'. The tangent of self"
1530
+ self._assert_output("tan")
1531
+ return self._math.tangent(self.socket).o.value # ty: ignore[invalid-return-type]
1532
+
1533
+ def asin(self) -> Self:
1534
+ "Create a Math node with operation 'ArcSine'. The arcsine of self"
1535
+ self._assert_output("asin")
1536
+ return self._math.arcsine(self.socket).o.value # ty: ignore[invalid-return-type]
1537
+
1538
+ def acos(self) -> Self:
1539
+ "Create a Math node with operation 'ArcCosine'. The arccosine of self"
1540
+ self._assert_output("acos")
1541
+ return self._math.arccosine(self.socket).o.value # ty: ignore[invalid-return-type]
1542
+
1543
+ def atan(self) -> Self:
1544
+ "Create a Math node with operation 'ArcTangent'. The arctangent of self"
1545
+ self._assert_output("atan")
1546
+ return self._math.arctangent(self.socket).o.value # ty: ignore[invalid-return-type]
1547
+
1548
+ def sinh(self) -> Self:
1549
+ "Create a Math node with operation 'Hyperbolic Sine'. The hyperbolic sine of self"
1550
+ self._assert_output("sinh")
1551
+ return self._math.hyperbolic_sine(self.socket).o.value # ty: ignore[invalid-return-type]
1552
+
1553
+ def cosh(self) -> Self:
1554
+ "Create a Math node with operation 'Hyperbolic Cosine'. The hyperbolic cosine of self"
1555
+ self._assert_output("cosh")
1556
+ return self._math.hyperbolic_cosine(self.socket).o.value # ty: ignore[invalid-return-type]
1557
+
1558
+ def tanh(self) -> Self:
1559
+ "Create a Math node with operation 'Hyperbolic Tangent'. The hyperbolic tangent of self"
1560
+ self._assert_output("tanh")
1561
+ return self._math.hyperbolic_tangent(self.socket).o.value # ty: ignore[invalid-return-type]
1562
+
1563
+ def exp(self) -> Self:
1564
+ "Create a Math node with operation 'Exponent'. The exponent of self"
1565
+ self._assert_output("exp")
1566
+ return self._math.exponent(self.socket).o.value # ty: ignore[invalid-return-type]
1567
+
1568
+ def snap(self, increment: InputFloat = 0.5) -> Self:
1569
+ "Create a Math node with operation 'Snap'. The snap of self"
1570
+ self._assert_output("snap")
1571
+ return self._math.snap(self.socket, increment).o.value # ty: ignore[invalid-return-type]
1572
+
1573
+ def atan2(self, value: InputFloat = 0.5) -> Self:
1574
+ "Create a Math node with operation 'ArcTan2'. The arctangent of self"
1575
+ self._assert_output("atan2")
1576
+ return self._math.arctan2(self.socket, value).o.value # ty: ignore[invalid-return-type]
1577
+
1507
1578
  def sqrt(self) -> Self:
1508
1579
  """Return the square root of this value."""
1509
1580
  self._assert_output("sqrt")
1510
1581
  return self._math.square_root(self.socket).o.value # ty: ignore[invalid-return-type]
1511
1582
 
1512
- def power(self, exponent: InputFloat) -> Self:
1583
+ def power(self, exponent: InputFloat = 2.0) -> Self:
1513
1584
  """Raise this value to *exponent*."""
1514
1585
  self._assert_output("power")
1515
1586
  return self._math.power(self.socket, exponent).o.value # ty: ignore[invalid-return-type]
1516
1587
 
1588
+ def log(self, base: InputFloat = 2.0) -> Self:
1589
+ """Return the logarithm of this value to *base*."""
1590
+ self._assert_output("log")
1591
+ return self._math.logarithm(self.socket, base).o.value # ty: ignore[invalid-return-type]
1592
+
1517
1593
  def floor(self) -> Self:
1518
1594
  """Round down to the nearest integer."""
1519
1595
  self._assert_output("floor")
@@ -1524,23 +1600,48 @@ class _FloatMixin(BaseSocket, Generic[_IntegerResult]):
1524
1600
  self._assert_output("ceil")
1525
1601
  return self._math.ceil(self.socket).o.value # ty: ignore[invalid-return-type]
1526
1602
 
1603
+ def truncate(self) -> Self:
1604
+ """The integer part of of the value, removing fractional digits"""
1605
+ self._assert_output("truncate")
1606
+ return self._math.truncate(self.socket).o.value # ty: ignore[invalid-return-type]
1607
+
1608
+ def fraction(self) -> Self:
1609
+ """The fractional part of the vlaue"""
1610
+ self._assert_output("fraction")
1611
+ return self._math.fraction(self.socket).o.value # ty: ignore[invalid-return-type]
1612
+
1527
1613
  def round(self) -> Self:
1528
1614
  """Round to the nearest integer."""
1529
1615
  self._assert_output("round")
1530
1616
  return self._math.round(self.socket).o.value # ty: ignore[invalid-return-type]
1531
1617
 
1618
+ def ping_pong(self, value: InputFloat = 1.0) -> Self:
1619
+ """Input ping-pongs between 0 and *value*."""
1620
+ self._assert_output("ping_pong")
1621
+ return self._math.ping_pong(self.socket, value).o.value # ty: ignore[invalid-return-type]
1622
+
1532
1623
  def modulo(self, divisor: InputFloat) -> Self:
1533
1624
  """Floored modulo — remainder after dividing by *divisor*, always non-negative."""
1534
1625
  self._assert_output("modulo")
1535
1626
  return self._math.floored_modulo(self.socket, divisor).o.value # ty: ignore[invalid-return-type]
1536
1627
 
1537
- def wrap(self, min: InputFloat, max: InputFloat) -> Self:
1628
+ def abs(self) -> Self:
1629
+ """Absolute value of the input"""
1630
+ self._assert_output("abs")
1631
+ return self._math.absolute(self.socket).o.value # ty: ignore[invalid-return-type]
1632
+
1633
+ def wrap(self, min: InputFloat = 0.0, max: InputFloat = 1.0) -> Self:
1538
1634
  """Wrap the value into the *[min, max]* range, repeating cyclically."""
1539
1635
  self._assert_output("wrap")
1540
1636
  # the wrap method has different order of arguments with max being first
1541
1637
  # compared to other nodes that are defined.
1542
1638
  return self._math.wrap(self.socket, value_001=max, value_002=min).o.value # ty: ignore[invalid-return-type]
1543
1639
 
1640
+ def mul_add(self, multiplier: InputFloat = 0.5, addend: InputFloat = 0.5) -> Self:
1641
+ """Multiply and then add a value. More efficient as it is a single CPU instruction."""
1642
+ self._assert_output("mul_add")
1643
+ return self._math.multiply_add(self.socket, multiplier, addend).o.value # ty: ignore[invalid-return-type]
1644
+
1544
1645
  def to_radians(self) -> Self:
1545
1646
  """Convert degrees to radians."""
1546
1647
  self._assert_output("to_radians")
@@ -1602,6 +1703,16 @@ class _IntegerMixin(BaseSocket, Generic[_FloatResult]):
1602
1703
  self._assert_output("negate")
1603
1704
  return self._imath.negate(self.socket).o.value # ty: ignore[invalid-return-type]
1604
1705
 
1706
+ def mul_add(self, multiplier: InputInteger = 0, addend: InputInteger = 0) -> Self:
1707
+ "Multiply and then add a value. More efficient as it is a single CPU instruction."
1708
+ self._assert_output("mul_add")
1709
+ return self._imath.multiply_add(self.socket, multiplier, addend).o.value # ty: ignore[invalid-return-type]
1710
+
1711
+ def power(self, exponent: InputInteger = 2) -> Self:
1712
+ """Raise this value to *exponent*."""
1713
+ self._assert_output("power")
1714
+ return self._imath.power(self.socket, exponent).o.value # ty: ignore[invalid-return-type]
1715
+
1605
1716
  @staticmethod
1606
1717
  def _is_integer_socket(value: Any) -> bool:
1607
1718
  socket = getattr(
@@ -629,7 +629,7 @@ class TreeBuilder(Generic[_TreeT]):
629
629
  ignore_visibility: bool = False,
630
630
  ):
631
631
  if isinstance(tree, str):
632
- self.tree = bpy.data.node_groups.new(tree, tree_type) # type: ignore[assignment]
632
+ self.tree = bpy.data.node_groups.new(tree, tree_type) # ty: ignore[invalid-assignment]
633
633
  else:
634
634
  self.tree = tree # type: ignore
635
635
 
@@ -725,6 +725,7 @@ class TreeBuilder(Generic[_TreeT]):
725
725
  keep_reroutes: bool = False,
726
726
  top_level: Literal["with", "class"] = "with",
727
727
  format: bool = True,
728
+ nodebpy_pkg: str = "nodebpy",
728
729
  ) -> str:
729
730
  """Generate Python source that recreates this tree using nodebpy.
730
731
 
@@ -741,6 +742,7 @@ class TreeBuilder(Generic[_TreeT]):
741
742
  keep_reroutes=keep_reroutes,
742
743
  top_level=top_level,
743
744
  format=format,
745
+ nodebpy_pkg=nodebpy_pkg,
744
746
  )
745
747
 
746
748
  def to_mermaid(self, fenced: bool = True) -> str: