nodebpy 520.27.0__tar.gz → 520.28.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.
- {nodebpy-520.27.0 → nodebpy-520.28.0}/PKG-INFO +1 -1
- {nodebpy-520.27.0 → nodebpy-520.28.0}/pyproject.toml +1 -1
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/__init__.py +2 -1
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/tree.py +26 -1
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/export/codegen.py +14 -0
- nodebpy-520.28.0/src/nodebpy/live.py +361 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/manual.py +2 -1
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/manual.py +2 -1
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/manual.py +2 -1
- {nodebpy-520.27.0 → nodebpy-520.28.0}/README.md +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/assets/__main__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/assets/_codegen.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/assets/_library.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/assets/_pipeline.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/_socket_order.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/_utils.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/layout.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/export/parity.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/export/plot.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/export/web_render.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/VENDORED.md +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/balancing.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/_mixins.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/assets.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/shader.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/nodes/shader/vector.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.28.0}/src/nodebpy/types.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from . import builder, export, nodes, types
|
|
1
|
+
from . import builder, export, live, nodes, types
|
|
2
2
|
from .builder import (
|
|
3
3
|
ArrangeMethod,
|
|
4
4
|
SimpleOptions,
|
|
@@ -22,6 +22,7 @@ __all__ = [
|
|
|
22
22
|
"default_sugiyama_options",
|
|
23
23
|
"export",
|
|
24
24
|
"geometry",
|
|
25
|
+
"live",
|
|
25
26
|
"nodes",
|
|
26
27
|
"shader",
|
|
27
28
|
"types",
|
|
@@ -1108,11 +1108,28 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1108
1108
|
fake_user: bool = False,
|
|
1109
1109
|
ignore_visibility: bool = False,
|
|
1110
1110
|
split_inputs: bool | None = None,
|
|
1111
|
+
clear: bool = False,
|
|
1111
1112
|
):
|
|
1113
|
+
# ``clear`` rebuilds in place: the tree is emptied (nodes, links and
|
|
1114
|
+
# interface) before the body runs, keeping the datablock so modifiers,
|
|
1115
|
+
# group nodes and pinned editors that reference it stay attached. With
|
|
1116
|
+
# a name, an existing group of that name and tree type is reused
|
|
1117
|
+
# instead of creating a ``Name.001`` duplicate; a group of another
|
|
1118
|
+
# tree type is left alone and a new one is created as usual.
|
|
1119
|
+
# Tree-level properties (description, color_tag, is_tool, fake user,
|
|
1120
|
+
# ...) are kept: only the contents are rebuilt.
|
|
1112
1121
|
if isinstance(tree, str):
|
|
1113
|
-
|
|
1122
|
+
existing = bpy.data.node_groups.get(tree) if clear else None
|
|
1123
|
+
if existing is not None and existing.bl_idname == tree_type:
|
|
1124
|
+
self.tree = existing
|
|
1125
|
+
else:
|
|
1126
|
+
self.tree = bpy.data.node_groups.new(tree, tree_type) # ty: ignore[invalid-assignment]
|
|
1114
1127
|
else:
|
|
1115
1128
|
self.tree = tree # type: ignore
|
|
1129
|
+
if clear:
|
|
1130
|
+
assert self.tree.interface is not None
|
|
1131
|
+
self.tree.nodes.clear()
|
|
1132
|
+
self.tree.interface.clear()
|
|
1116
1133
|
|
|
1117
1134
|
self._menu_defaults: list[_MenuDefault] = []
|
|
1118
1135
|
self._exited = False
|
|
@@ -1133,6 +1150,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1133
1150
|
arrange: ArrangeMethod = "sugiyama",
|
|
1134
1151
|
fake_user: bool = False,
|
|
1135
1152
|
split_inputs: bool | None = None,
|
|
1153
|
+
clear: bool = False,
|
|
1136
1154
|
) -> TreeBuilder[GeometryNodeTree]:
|
|
1137
1155
|
"""Create a geometry node tree."""
|
|
1138
1156
|
return cast(
|
|
@@ -1144,6 +1162,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1144
1162
|
arrange=arrange,
|
|
1145
1163
|
fake_user=fake_user,
|
|
1146
1164
|
split_inputs=split_inputs,
|
|
1165
|
+
clear=clear,
|
|
1147
1166
|
),
|
|
1148
1167
|
)
|
|
1149
1168
|
|
|
@@ -1156,6 +1175,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1156
1175
|
arrange: ArrangeMethod = "sugiyama",
|
|
1157
1176
|
fake_user: bool = False,
|
|
1158
1177
|
split_inputs: bool | None = None,
|
|
1178
|
+
clear: bool = False,
|
|
1159
1179
|
) -> TreeBuilder[ShaderNodeTree]:
|
|
1160
1180
|
"""Create a shader node tree."""
|
|
1161
1181
|
return cast(
|
|
@@ -1167,6 +1187,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1167
1187
|
arrange=arrange,
|
|
1168
1188
|
fake_user=fake_user,
|
|
1169
1189
|
split_inputs=split_inputs,
|
|
1190
|
+
clear=clear,
|
|
1170
1191
|
),
|
|
1171
1192
|
)
|
|
1172
1193
|
|
|
@@ -1179,6 +1200,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1179
1200
|
arrange: ArrangeMethod = "sugiyama",
|
|
1180
1201
|
fake_user: bool = False,
|
|
1181
1202
|
split_inputs: bool | None = None,
|
|
1203
|
+
clear: bool = False,
|
|
1182
1204
|
) -> TreeBuilder[CompositorNodeTree]:
|
|
1183
1205
|
"""Create a compositor node tree."""
|
|
1184
1206
|
return cast(
|
|
@@ -1190,6 +1212,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1190
1212
|
arrange=arrange,
|
|
1191
1213
|
fake_user=fake_user,
|
|
1192
1214
|
split_inputs=split_inputs,
|
|
1215
|
+
clear=clear,
|
|
1193
1216
|
),
|
|
1194
1217
|
)
|
|
1195
1218
|
|
|
@@ -1215,6 +1238,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1215
1238
|
top_level: Literal["with", "class"] = "with",
|
|
1216
1239
|
format: bool = True,
|
|
1217
1240
|
nodebpy_pkg: str = "nodebpy",
|
|
1241
|
+
in_place: bool = False,
|
|
1218
1242
|
) -> str:
|
|
1219
1243
|
"""Generate Python source that recreates this tree using nodebpy.
|
|
1220
1244
|
|
|
@@ -1232,6 +1256,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1232
1256
|
top_level=top_level,
|
|
1233
1257
|
format=format,
|
|
1234
1258
|
nodebpy_pkg=nodebpy_pkg,
|
|
1259
|
+
in_place=in_place,
|
|
1235
1260
|
)
|
|
1236
1261
|
|
|
1237
1262
|
def to_mermaid(self, fenced: bool = True) -> str:
|
|
@@ -3724,6 +3724,7 @@ def to_python(
|
|
|
3724
3724
|
external_groups: Collection[str] | None = None,
|
|
3725
3725
|
typed_groups: Collection[str] | None = None,
|
|
3726
3726
|
root_interface: GroupInterface | None = None,
|
|
3727
|
+
in_place: bool = False,
|
|
3727
3728
|
) -> str:
|
|
3728
3729
|
"""Generate Python code that recreates the given node tree using nodebpy.
|
|
3729
3730
|
|
|
@@ -3794,6 +3795,17 @@ def to_python(
|
|
|
3794
3795
|
Typed-interface parts (docstring, class attributes, accessors and
|
|
3795
3796
|
``__init__``) spliced into the top-level tree's class in ``class``
|
|
3796
3797
|
mode, ahead of ``_build_group``.
|
|
3798
|
+
in_place: bool
|
|
3799
|
+
If True, the ``with`` header is emitted as
|
|
3800
|
+
``with g.tree("Name", clear=True) as tree:`` so running the source
|
|
3801
|
+
rebuilds the exported tree in place — the same datablock, emptied
|
|
3802
|
+
first — instead of creating a ``Name.001`` copy, keeping modifiers,
|
|
3803
|
+
group nodes and pinned editors attached across re-runs. Only the
|
|
3804
|
+
top-level tree is rebuilt this way: nested groups are emitted as
|
|
3805
|
+
``Custom*Group`` classes whose ``create_group()`` reuses an existing
|
|
3806
|
+
tree of the same name, so edits to them only take effect through
|
|
3807
|
+
:func:`nodebpy.live.run_source`. Ignored with ``top_level="class"``
|
|
3808
|
+
(a class is rebuilt by ``run_source``, not by a header).
|
|
3797
3809
|
|
|
3798
3810
|
Returns
|
|
3799
3811
|
-------
|
|
@@ -3876,6 +3888,8 @@ def to_python(
|
|
|
3876
3888
|
# for either option.
|
|
3877
3889
|
if snapshot_positions or keep_reroutes:
|
|
3878
3890
|
ctor_args.append("arrange=None")
|
|
3891
|
+
if in_place:
|
|
3892
|
+
ctor_args.append("clear=True")
|
|
3879
3893
|
lines.append(f"with {constructor}({', '.join(ctor_args)}) as tree:")
|
|
3880
3894
|
lines.extend(_assemble_tree_body(emission))
|
|
3881
3895
|
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Re-run nodebpy source safely inside a live Blender session.
|
|
2
|
+
|
|
3
|
+
A live editor (a Text block, a file on disk) re-executes its code on every
|
|
4
|
+
change. Three things make a naive ``exec`` unpleasant, and :func:`run_source`
|
|
5
|
+
handles all of them:
|
|
6
|
+
|
|
7
|
+
- ``Custom*Group.create_group()`` reuses an existing group of the same
|
|
8
|
+
``_name`` rather than rebuilding it, so re-running edited class code would
|
|
9
|
+
silently hand back the previous build. Existing groups the code claims are
|
|
10
|
+
renamed out of the way first (:func:`stash_groups`), then their users are
|
|
11
|
+
remapped onto the rebuilt trees and the old ones removed
|
|
12
|
+
(:meth:`GroupStash.replace`); on failure the old names are put back
|
|
13
|
+
(:meth:`GroupStash.restore`).
|
|
14
|
+
- Rebuilding a tree's interface regenerates its socket identifiers, which is
|
|
15
|
+
what Geometry Nodes modifiers key their input values on, so the values
|
|
16
|
+
would be lost. :func:`preserve_modifier_inputs` snapshots them by socket
|
|
17
|
+
name and reapplies them afterwards.
|
|
18
|
+
- The tree the run produced has to be found among whatever the code did:
|
|
19
|
+
the ``tree`` variable of the ``with g.tree(...) as tree:`` form, the newest
|
|
20
|
+
top-level group it created, or the last group class it defined.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
from collections.abc import Iterable, Iterator
|
|
27
|
+
from contextlib import contextmanager, suppress
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Any, cast
|
|
30
|
+
|
|
31
|
+
import bpy
|
|
32
|
+
|
|
33
|
+
from .builder import TreeBuilder
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"GroupStash",
|
|
37
|
+
"RunResult",
|
|
38
|
+
"group_names_in_source",
|
|
39
|
+
"preserve_modifier_inputs",
|
|
40
|
+
"run_source",
|
|
41
|
+
"stash_groups",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def group_names_in_source(code: str) -> set[str]:
|
|
46
|
+
"""Literal ``_name`` values assigned in class bodies of ``code``.
|
|
47
|
+
|
|
48
|
+
Parsed with :mod:`ast`; both ``_name = "..."`` and ``_name: str = "..."``
|
|
49
|
+
count. Returns an empty set on a :class:`SyntaxError` (``exec`` reports
|
|
50
|
+
it).
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
module = ast.parse(code)
|
|
54
|
+
except SyntaxError:
|
|
55
|
+
return set()
|
|
56
|
+
names: set[str] = set()
|
|
57
|
+
for node in ast.walk(module):
|
|
58
|
+
if not isinstance(node, ast.ClassDef):
|
|
59
|
+
continue
|
|
60
|
+
for stmt in node.body:
|
|
61
|
+
if isinstance(stmt, ast.Assign):
|
|
62
|
+
targets, value = stmt.targets, stmt.value
|
|
63
|
+
elif isinstance(stmt, ast.AnnAssign) and stmt.value is not None:
|
|
64
|
+
targets, value = [stmt.target], stmt.value
|
|
65
|
+
else:
|
|
66
|
+
continue
|
|
67
|
+
if not (isinstance(value, ast.Constant) and isinstance(value.value, str)):
|
|
68
|
+
continue
|
|
69
|
+
if any(isinstance(t, ast.Name) and t.id == "_name" for t in targets):
|
|
70
|
+
names.add(value.value)
|
|
71
|
+
return names
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class GroupStash:
|
|
76
|
+
"""Existing node groups renamed out of the way before a run.
|
|
77
|
+
|
|
78
|
+
Maps each original name to the old tree, now named ``<name>.stale``.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
stashed: dict[str, bpy.types.NodeTree] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
def restore(self) -> None:
|
|
84
|
+
"""Failure path: remove any partial build under an original name and
|
|
85
|
+
give the old trees their names back. Empties the stash, so calling
|
|
86
|
+
it (or :meth:`replace`) again is a no-op."""
|
|
87
|
+
for name, old in self.stashed.items():
|
|
88
|
+
leftover = bpy.data.node_groups.get(name)
|
|
89
|
+
if leftover is not None and leftover != old:
|
|
90
|
+
bpy.data.node_groups.remove(leftover)
|
|
91
|
+
old.name = name
|
|
92
|
+
self.stashed.clear()
|
|
93
|
+
|
|
94
|
+
def replace(self) -> None:
|
|
95
|
+
"""Success path: for each name, if the run created a new tree of the
|
|
96
|
+
same type under it, point the old tree's users (group nodes,
|
|
97
|
+
modifiers, pinned editors) at the new one and remove the old;
|
|
98
|
+
otherwise the old tree keeps its original name. Empties the stash,
|
|
99
|
+
so calling it (or :meth:`restore`) again is a no-op."""
|
|
100
|
+
for name, old in self.stashed.items():
|
|
101
|
+
new = bpy.data.node_groups.get(name)
|
|
102
|
+
if new is not None and new != old and new.bl_idname == old.bl_idname:
|
|
103
|
+
old.user_remap(new)
|
|
104
|
+
bpy.data.node_groups.remove(old)
|
|
105
|
+
else:
|
|
106
|
+
old.name = name
|
|
107
|
+
self.stashed.clear()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def stash_groups(names: Iterable[str]) -> GroupStash:
|
|
111
|
+
"""Rename each existing ``bpy.data.node_groups[name]`` to ``"<name>.stale"``
|
|
112
|
+
and return the :class:`GroupStash` that undoes or completes the move.
|
|
113
|
+
|
|
114
|
+
A group linked from a library (an appended asset) cannot be renamed and
|
|
115
|
+
is left in place, so ``create_group()`` reuses it as usual.
|
|
116
|
+
"""
|
|
117
|
+
stash = GroupStash()
|
|
118
|
+
for name in names:
|
|
119
|
+
if name in stash.stashed:
|
|
120
|
+
continue
|
|
121
|
+
existing = bpy.data.node_groups.get(name)
|
|
122
|
+
if existing is not None and existing.library is None:
|
|
123
|
+
existing.name = f"{name}.stale"
|
|
124
|
+
stash.stashed[name] = existing
|
|
125
|
+
return stash
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _copy_value(value: Any) -> Any:
|
|
129
|
+
# Vector / colour values come back as bpy_prop_array views into the
|
|
130
|
+
# modifier's storage, which the rebuild replaces; keep a plain tuple.
|
|
131
|
+
if hasattr(value, "__len__") and not isinstance(value, (str, bytes, bpy.types.ID)):
|
|
132
|
+
return tuple(value)
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _input_sockets(
|
|
137
|
+
tree: bpy.types.NodeTree,
|
|
138
|
+
) -> dict[str, bpy.types.NodeTreeInterfaceSocket]:
|
|
139
|
+
"""Interface input sockets by name (first wins on duplicate names)."""
|
|
140
|
+
sockets: dict[str, bpy.types.NodeTreeInterfaceSocket] = {}
|
|
141
|
+
assert tree.interface is not None
|
|
142
|
+
for item in tree.interface.items_tree:
|
|
143
|
+
if item.item_type != "SOCKET":
|
|
144
|
+
continue
|
|
145
|
+
assert isinstance(item, bpy.types.NodeTreeInterfaceSocket)
|
|
146
|
+
if item.in_out == "INPUT" and item.name not in sockets:
|
|
147
|
+
sockets[item.name] = item
|
|
148
|
+
return sockets
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _modifier_inputs(modifier: bpy.types.NodesModifier) -> Any:
|
|
152
|
+
"""``modifier.properties.inputs``: one RNA property per interface input,
|
|
153
|
+
named by socket identifier, each wrapping the stored ``value`` (Blender
|
|
154
|
+
5.x; the type stubs predate it)."""
|
|
155
|
+
properties = cast("Any", modifier.properties)
|
|
156
|
+
return properties.inputs
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _nodes_modifiers(pointers: set[int]) -> Iterator[bpy.types.NodesModifier]:
|
|
160
|
+
for obj in bpy.data.objects:
|
|
161
|
+
for modifier in obj.modifiers:
|
|
162
|
+
if not isinstance(modifier, bpy.types.NodesModifier):
|
|
163
|
+
continue
|
|
164
|
+
group = modifier.node_group
|
|
165
|
+
if group is not None and group.as_pointer() in pointers:
|
|
166
|
+
yield modifier
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Per-input state a modifier stores besides ``value``: whether the input
|
|
170
|
+
# reads a named attribute instead (``type`` is ``VALUE`` or ``ATTRIBUTE``)
|
|
171
|
+
# and which one.
|
|
172
|
+
_INPUT_ATTRS = ("value", "type", "attribute_name")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class _InputState:
|
|
177
|
+
socket_type: str
|
|
178
|
+
attrs: dict[str, Any]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@contextmanager
|
|
182
|
+
def preserve_modifier_inputs(trees: Iterable[bpy.types.NodeTree]) -> Iterator[None]:
|
|
183
|
+
"""Keep Geometry Nodes modifier input values across an interface rebuild.
|
|
184
|
+
|
|
185
|
+
Snapshots the input state of every Geometry Nodes modifier (on any
|
|
186
|
+
object) whose ``node_group`` is one of ``trees``, keyed by interface
|
|
187
|
+
socket *name*: the value, and whether the input reads a named attribute
|
|
188
|
+
instead (``type`` and ``attribute_name``). On exit — also when the body
|
|
189
|
+
raised, so a failed in-place rebuild keeps the values of the sockets it
|
|
190
|
+
did build — each entry whose socket name still exists on the modifier's
|
|
191
|
+
(possibly rebuilt, possibly remapped) tree is reapplied; sockets whose
|
|
192
|
+
type changed are skipped, as is anything that fails to apply — the
|
|
193
|
+
reapply step never raises.
|
|
194
|
+
"""
|
|
195
|
+
pointers = {tree.as_pointer() for tree in trees}
|
|
196
|
+
snapshot: list[tuple[bpy.types.NodesModifier, dict[str, _InputState]]] = []
|
|
197
|
+
for modifier in _nodes_modifiers(pointers):
|
|
198
|
+
assert modifier.node_group is not None
|
|
199
|
+
inputs = _modifier_inputs(modifier)
|
|
200
|
+
values: dict[str, _InputState] = {}
|
|
201
|
+
for name, socket in _input_sockets(modifier.node_group).items():
|
|
202
|
+
wrapper = getattr(inputs, socket.identifier, None)
|
|
203
|
+
if wrapper is None or not hasattr(wrapper, "value"):
|
|
204
|
+
continue # a geometry socket, or one with no stored value
|
|
205
|
+
attrs = {
|
|
206
|
+
attr: _copy_value(getattr(wrapper, attr))
|
|
207
|
+
for attr in _INPUT_ATTRS
|
|
208
|
+
if hasattr(wrapper, attr)
|
|
209
|
+
}
|
|
210
|
+
values[name] = _InputState(socket.socket_type, attrs)
|
|
211
|
+
snapshot.append((modifier, values))
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
yield
|
|
215
|
+
finally:
|
|
216
|
+
for modifier, values in snapshot:
|
|
217
|
+
# A modifier the run removed, a tree it freed, ...: never raise.
|
|
218
|
+
with suppress(Exception):
|
|
219
|
+
_reapply_inputs(modifier, values)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _reapply_inputs(
|
|
223
|
+
modifier: bpy.types.NodesModifier, values: dict[str, _InputState]
|
|
224
|
+
) -> None:
|
|
225
|
+
group = modifier.node_group
|
|
226
|
+
if group is None:
|
|
227
|
+
return
|
|
228
|
+
inputs = _modifier_inputs(modifier)
|
|
229
|
+
sockets = _input_sockets(group)
|
|
230
|
+
for name, state in values.items():
|
|
231
|
+
socket = sockets.get(name)
|
|
232
|
+
if socket is None or socket.socket_type != state.socket_type:
|
|
233
|
+
continue
|
|
234
|
+
wrapper = getattr(inputs, socket.identifier, None)
|
|
235
|
+
if wrapper is None or not hasattr(wrapper, "value"):
|
|
236
|
+
continue
|
|
237
|
+
for attr, value in state.attrs.items():
|
|
238
|
+
with suppress(Exception): # best effort, per attribute
|
|
239
|
+
setattr(wrapper, attr, value)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass
|
|
243
|
+
class RunResult:
|
|
244
|
+
"""What :func:`run_source` produced."""
|
|
245
|
+
|
|
246
|
+
tree: bpy.types.NodeTree | None
|
|
247
|
+
"""The tree the run produced, or ``None`` if it made no tree."""
|
|
248
|
+
created: list[bpy.types.NodeTree]
|
|
249
|
+
"""Every node group that exists after the run and did not before."""
|
|
250
|
+
namespace: dict[str, Any]
|
|
251
|
+
"""The globals the code ran in."""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _is_group_class(value: Any, module_name: str | None) -> bool:
|
|
255
|
+
return (
|
|
256
|
+
isinstance(value, type)
|
|
257
|
+
and getattr(value, "__module__", None) == module_name
|
|
258
|
+
and callable(getattr(value, "create_group", None))
|
|
259
|
+
and bool(getattr(value, "_name", None))
|
|
260
|
+
and bool(getattr(value, "_tree_idname", None))
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _produced_tree(
|
|
265
|
+
namespace: dict[str, Any], before: set[int], prior: Any
|
|
266
|
+
) -> bpy.types.NodeTree | None:
|
|
267
|
+
"""The tree a run produced: the ``tree`` variable's, else the newest
|
|
268
|
+
top-level group created, else the last group class defined (built now).
|
|
269
|
+
|
|
270
|
+
``before`` holds the ``session_uid`` of every group that existed before
|
|
271
|
+
the run; ``prior`` is whatever ``tree`` was bound to before it (a value
|
|
272
|
+
the caller's namespace carried over from an earlier run), which does not
|
|
273
|
+
count as produced by this one."""
|
|
274
|
+
bound = namespace.get("tree")
|
|
275
|
+
if bound is not prior:
|
|
276
|
+
if isinstance(bound, TreeBuilder):
|
|
277
|
+
return bound.tree
|
|
278
|
+
if isinstance(bound, bpy.types.NodeTree):
|
|
279
|
+
return bound
|
|
280
|
+
|
|
281
|
+
new_groups = [g for g in bpy.data.node_groups if g.session_uid not in before]
|
|
282
|
+
if new_groups:
|
|
283
|
+
nested = {
|
|
284
|
+
sub.as_pointer()
|
|
285
|
+
for group in new_groups
|
|
286
|
+
for node in group.nodes
|
|
287
|
+
if (sub := getattr(node, "node_tree", None)) is not None
|
|
288
|
+
}
|
|
289
|
+
top_level = [g for g in new_groups if g.as_pointer() not in nested]
|
|
290
|
+
# bpy.data.node_groups is sorted by name; session_uid counts up with
|
|
291
|
+
# creation, so it picks the group the code made last.
|
|
292
|
+
return max(top_level or new_groups, key=lambda g: g.session_uid)
|
|
293
|
+
|
|
294
|
+
module_name = namespace.get("__name__")
|
|
295
|
+
for value in reversed(list(namespace.values())):
|
|
296
|
+
if _is_group_class(value, module_name):
|
|
297
|
+
tree = value.create_group()
|
|
298
|
+
if isinstance(tree, bpy.types.NodeTree):
|
|
299
|
+
return tree
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def run_source(
|
|
304
|
+
code: str,
|
|
305
|
+
*,
|
|
306
|
+
filename: str = "<nodebpy>",
|
|
307
|
+
namespace: dict[str, Any] | None = None,
|
|
308
|
+
) -> RunResult:
|
|
309
|
+
"""Execute nodebpy source for a live re-run.
|
|
310
|
+
|
|
311
|
+
Existing groups the code's classes claim by ``_name`` are stashed so
|
|
312
|
+
``create_group()`` builds fresh, Geometry Nodes modifier input values on
|
|
313
|
+
every pre-existing tree are preserved across the rebuild, and the
|
|
314
|
+
stashed trees' users are remapped onto the new builds afterwards. On any
|
|
315
|
+
exception the stash is restored and the exception re-raised unchanged;
|
|
316
|
+
``filename`` is what its traceback names. Nothing is printed or captured.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
code
|
|
321
|
+
The source to run.
|
|
322
|
+
filename
|
|
323
|
+
Filename for the compiled code, so tracebacks point at the caller's
|
|
324
|
+
Text block or file.
|
|
325
|
+
namespace
|
|
326
|
+
Extra globals merged over the defaults (``__name__``, ``__file__``
|
|
327
|
+
and ``bpy``). A ``tree`` it carries over from an earlier run's
|
|
328
|
+
:attr:`RunResult.namespace` is not mistaken for this run's tree.
|
|
329
|
+
|
|
330
|
+
A run that fails partway through a ``with g.tree(..., clear=True)`` body
|
|
331
|
+
leaves that tree half built (the class form is atomic: the old build is
|
|
332
|
+
restored). Groups the failed run created under other names are left
|
|
333
|
+
behind as well.
|
|
334
|
+
"""
|
|
335
|
+
stash = stash_groups(group_names_in_source(code))
|
|
336
|
+
before = {group.session_uid for group in bpy.data.node_groups}
|
|
337
|
+
globals_: dict[str, Any] = {
|
|
338
|
+
"__name__": "__nodebpy_live__",
|
|
339
|
+
"__file__": filename,
|
|
340
|
+
"bpy": bpy,
|
|
341
|
+
}
|
|
342
|
+
if namespace:
|
|
343
|
+
globals_.update(namespace)
|
|
344
|
+
prior = globals_.get("tree")
|
|
345
|
+
|
|
346
|
+
existing = list(bpy.data.node_groups)
|
|
347
|
+
with preserve_modifier_inputs(existing):
|
|
348
|
+
try:
|
|
349
|
+
exec(compile(code, filename, "exec"), globals_) # noqa: S102
|
|
350
|
+
# Resolved before the stash is replaced: building a defined-only
|
|
351
|
+
# class must not find the old tree back under its name.
|
|
352
|
+
tree = _produced_tree(globals_, before, prior)
|
|
353
|
+
except BaseException:
|
|
354
|
+
stash.restore()
|
|
355
|
+
raise
|
|
356
|
+
# Inside the preserve context: remapping a modifier onto the rebuilt
|
|
357
|
+
# tree resets its inputs just like an in-place rebuild does.
|
|
358
|
+
stash.replace()
|
|
359
|
+
|
|
360
|
+
created = [g for g in bpy.data.node_groups if g.session_uid not in before]
|
|
361
|
+
return RunResult(tree=tree, created=created, namespace=globals_)
|
|
@@ -42,9 +42,10 @@ def tree(
|
|
|
42
42
|
collapse: bool = False,
|
|
43
43
|
arrange: ArrangeMethod = "sugiyama",
|
|
44
44
|
fake_user: bool = False,
|
|
45
|
+
clear: bool = False,
|
|
45
46
|
) -> TreeBuilder[CompositorNodeTree]:
|
|
46
47
|
return TreeBuilder.compositor(
|
|
47
|
-
name, collapse=collapse, arrange=arrange, fake_user=fake_user
|
|
48
|
+
name, collapse=collapse, arrange=arrange, fake_user=fake_user, clear=clear
|
|
48
49
|
)
|
|
49
50
|
|
|
50
51
|
|
|
@@ -152,8 +152,9 @@ def tree(
|
|
|
152
152
|
*,
|
|
153
153
|
collapse: bool = False,
|
|
154
154
|
arrange: ArrangeMethod = "sugiyama",
|
|
155
|
+
clear: bool = False,
|
|
155
156
|
) -> TreeBuilder[GeometryNodeTree]:
|
|
156
|
-
return TreeBuilder.geometry(name, collapse=collapse, arrange=arrange)
|
|
157
|
+
return TreeBuilder.geometry(name, collapse=collapse, arrange=arrange, clear=clear)
|
|
157
158
|
|
|
158
159
|
|
|
159
160
|
_ColorRampColorInterpolations = Literal[
|
|
@@ -52,9 +52,10 @@ def tree(
|
|
|
52
52
|
collapse: bool = False,
|
|
53
53
|
arrange: ArrangeMethod = "sugiyama",
|
|
54
54
|
fake_user: bool = False,
|
|
55
|
+
clear: bool = False,
|
|
55
56
|
) -> TreeBuilder[ShaderNodeTree]:
|
|
56
57
|
return TreeBuilder.shader(
|
|
57
|
-
name, collapse=collapse, arrange=arrange, fake_user=fake_user
|
|
58
|
+
name, collapse=collapse, arrange=arrange, fake_user=fake_user, clear=clear
|
|
58
59
|
)
|
|
59
60
|
|
|
60
61
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|