nodebpy 520.37.0__tar.gz → 520.39.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.37.0 → nodebpy-520.39.0}/PKG-INFO +1 -1
- {nodebpy-520.37.0 → nodebpy-520.39.0}/pyproject.toml +1 -1
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/__init__.py +2 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/__init__.py +4 -1
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/_utils.py +4 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/socket.py +4 -2
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/tree.py +152 -3
- nodebpy-520.39.0/src/nodebpy/export/_groups.py +168 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/export/codegen.py +112 -4
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/DESIGN.md +3 -2
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/api.py +3 -0
- nodebpy-520.39.0/src/nodebpy/layout/keep.py +535 -0
- nodebpy-520.39.0/src/nodebpy/live.py +767 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/manual.py +9 -1
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/skills/nodebpy/SKILL.md +1 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/skills/nodebpy/references/existing-trees.md +19 -6
- nodebpy-520.37.0/src/nodebpy/live.py +0 -363
- {nodebpy-520.37.0 → nodebpy-520.39.0}/README.md +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/__main__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/_lookup.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/_skill.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/assets/__main__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/assets/_codegen.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/assets/_library.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/assets/_pipeline.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/assets/_textconv.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/_combine.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/_infer.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/_socket_order.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/export/_floats.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/export/parity.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/export/plot.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/export/web_render.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/__init__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/apply.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/balancing.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/build.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/common.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/config.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/digraph.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/dna.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/edits.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/extract.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/feeders.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/long_links.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/model.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/node_size.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/ordering.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/packing.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/pipeline.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/placement.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/priority.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/ranking.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/realize.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/reroutes.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/routing.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/snapping.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/spacing.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/stacking.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/sugiyama.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/layout/zones.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/_mixins.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/assets.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/shader.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/nodes/shader/vector.py +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/py.typed +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/skills/nodebpy/references/shader-compositor.md +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/skills/nodebpy/references/verifying.md +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/skills/nodebpy/references/writing-trees.md +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/skills/nodebpy/references/zones-and-items.md +0 -0
- {nodebpy-520.37.0 → nodebpy-520.39.0}/src/nodebpy/types.py +0 -0
|
@@ -5,6 +5,7 @@ from .builder import (
|
|
|
5
5
|
SugiyamaOptions,
|
|
6
6
|
TreeBuilder,
|
|
7
7
|
arrange,
|
|
8
|
+
default_keep_edits,
|
|
8
9
|
default_split_inputs,
|
|
9
10
|
default_sugiyama_options,
|
|
10
11
|
)
|
|
@@ -20,6 +21,7 @@ __all__ = [
|
|
|
20
21
|
"arrange",
|
|
21
22
|
"builder",
|
|
22
23
|
"compositor",
|
|
24
|
+
"default_keep_edits",
|
|
23
25
|
"default_split_inputs",
|
|
24
26
|
"default_sugiyama_options",
|
|
25
27
|
"export",
|
|
@@ -5,7 +5,7 @@ from ..layout import (
|
|
|
5
5
|
arrange,
|
|
6
6
|
default_sugiyama_options,
|
|
7
7
|
)
|
|
8
|
-
from ._utils import SocketError, denormalize_name, normalize_name
|
|
8
|
+
from ._utils import InvalidLinkWarning, SocketError, denormalize_name, normalize_name
|
|
9
9
|
from .accessor import SocketAccessor
|
|
10
10
|
from .asset import (
|
|
11
11
|
AssetCompositorGroup,
|
|
@@ -82,6 +82,7 @@ from .tree import (
|
|
|
82
82
|
SocketContext,
|
|
83
83
|
TreeBuilder,
|
|
84
84
|
WorldBuilder,
|
|
85
|
+
default_keep_edits,
|
|
85
86
|
default_split_inputs,
|
|
86
87
|
)
|
|
87
88
|
|
|
@@ -123,6 +124,7 @@ __all__ = [
|
|
|
123
124
|
"IntegerSocketGrid",
|
|
124
125
|
"IntegerSocketList",
|
|
125
126
|
"IntegerVectorSocket",
|
|
127
|
+
"InvalidLinkWarning",
|
|
126
128
|
"Item",
|
|
127
129
|
"ItemsMixin",
|
|
128
130
|
"LinkingMixin",
|
|
@@ -162,6 +164,7 @@ __all__ = [
|
|
|
162
164
|
"arrange",
|
|
163
165
|
"asset_group_base",
|
|
164
166
|
"build_from_source",
|
|
167
|
+
"default_keep_edits",
|
|
165
168
|
"default_split_inputs",
|
|
166
169
|
"default_sugiyama_options",
|
|
167
170
|
"denormalize_name",
|
|
@@ -20,6 +20,10 @@ class SocketError(Exception):
|
|
|
20
20
|
"""Raised when a socket operation fails."""
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
class InvalidLinkWarning(UserWarning):
|
|
24
|
+
"""Emitted when a built tree holds links Blender can't evaluate."""
|
|
25
|
+
|
|
26
|
+
|
|
23
27
|
_ENUM_NOT_FOUND = re.compile(
|
|
24
28
|
r'enum "(?P<value>[^"]*)" not found in \((?P<options>.*)\)'
|
|
25
29
|
)
|
|
@@ -15,6 +15,7 @@ Organization (top to bottom):
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import math
|
|
18
19
|
from collections.abc import Iterable, Iterator, Mapping
|
|
19
20
|
from typing import (
|
|
20
21
|
TYPE_CHECKING,
|
|
@@ -1958,8 +1959,9 @@ class _FloatMixin[IntegerResult: (IntegerSocket, IntegerSocketGrid, IntegerSocke
|
|
|
1958
1959
|
self._assert_output("power")
|
|
1959
1960
|
return self._math.power(self.socket, exponent).o.value # ty: ignore[invalid-return-type]
|
|
1960
1961
|
|
|
1961
|
-
def log(self, base: InputFloat =
|
|
1962
|
-
"""Return the logarithm of this value to *base
|
|
1962
|
+
def log(self, base: InputFloat = math.e) -> Self:
|
|
1963
|
+
"""Return the logarithm of this value to *base*, the natural logarithm
|
|
1964
|
+
by default (as ``math.log``)."""
|
|
1963
1965
|
self._assert_output("log")
|
|
1964
1966
|
return self._math.logarithm(self.socket, base).o.value # ty: ignore[invalid-return-type]
|
|
1965
1967
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from collections import Counter
|
|
4
|
-
from collections.abc import Iterator
|
|
5
|
+
from collections.abc import Collection, Iterator
|
|
5
6
|
from contextlib import AbstractContextManager, contextmanager
|
|
6
7
|
from contextvars import ContextVar
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Any, ClassVar, Literal, Self, TypeVar, cast
|
|
10
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Self, TypeVar, cast
|
|
10
11
|
|
|
11
12
|
import bpy
|
|
12
13
|
from bpy.types import (
|
|
@@ -22,6 +23,7 @@ from bpy.types import (
|
|
|
22
23
|
|
|
23
24
|
from ..layout import ArrangeMethod
|
|
24
25
|
from ..layout import arrange as _arrange_nodes
|
|
26
|
+
from ..layout import keep as _keep
|
|
25
27
|
from ..types import (
|
|
26
28
|
SOCKET_COMPATIBILITY,
|
|
27
29
|
Default,
|
|
@@ -33,6 +35,7 @@ from ..types import (
|
|
|
33
35
|
_SocketShapeStructureType,
|
|
34
36
|
)
|
|
35
37
|
from ._utils import (
|
|
38
|
+
InvalidLinkWarning,
|
|
36
39
|
SocketError,
|
|
37
40
|
_allow_innactive_sockets,
|
|
38
41
|
enum_value_error,
|
|
@@ -62,6 +65,9 @@ from .socket import (
|
|
|
62
65
|
VectorSocket,
|
|
63
66
|
)
|
|
64
67
|
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
from ..live import NodeAnimation
|
|
70
|
+
|
|
65
71
|
_SocketT = TypeVar("_SocketT", bound=Socket)
|
|
66
72
|
|
|
67
73
|
# What TreeBuilder's split_inputs=None resolves to; see `default_split_inputs`.
|
|
@@ -86,6 +92,41 @@ def default_split_inputs(split: bool = True) -> Iterator[None]:
|
|
|
86
92
|
_DEFAULT_SPLIT_INPUTS.reset(token)
|
|
87
93
|
|
|
88
94
|
|
|
95
|
+
# What TreeBuilder's keep_edits=None resolves to; see `default_keep_edits`.
|
|
96
|
+
_DEFAULT_KEEP_EDITS: ContextVar[bool] = ContextVar(
|
|
97
|
+
"nodebpy_default_keep_edits", default=False
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@contextmanager
|
|
102
|
+
def default_keep_edits(keep: bool = True) -> Iterator[None]:
|
|
103
|
+
"""Scope in which every ``TreeBuilder`` left at its default
|
|
104
|
+
``keep_edits`` keeps the layout edits made to the tree it rebuilds with
|
|
105
|
+
``clear=True`` (see :class:`TreeBuilder`). :func:`nodebpy.live.run_source`
|
|
106
|
+
runs its code in this scope.
|
|
107
|
+
"""
|
|
108
|
+
token = _DEFAULT_KEEP_EDITS.set(keep)
|
|
109
|
+
try:
|
|
110
|
+
yield
|
|
111
|
+
finally:
|
|
112
|
+
_DEFAULT_KEEP_EDITS.reset(token)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Whether every TreeBuilder records what it built (see nodebpy.live.stamp_build),
|
|
116
|
+
# not only the clear=True rebuilds; run_source sets it for the group classes
|
|
117
|
+
# it rebuilds. Recording costs a pass over the nodes, so other builds skip it.
|
|
118
|
+
_RECORD_BUILDS: ContextVar[bool] = ContextVar("nodebpy_record_builds", default=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@contextmanager
|
|
122
|
+
def _record_builds() -> Iterator[None]:
|
|
123
|
+
token = _RECORD_BUILDS.set(True)
|
|
124
|
+
try:
|
|
125
|
+
yield
|
|
126
|
+
finally:
|
|
127
|
+
_RECORD_BUILDS.reset(token)
|
|
128
|
+
|
|
129
|
+
|
|
89
130
|
# The interface ``default_input`` values Blender actually accepts at runtime,
|
|
90
131
|
# per socket type (the RNA enum lists every option on every type, but
|
|
91
132
|
# assignment validates a per-type subset — union across tree types here).
|
|
@@ -1138,6 +1179,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1138
1179
|
ignore_visibility: bool = False,
|
|
1139
1180
|
split_inputs: bool | None = None,
|
|
1140
1181
|
clear: bool = False,
|
|
1182
|
+
keep_edits: bool | None = None,
|
|
1141
1183
|
is_modifier: bool | None = None,
|
|
1142
1184
|
is_tool: bool | None = None,
|
|
1143
1185
|
):
|
|
@@ -1154,6 +1196,10 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1154
1196
|
# interface gives its sockets new identifiers, which is what
|
|
1155
1197
|
# Geometry Nodes modifiers key their input values by, so those values
|
|
1156
1198
|
# are snapshotted by socket name here and reapplied on exit.
|
|
1199
|
+
# ``keep_edits`` (default: see default_keep_edits) carries the layout
|
|
1200
|
+
# edits made to the tree since it was last arranged (moved, resized,
|
|
1201
|
+
# collapsed, framed or relabelled nodes) over to the rebuild, onto
|
|
1202
|
+
# the nodes that match; see nodebpy.layout.keep.
|
|
1157
1203
|
if isinstance(tree, str):
|
|
1158
1204
|
existing = bpy.data.node_groups.get(tree) if clear else None
|
|
1159
1205
|
if existing is not None and existing.bl_idname == tree_type:
|
|
@@ -1163,12 +1209,33 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1163
1209
|
else:
|
|
1164
1210
|
self.tree = tree # type: ignore
|
|
1165
1211
|
self._preserved_inputs: AbstractContextManager[None] | None = None
|
|
1212
|
+
self._kept_edits: _keep.Edits | None = None
|
|
1213
|
+
if keep_edits is None:
|
|
1214
|
+
keep_edits = _DEFAULT_KEEP_EDITS.get()
|
|
1215
|
+
if clear and keep_edits and arrange is not None:
|
|
1216
|
+
self._kept_edits = _keep.record(self.tree)
|
|
1217
|
+
self._animation: NodeAnimation | None = None
|
|
1218
|
+
self._unedited_users: list[NodeTree] = []
|
|
1219
|
+
self._record = clear or _RECORD_BUILDS.get()
|
|
1166
1220
|
if clear:
|
|
1167
|
-
from ..live import
|
|
1221
|
+
from ..live import (
|
|
1222
|
+
NodeAnimation,
|
|
1223
|
+
keep_edited,
|
|
1224
|
+
preserve_modifier_inputs,
|
|
1225
|
+
unedited_users,
|
|
1226
|
+
was_edited,
|
|
1227
|
+
)
|
|
1168
1228
|
|
|
1229
|
+
self._unedited_users = unedited_users(self.tree)
|
|
1230
|
+
if was_edited(self.tree):
|
|
1231
|
+
copy = self.tree.copy()
|
|
1232
|
+
assert copy is not None
|
|
1233
|
+
keep_edited(copy, self.tree.name)
|
|
1169
1234
|
assert self.tree.interface is not None
|
|
1170
1235
|
self._preserved_inputs = preserve_modifier_inputs([self.tree])
|
|
1171
1236
|
self._preserved_inputs.__enter__()
|
|
1237
|
+
# Removing the nodes would delete their keyframes and drivers.
|
|
1238
|
+
self._animation = NodeAnimation(self.tree)
|
|
1172
1239
|
self.tree.nodes.clear()
|
|
1173
1240
|
self.tree.interface.clear()
|
|
1174
1241
|
|
|
@@ -1197,6 +1264,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1197
1264
|
fake_user: bool = False,
|
|
1198
1265
|
split_inputs: bool | None = None,
|
|
1199
1266
|
clear: bool = False,
|
|
1267
|
+
keep_edits: bool | None = None,
|
|
1200
1268
|
is_modifier: bool | None = None,
|
|
1201
1269
|
is_tool: bool | None = None,
|
|
1202
1270
|
) -> TreeBuilder[GeometryNodeTree]:
|
|
@@ -1215,6 +1283,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1215
1283
|
fake_user=fake_user,
|
|
1216
1284
|
split_inputs=split_inputs,
|
|
1217
1285
|
clear=clear,
|
|
1286
|
+
keep_edits=keep_edits,
|
|
1218
1287
|
is_modifier=is_modifier,
|
|
1219
1288
|
is_tool=is_tool,
|
|
1220
1289
|
),
|
|
@@ -1230,6 +1299,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1230
1299
|
fake_user: bool = False,
|
|
1231
1300
|
split_inputs: bool | None = None,
|
|
1232
1301
|
clear: bool = False,
|
|
1302
|
+
keep_edits: bool | None = None,
|
|
1233
1303
|
) -> TreeBuilder[ShaderNodeTree]:
|
|
1234
1304
|
"""Create a shader node tree."""
|
|
1235
1305
|
return cast(
|
|
@@ -1242,6 +1312,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1242
1312
|
fake_user=fake_user,
|
|
1243
1313
|
split_inputs=split_inputs,
|
|
1244
1314
|
clear=clear,
|
|
1315
|
+
keep_edits=keep_edits,
|
|
1245
1316
|
),
|
|
1246
1317
|
)
|
|
1247
1318
|
|
|
@@ -1255,6 +1326,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1255
1326
|
fake_user: bool = False,
|
|
1256
1327
|
split_inputs: bool | None = None,
|
|
1257
1328
|
clear: bool = False,
|
|
1329
|
+
keep_edits: bool | None = None,
|
|
1258
1330
|
scene: bool = False,
|
|
1259
1331
|
) -> TreeBuilder[CompositorNodeTree]:
|
|
1260
1332
|
"""Create a compositor node tree; ``scene=True`` also makes it the
|
|
@@ -1269,6 +1341,7 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1269
1341
|
fake_user=fake_user,
|
|
1270
1342
|
split_inputs=split_inputs,
|
|
1271
1343
|
clear=clear,
|
|
1344
|
+
keep_edits=keep_edits,
|
|
1272
1345
|
),
|
|
1273
1346
|
)
|
|
1274
1347
|
if scene:
|
|
@@ -1318,6 +1391,8 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1318
1391
|
format: bool = True,
|
|
1319
1392
|
nodebpy_pkg: str = "nodebpy",
|
|
1320
1393
|
in_place: bool = False,
|
|
1394
|
+
groups: Literal["inline", "reference"] = "inline",
|
|
1395
|
+
group_modules: Collection[str] | None = None,
|
|
1321
1396
|
) -> str:
|
|
1322
1397
|
"""Generate Python source that recreates this tree using nodebpy.
|
|
1323
1398
|
|
|
@@ -1336,6 +1411,8 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1336
1411
|
format=format,
|
|
1337
1412
|
nodebpy_pkg=nodebpy_pkg,
|
|
1338
1413
|
in_place=in_place,
|
|
1414
|
+
groups=groups,
|
|
1415
|
+
group_modules=group_modules,
|
|
1339
1416
|
)
|
|
1340
1417
|
|
|
1341
1418
|
def to_mermaid(self, fenced: bool = True) -> str:
|
|
@@ -1415,16 +1492,82 @@ class TreeBuilder[TreeT: NodeTree]:
|
|
|
1415
1492
|
self.split_group_inputs()
|
|
1416
1493
|
if self._arrange is not None:
|
|
1417
1494
|
self.arrange()
|
|
1495
|
+
if self._kept_edits is not None:
|
|
1496
|
+
_keep.restore(self._kept_edits, self.tree)
|
|
1497
|
+
self._kept_edits = None
|
|
1498
|
+
if self._animation is not None:
|
|
1499
|
+
self._animation.restore()
|
|
1500
|
+
self._animation = None
|
|
1418
1501
|
self._apply_input_defaults()
|
|
1419
1502
|
if self._preserved_inputs is not None:
|
|
1420
1503
|
self._preserved_inputs.__exit__(None, None, None)
|
|
1421
1504
|
self._preserved_inputs = None
|
|
1505
|
+
self._warn_invalid_links()
|
|
1506
|
+
if self._record:
|
|
1507
|
+
from ..live import stamp_build
|
|
1508
|
+
|
|
1509
|
+
stamp_build(self.tree)
|
|
1510
|
+
for user in self._unedited_users:
|
|
1511
|
+
stamp_build(user)
|
|
1512
|
+
self._unedited_users = []
|
|
1422
1513
|
# Interface-menu defaults assigned from here on can't wait for a
|
|
1423
1514
|
# context exit that already happened — apply them immediately
|
|
1424
1515
|
# (see MenuSocket.default_value).
|
|
1425
1516
|
self._exited = True
|
|
1426
1517
|
self.deactivate_tree()
|
|
1427
1518
|
|
|
1519
|
+
def invalid_links(self) -> list[bpy.types.NodeLink]:
|
|
1520
|
+
"""Links Blender can't evaluate, drawn red in the node editor: most
|
|
1521
|
+
often a field reaching an input that takes a single value, such as a
|
|
1522
|
+
Repeat Zone's Iterations or Points' Count. Muted links and links to
|
|
1523
|
+
sockets the node's settings hide or grey out aren't evaluated, so they
|
|
1524
|
+
aren't reported."""
|
|
1525
|
+
|
|
1526
|
+
def evaluated(socket: NodeSocket | None) -> bool:
|
|
1527
|
+
return (
|
|
1528
|
+
socket is not None
|
|
1529
|
+
and not socket.is_unavailable
|
|
1530
|
+
and not socket.is_inactive
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
return [
|
|
1534
|
+
link
|
|
1535
|
+
for link in self.tree.links
|
|
1536
|
+
if not link.is_valid
|
|
1537
|
+
and not link.is_muted
|
|
1538
|
+
and not link.is_hidden
|
|
1539
|
+
and evaluated(link.from_socket)
|
|
1540
|
+
and evaluated(link.to_socket)
|
|
1541
|
+
]
|
|
1542
|
+
|
|
1543
|
+
def _warn_invalid_links(self) -> None:
|
|
1544
|
+
invalid = self.invalid_links()
|
|
1545
|
+
if not invalid:
|
|
1546
|
+
return
|
|
1547
|
+
lines = []
|
|
1548
|
+
for link in invalid:
|
|
1549
|
+
source, target = link.from_socket, link.to_socket
|
|
1550
|
+
assert source is not None and target is not None
|
|
1551
|
+
assert link.from_node is not None and link.to_node is not None
|
|
1552
|
+
line = (
|
|
1553
|
+
f" {link.from_node.name}.{source.name} -> "
|
|
1554
|
+
f"{link.to_node.name}.{target.name}"
|
|
1555
|
+
)
|
|
1556
|
+
# Blender draws inputs that only take a single value as a line.
|
|
1557
|
+
if (
|
|
1558
|
+
target.display_shape == "LINE"
|
|
1559
|
+
and source.inferred_structure_type == "FIELD"
|
|
1560
|
+
):
|
|
1561
|
+
line += f" ('{target.name}' takes a single value, not a field)"
|
|
1562
|
+
lines.append(line)
|
|
1563
|
+
count = "1 link" if len(invalid) == 1 else f"{len(invalid)} links"
|
|
1564
|
+
warnings.warn(
|
|
1565
|
+
f"Blender can't evaluate {count} in '{self.tree.name}':\n"
|
|
1566
|
+
+ "\n".join(lines),
|
|
1567
|
+
InvalidLinkWarning,
|
|
1568
|
+
stacklevel=3,
|
|
1569
|
+
)
|
|
1570
|
+
|
|
1428
1571
|
def _apply_input_defaults(self) -> None:
|
|
1429
1572
|
if not self._menu_defaults:
|
|
1430
1573
|
return
|
|
@@ -1880,12 +2023,14 @@ class MaterialBuilder(TreeBuilder[ShaderNodeTree]):
|
|
|
1880
2023
|
fake_user: bool = False,
|
|
1881
2024
|
ignore_visibility: bool = False,
|
|
1882
2025
|
clear: bool = False,
|
|
2026
|
+
keep_edits: bool | None = None,
|
|
1883
2027
|
):
|
|
1884
2028
|
# With ``clear`` an existing material of that name is rebuilt in place
|
|
1885
2029
|
# (objects keep it assigned) instead of creating ``Name.001``. A new
|
|
1886
2030
|
# material starts with Blender's default Principled BSDF and Material
|
|
1887
2031
|
# Output nodes, which are removed so the body builds from empty.
|
|
1888
2032
|
material = bpy.data.materials.get(name) if clear else None
|
|
2033
|
+
reused = material is not None
|
|
1889
2034
|
if material is None:
|
|
1890
2035
|
material = bpy.data.materials.new(name)
|
|
1891
2036
|
assert material is not None
|
|
@@ -1899,6 +2044,7 @@ class MaterialBuilder(TreeBuilder[ShaderNodeTree]):
|
|
|
1899
2044
|
fake_user=fake_user,
|
|
1900
2045
|
ignore_visibility=ignore_visibility,
|
|
1901
2046
|
clear=True,
|
|
2047
|
+
keep_edits=keep_edits if reused else False,
|
|
1902
2048
|
)
|
|
1903
2049
|
|
|
1904
2050
|
|
|
@@ -1915,12 +2061,14 @@ class WorldBuilder(TreeBuilder[ShaderNodeTree]):
|
|
|
1915
2061
|
fake_user: bool = False,
|
|
1916
2062
|
ignore_visibility: bool = False,
|
|
1917
2063
|
clear: bool = False,
|
|
2064
|
+
keep_edits: bool | None = None,
|
|
1918
2065
|
scene: bool = False,
|
|
1919
2066
|
):
|
|
1920
2067
|
# With ``clear`` an existing world of that name is rebuilt in place (a
|
|
1921
2068
|
# scene using it keeps it). A new world's default Background and World
|
|
1922
2069
|
# Output nodes are removed so the body builds from empty.
|
|
1923
2070
|
world = bpy.data.worlds.get(name) if clear else None
|
|
2071
|
+
reused = world is not None
|
|
1924
2072
|
if world is None:
|
|
1925
2073
|
world = bpy.data.worlds.new(name)
|
|
1926
2074
|
assert world is not None and world.node_tree is not None
|
|
@@ -1933,6 +2081,7 @@ class WorldBuilder(TreeBuilder[ShaderNodeTree]):
|
|
|
1933
2081
|
fake_user=fake_user,
|
|
1934
2082
|
ignore_visibility=ignore_visibility,
|
|
1935
2083
|
clear=True,
|
|
2084
|
+
keep_edits=keep_edits if reused else False,
|
|
1936
2085
|
)
|
|
1937
2086
|
if scene:
|
|
1938
2087
|
active = bpy.context.scene
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Group classes that exist in Python already, for
|
|
2
|
+
``to_python(groups="reference")``.
|
|
3
|
+
|
|
4
|
+
A tree can use a group that a loaded class builds or links: one of Blender's
|
|
5
|
+
bundled assets (``g.Array``), one of nodebpy's own groups, a class from a
|
|
6
|
+
generated asset module or one the live script defines. Codegen can then call
|
|
7
|
+
that class instead of writing the group out again.
|
|
8
|
+
|
|
9
|
+
A tree belongs to a class when:
|
|
10
|
+
|
|
11
|
+
- an asset class linked it: the tree is linked from the class's library under
|
|
12
|
+
the asset's name;
|
|
13
|
+
- it was appended from that library (the asset browser's default), which
|
|
14
|
+
leaves a weak reference to the file and the asset's name;
|
|
15
|
+
- it is a local tree with the name the class gives its tree, which is the tree
|
|
16
|
+
the class reuses when it runs. A tree changed by hand since nodebpy built
|
|
17
|
+
it in this session is not matched, so the edits are written out.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import inspect
|
|
24
|
+
import os
|
|
25
|
+
import pkgutil
|
|
26
|
+
import sys
|
|
27
|
+
from collections.abc import Iterable, Iterator
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import bpy
|
|
32
|
+
|
|
33
|
+
from ..builder._utils import typed_param_names
|
|
34
|
+
from ..builder.asset import BundledLibrary, _AssetGroupMixin
|
|
35
|
+
from ..builder.node import NodeGroupBuilder
|
|
36
|
+
|
|
37
|
+
# Modules a script's own classes live in: run_source runs code as
|
|
38
|
+
# __nodebpy_live__, Blender runs scripts and Text blocks as __main__.
|
|
39
|
+
SCRIPT_MODULES = frozenset({"__main__", "__nodebpy_live__"})
|
|
40
|
+
|
|
41
|
+
# Modules holding nodebpy's own group classes, which nothing else imports.
|
|
42
|
+
_BUILTIN_MODULES = ("nodebpy.nodes.geometry.groups",)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class Reference:
|
|
47
|
+
"""How codegen calls the class that builds a group node's tree."""
|
|
48
|
+
|
|
49
|
+
cls: type[NodeGroupBuilder]
|
|
50
|
+
params: dict[str, str] | None
|
|
51
|
+
"""Socket identifier → ``__init__`` parameter for a typed class, or
|
|
52
|
+
``None`` to pass inputs by socket name."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def import_modules(names: Iterable[str]) -> None:
|
|
56
|
+
"""Import each module, and every submodule of a package, so the group
|
|
57
|
+
classes they define can be found."""
|
|
58
|
+
for name in names:
|
|
59
|
+
module = importlib.import_module(name)
|
|
60
|
+
if hasattr(module, "__path__"):
|
|
61
|
+
for info in pkgutil.walk_packages(module.__path__, f"{name}."):
|
|
62
|
+
importlib.import_module(info.name)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def configured_modules(start: str | Path | None = None) -> list[str]:
|
|
66
|
+
"""The package holding the dumped sources that ``[tool.nodebpy.assets]``
|
|
67
|
+
names in the nearest ``pyproject.toml``, if it can be imported from a
|
|
68
|
+
``sys.path`` entry."""
|
|
69
|
+
from ..assets._pipeline import load_config
|
|
70
|
+
|
|
71
|
+
table, root = load_config(start)
|
|
72
|
+
source = table.get("source")
|
|
73
|
+
if not source or root is None:
|
|
74
|
+
return []
|
|
75
|
+
path = (root / source).resolve()
|
|
76
|
+
names = []
|
|
77
|
+
for entry in sys.path:
|
|
78
|
+
try:
|
|
79
|
+
parts = path.relative_to(Path(entry or ".").resolve()).parts
|
|
80
|
+
except ValueError:
|
|
81
|
+
continue
|
|
82
|
+
if parts and all(part.isidentifier() for part in parts):
|
|
83
|
+
names.append(".".join(parts))
|
|
84
|
+
return [min(names, key=len)] if names else []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _subclasses[T](cls: type[T]) -> Iterator[type[T]]:
|
|
88
|
+
for sub in cls.__subclasses__():
|
|
89
|
+
yield sub
|
|
90
|
+
yield from _subclasses(sub)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _group_classes() -> list[type[NodeGroupBuilder]]:
|
|
94
|
+
import_modules(_BUILTIN_MODULES)
|
|
95
|
+
return [
|
|
96
|
+
cls
|
|
97
|
+
for cls in dict.fromkeys(_subclasses(NodeGroupBuilder))
|
|
98
|
+
if not inspect.isabstract(cls)
|
|
99
|
+
and (getattr(cls, "_asset_name", None) or getattr(cls, "_name", None))
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _path(path: str) -> str:
|
|
104
|
+
return os.path.normcase(os.path.realpath(bpy.path.abspath(path)))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _from_library(cls: type[_AssetGroupMixin], path: str) -> bool:
|
|
108
|
+
library = cls._library
|
|
109
|
+
if isinstance(library, BundledLibrary):
|
|
110
|
+
# Blender installs differ, the bundled file names don't.
|
|
111
|
+
return os.path.basename(bpy.path.abspath(path)) == library.filename
|
|
112
|
+
return _path(path) == _path(library.path())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _builds(cls: type[NodeGroupBuilder], tree: bpy.types.NodeTree) -> bool:
|
|
116
|
+
if cls._tree_idname != tree.bl_idname:
|
|
117
|
+
return False
|
|
118
|
+
if issubclass(cls, _AssetGroupMixin):
|
|
119
|
+
name = cls._asset_name
|
|
120
|
+
if tree.library is not None:
|
|
121
|
+
return tree.name == name and _from_library(cls, tree.library.filepath)
|
|
122
|
+
weak = tree.library_weak_reference
|
|
123
|
+
if weak is not None:
|
|
124
|
+
return weak.id_name[2:] == name and _from_library(cls, weak.filepath)
|
|
125
|
+
return tree.name == name
|
|
126
|
+
# A class choosing its tree's name per instance can't be matched by name.
|
|
127
|
+
return (
|
|
128
|
+
tree.library is None
|
|
129
|
+
and cls._group_name is NodeGroupBuilder._group_name
|
|
130
|
+
and tree.name == getattr(cls, "_name", None)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def find_class(tree: bpy.types.NodeTree) -> type[NodeGroupBuilder] | None:
|
|
135
|
+
"""The loaded class that builds or links *tree*, the latest defined when
|
|
136
|
+
several do (a live script defines its classes again on every run)."""
|
|
137
|
+
from ..live import was_edited
|
|
138
|
+
|
|
139
|
+
if tree.library is None and was_edited(tree):
|
|
140
|
+
return None
|
|
141
|
+
found = [cls for cls in _group_classes() if _builds(cls, tree)]
|
|
142
|
+
return found[-1] if found else None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def reference(node: bpy.types.Node) -> Reference | None:
|
|
146
|
+
"""How to call the class that builds *node*'s group, or ``None`` when no
|
|
147
|
+
loaded class does or its ``__init__`` can't take the node's inputs."""
|
|
148
|
+
tree = getattr(node, "node_tree", None)
|
|
149
|
+
cls = find_class(tree) if tree is not None else None
|
|
150
|
+
if cls is None:
|
|
151
|
+
return None
|
|
152
|
+
if cls.__init__ is NodeGroupBuilder.__init__:
|
|
153
|
+
return Reference(cls, None)
|
|
154
|
+
parameters = list(inspect.signature(cls.__init__).parameters.values())[1:]
|
|
155
|
+
named = {
|
|
156
|
+
p.name: p
|
|
157
|
+
for p in parameters
|
|
158
|
+
if p.kind in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY)
|
|
159
|
+
}
|
|
160
|
+
if not named and any(p.kind is p.VAR_KEYWORD for p in parameters):
|
|
161
|
+
# ``__init__(self, **kwargs)`` passes socket names through.
|
|
162
|
+
return Reference(cls, None)
|
|
163
|
+
params = typed_param_names(node.inputs)
|
|
164
|
+
if not set(params.values()) <= named.keys() or any(
|
|
165
|
+
p.default is p.empty for p in named.values()
|
|
166
|
+
):
|
|
167
|
+
return None
|
|
168
|
+
return Reference(cls, params)
|