nodebpy 520.27.0__tar.gz → 520.29.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.29.0}/PKG-INFO +1 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/pyproject.toml +1 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/__init__.py +2 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/tree.py +26 -1
- nodebpy-520.29.0/src/nodebpy/export/_floats.py +114 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/export/codegen.py +22 -106
- nodebpy-520.29.0/src/nodebpy/live.py +361 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/color.py +39 -39
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/converter.py +51 -42
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/distort.py +27 -27
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/filter.py +81 -67
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/input.py +6 -5
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/interface.py +4 -4
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/manual.py +2 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/matte.py +21 -20
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/output.py +1 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/assets.py +9 -8
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/attribute.py +2 -2
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/color.py +2 -2
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/converter.py +75 -66
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/geometry.py +157 -146
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/grid.py +14 -14
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/input.py +1 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/interface.py +9 -9
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/manual.py +2 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/texture.py +38 -35
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/utilities.py +1 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/vector.py +88 -58
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/color.py +3 -3
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/converter.py +25 -17
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/grid.py +9 -9
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/input.py +7 -7
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/manual.py +2 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/output.py +3 -3
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/shader.py +58 -58
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/texture.py +11 -10
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/vector.py +27 -27
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/types.py +8 -1
- {nodebpy-520.27.0 → nodebpy-520.29.0}/README.md +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/assets/__main__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/assets/_codegen.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/assets/_library.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/assets/_pipeline.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/_socket_order.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/_utils.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/layout.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/export/parity.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/export/plot.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/export/web_render.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/VENDORED.md +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/balancing.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/_mixins.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/assets.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.27.0 → nodebpy-520.29.0}/src/nodebpy/nodes/shader/script.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:
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Float formatting shared by ``to_python`` export and the node-class generator.
|
|
2
|
+
|
|
3
|
+
Blender stores socket values as float32. This module renders such a value as
|
|
4
|
+
the shortest Python literal that rebuilds the identical float32 (``0.1`` rather
|
|
5
|
+
than ``0.10000000149011612``), or as a ``math.pi`` / ``math.tau`` / ``math.e``
|
|
6
|
+
expression for the rational multiples of those constants (``3 * math.pi / 4``).
|
|
7
|
+
|
|
8
|
+
It is a leaf: stdlib and numpy only, no ``nodebpy`` imports. The generator in
|
|
9
|
+
``gen/`` loads it straight from this file (see ``gen/config.py``) so that a
|
|
10
|
+
broken generated tree can never block regenerating it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def within_one_ulp(candidate: float, f32) -> bool:
|
|
21
|
+
"""Whether ``candidate`` lands on ``f32`` or its immediate float32
|
|
22
|
+
neighbour. One ULP is the round-off a value picks up crossing precisions
|
|
23
|
+
(an authored ``0.15`` surviving as ``0.14999999``), so snapping across it
|
|
24
|
+
restores the authored constant without moving any genuinely different
|
|
25
|
+
value."""
|
|
26
|
+
# A candidate beyond float32 range (a huge clamp bound) overflows the
|
|
27
|
+
# cast to inf with a RuntimeWarning; it is never within one ULP.
|
|
28
|
+
with np.errstate(over="ignore"):
|
|
29
|
+
c32 = np.float32(candidate)
|
|
30
|
+
if not np.isfinite(c32):
|
|
31
|
+
return False
|
|
32
|
+
return bool(c32 == f32 or np.nextafter(f32, c32) == c32)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def math_constant_expression(f32) -> str | None:
|
|
36
|
+
"""A ``math.pi``/``math.tau``/``math.e`` expression for a rational
|
|
37
|
+
multiple of one of those constants, or None.
|
|
38
|
+
|
|
39
|
+
Constants authored as expressions (``2 * math.pi``, ``pi / 3``, ``tau``,
|
|
40
|
+
``e``) survive in a blend only as float32 values; matching small-fraction
|
|
41
|
+
multiples (within one ULP) restores the readable form. Numerators up to 48
|
|
42
|
+
over denominators up to 12 cover the usual turns and subdivisions while
|
|
43
|
+
keeping a chance coincidence with an ordinary decimal essentially
|
|
44
|
+
impossible. An even multiple of pi renders in tau form (``math.tau``,
|
|
45
|
+
``math.tau / 3``) — with the fraction reduced, halving the numerator is
|
|
46
|
+
always the simpler expression.
|
|
47
|
+
"""
|
|
48
|
+
magnitude = abs(float(f32))
|
|
49
|
+
if magnitude == 0.0:
|
|
50
|
+
return None
|
|
51
|
+
for name, const in (("pi", math.pi), ("e", math.e)):
|
|
52
|
+
for den in range(1, 13):
|
|
53
|
+
num = round(magnitude * den / const)
|
|
54
|
+
if not 1 <= num <= 48 or math.gcd(num, den) != 1:
|
|
55
|
+
continue
|
|
56
|
+
if not within_one_ulp(num * const / den, abs(f32)):
|
|
57
|
+
continue
|
|
58
|
+
if name == "pi" and num % 2 == 0:
|
|
59
|
+
name, num = "tau", num // 2
|
|
60
|
+
expr = f"math.{name}" if num == 1 else f"{num} * math.{name}"
|
|
61
|
+
if den > 1:
|
|
62
|
+
expr = f"{expr} / {den}"
|
|
63
|
+
return f"-{expr}" if float(f32) < 0 else expr
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def group_digits(text: str) -> str:
|
|
68
|
+
"""Underscore-group a positional float literal's integer part when it has
|
|
69
|
+
five or more digits (``10000.0`` → ``10_000.0``)."""
|
|
70
|
+
sign = ""
|
|
71
|
+
if text.startswith("-"):
|
|
72
|
+
sign, text = "-", text[1:]
|
|
73
|
+
int_part, dot, frac = text.partition(".")
|
|
74
|
+
if len(int_part) < 5:
|
|
75
|
+
return sign + text
|
|
76
|
+
return sign + f"{int(int_part):_}" + dot + frac
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def fmt_float(value: float, *, snap: bool = True) -> str:
|
|
80
|
+
"""Shortest readable literal for a float32-backed value.
|
|
81
|
+
|
|
82
|
+
Blender stores socket values as float32, so reading them back through
|
|
83
|
+
Python gives noisy float64 reprs (``0.10000000149011612``); the shortest
|
|
84
|
+
decimal that uniquely identifies the float32 (``0.1``) rebuilds the
|
|
85
|
+
identical socket value. On top of that, rational multiples of pi, tau
|
|
86
|
+
and e render as ``math.*`` expressions, a value one ULP off a much shorter decimal
|
|
87
|
+
snaps to it (``0.14999999`` → ``0.15``), and long integer parts get
|
|
88
|
+
underscore grouping (``-10_000.0``). ``snap=False`` keeps the literal that
|
|
89
|
+
rebuilds exactly this float32 (a generated default must be Blender's own
|
|
90
|
+
value, not its prettier neighbour).
|
|
91
|
+
"""
|
|
92
|
+
if not math.isfinite(value):
|
|
93
|
+
return repr(value)
|
|
94
|
+
f32 = np.float32(value)
|
|
95
|
+
if float(f32) != value:
|
|
96
|
+
return repr(value) # genuine float64 — keep full precision
|
|
97
|
+
const_expr = math_constant_expression(f32)
|
|
98
|
+
if const_expr is not None:
|
|
99
|
+
return const_expr
|
|
100
|
+
text = np.format_float_positional(f32, unique=True, trim="0")
|
|
101
|
+
# Snap to the shortest decimal within one ULP: fewer significant digits
|
|
102
|
+
# first, so ``0.14999999`` becomes ``0.15`` rather than ``0.1499999``.
|
|
103
|
+
digits = sum(c.isdigit() for c in text.lstrip("-0.")) if snap else 0
|
|
104
|
+
for sig in range(1, digits):
|
|
105
|
+
candidate = float(f"%.{sig}g" % float(f32))
|
|
106
|
+
if not within_one_ulp(candidate, f32):
|
|
107
|
+
continue
|
|
108
|
+
snapped = np.format_float_positional(
|
|
109
|
+
np.float32(candidate), unique=True, trim="0"
|
|
110
|
+
)
|
|
111
|
+
if len(snapped) < len(text):
|
|
112
|
+
text = snapped
|
|
113
|
+
break
|
|
114
|
+
return group_digits(text)
|
|
@@ -31,8 +31,13 @@ from collections.abc import Callable, Collection, Mapping
|
|
|
31
31
|
from dataclasses import dataclass, field
|
|
32
32
|
from typing import TYPE_CHECKING, Any, Literal, NamedTuple
|
|
33
33
|
|
|
34
|
+
import numpy as np
|
|
34
35
|
from bpy.types import ID, FunctionNodeCompare, NodeTree
|
|
35
36
|
|
|
37
|
+
from ._floats import (
|
|
38
|
+
fmt_float as _fmt_float,
|
|
39
|
+
)
|
|
40
|
+
|
|
36
41
|
if TYPE_CHECKING:
|
|
37
42
|
from ..builder.tree import TreeBuilder
|
|
38
43
|
|
|
@@ -354,111 +359,6 @@ def _stmt_lines(
|
|
|
354
359
|
# ---------------------------------------------------------------------------
|
|
355
360
|
|
|
356
361
|
|
|
357
|
-
def _within_one_ulp(candidate: float, f32) -> bool:
|
|
358
|
-
"""Whether ``candidate`` lands on ``f32`` or its immediate float32
|
|
359
|
-
neighbour. One ULP is the round-off a value picks up crossing precisions
|
|
360
|
-
(an authored ``0.15`` surviving as ``0.14999999``), so snapping across it
|
|
361
|
-
restores the authored constant without moving any genuinely different
|
|
362
|
-
value."""
|
|
363
|
-
import numpy as np
|
|
364
|
-
|
|
365
|
-
# A candidate beyond float32 range (a huge clamp bound) overflows the
|
|
366
|
-
# cast to inf with a RuntimeWarning; it is never within one ULP.
|
|
367
|
-
with np.errstate(over="ignore"):
|
|
368
|
-
c32 = np.float32(candidate)
|
|
369
|
-
if not np.isfinite(c32):
|
|
370
|
-
return False
|
|
371
|
-
return bool(c32 == f32 or np.nextafter(f32, c32) == c32)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
def _math_constant_expression(f32) -> str | None:
|
|
375
|
-
"""A ``math.pi``/``math.tau``/``math.e`` expression for a rational
|
|
376
|
-
multiple of one of those constants, or None.
|
|
377
|
-
|
|
378
|
-
Constants authored as expressions (``2 * math.pi``, ``pi / 3``, ``tau``,
|
|
379
|
-
``e``) survive in a blend only as float32 values; matching small-fraction
|
|
380
|
-
multiples (within one ULP) restores the readable form. Numerators up to 48
|
|
381
|
-
over denominators up to 12 cover the usual turns and subdivisions while
|
|
382
|
-
keeping a chance coincidence with an ordinary decimal essentially
|
|
383
|
-
impossible. An even multiple of pi renders in tau form (``math.tau``,
|
|
384
|
-
``math.tau / 3``) — with the fraction reduced, halving the numerator is
|
|
385
|
-
always the simpler expression.
|
|
386
|
-
"""
|
|
387
|
-
import math
|
|
388
|
-
|
|
389
|
-
magnitude = abs(float(f32))
|
|
390
|
-
if magnitude == 0.0:
|
|
391
|
-
return None
|
|
392
|
-
for name, const in (("pi", math.pi), ("e", math.e)):
|
|
393
|
-
for den in range(1, 13):
|
|
394
|
-
num = round(magnitude * den / const)
|
|
395
|
-
if not 1 <= num <= 48 or math.gcd(num, den) != 1:
|
|
396
|
-
continue
|
|
397
|
-
if not _within_one_ulp(num * const / den, abs(f32)):
|
|
398
|
-
continue
|
|
399
|
-
if name == "pi" and num % 2 == 0:
|
|
400
|
-
name, num = "tau", num // 2
|
|
401
|
-
expr = f"math.{name}" if num == 1 else f"{num} * math.{name}"
|
|
402
|
-
if den > 1:
|
|
403
|
-
expr = f"{expr} / {den}"
|
|
404
|
-
return f"-{expr}" if float(f32) < 0 else expr
|
|
405
|
-
return None
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
def _group_digits(text: str) -> str:
|
|
409
|
-
"""Underscore-group a positional float literal's integer part when it has
|
|
410
|
-
five or more digits (``10000.0`` → ``10_000.0``)."""
|
|
411
|
-
sign = ""
|
|
412
|
-
if text.startswith("-"):
|
|
413
|
-
sign, text = "-", text[1:]
|
|
414
|
-
int_part, dot, frac = text.partition(".")
|
|
415
|
-
if len(int_part) < 5:
|
|
416
|
-
return sign + text
|
|
417
|
-
return sign + f"{int(int_part):_}" + dot + frac
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def _fmt_float(value: float) -> str:
|
|
421
|
-
"""Shortest readable literal for a float32-backed value.
|
|
422
|
-
|
|
423
|
-
Blender stores socket values as float32, so reading them back through
|
|
424
|
-
Python gives noisy float64 reprs (``0.10000000149011612``); the shortest
|
|
425
|
-
decimal that uniquely identifies the float32 (``0.1``) rebuilds the
|
|
426
|
-
identical socket value. On top of that, rational multiples of pi, tau
|
|
427
|
-
and e render as ``math.*`` expressions, a value one ULP off a much shorter decimal
|
|
428
|
-
snaps to it (``0.14999999`` → ``0.15``), and long integer parts get
|
|
429
|
-
underscore grouping (``-10_000.0``).
|
|
430
|
-
"""
|
|
431
|
-
import math
|
|
432
|
-
|
|
433
|
-
if not math.isfinite(value):
|
|
434
|
-
return repr(value)
|
|
435
|
-
try:
|
|
436
|
-
import numpy as np
|
|
437
|
-
except ImportError: # pragma: no cover - Blender ships numpy
|
|
438
|
-
return repr(value)
|
|
439
|
-
f32 = np.float32(value)
|
|
440
|
-
if float(f32) != value:
|
|
441
|
-
return repr(value) # genuine float64 — keep full precision
|
|
442
|
-
const_expr = _math_constant_expression(f32)
|
|
443
|
-
if const_expr is not None:
|
|
444
|
-
return const_expr
|
|
445
|
-
text = np.format_float_positional(f32, unique=True, trim="0")
|
|
446
|
-
# Snap to the shortest decimal within one ULP: fewer significant digits
|
|
447
|
-
# first, so ``0.14999999`` becomes ``0.15`` rather than ``0.1499999``.
|
|
448
|
-
digits = sum(c.isdigit() for c in text.lstrip("-0."))
|
|
449
|
-
for sig in range(1, digits):
|
|
450
|
-
candidate = float(f"%.{sig}g" % float(f32))
|
|
451
|
-
if not _within_one_ulp(candidate, f32):
|
|
452
|
-
continue
|
|
453
|
-
snapped = np.format_float_positional(
|
|
454
|
-
np.float32(candidate), unique=True, trim="0"
|
|
455
|
-
)
|
|
456
|
-
if len(snapped) < len(text):
|
|
457
|
-
text = snapped
|
|
458
|
-
break
|
|
459
|
-
return _group_digits(text)
|
|
460
|
-
|
|
461
|
-
|
|
462
362
|
def _fmt(value: Any) -> str:
|
|
463
363
|
"""Format a value as a Python literal using double-quoted strings."""
|
|
464
364
|
if isinstance(value, bool):
|
|
@@ -507,7 +407,9 @@ def _eq(a: Any, b: Any) -> bool:
|
|
|
507
407
|
return False
|
|
508
408
|
return all(_eq(x, y) for x, y in zip(a, b))
|
|
509
409
|
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
|
510
|
-
|
|
410
|
+
# Sockets store float32: a Python default of ``0.05`` reads back
|
|
411
|
+
# as ``0.05000000074505806``, so compare at that precision.
|
|
412
|
+
return bool(np.float32(a) == np.float32(b))
|
|
511
413
|
return a == b
|
|
512
414
|
except Exception: # noqa: BLE001
|
|
513
415
|
return False
|
|
@@ -3724,6 +3626,7 @@ def to_python(
|
|
|
3724
3626
|
external_groups: Collection[str] | None = None,
|
|
3725
3627
|
typed_groups: Collection[str] | None = None,
|
|
3726
3628
|
root_interface: GroupInterface | None = None,
|
|
3629
|
+
in_place: bool = False,
|
|
3727
3630
|
) -> str:
|
|
3728
3631
|
"""Generate Python code that recreates the given node tree using nodebpy.
|
|
3729
3632
|
|
|
@@ -3794,6 +3697,17 @@ def to_python(
|
|
|
3794
3697
|
Typed-interface parts (docstring, class attributes, accessors and
|
|
3795
3698
|
``__init__``) spliced into the top-level tree's class in ``class``
|
|
3796
3699
|
mode, ahead of ``_build_group``.
|
|
3700
|
+
in_place: bool
|
|
3701
|
+
If True, the ``with`` header is emitted as
|
|
3702
|
+
``with g.tree("Name", clear=True) as tree:`` so running the source
|
|
3703
|
+
rebuilds the exported tree in place — the same datablock, emptied
|
|
3704
|
+
first — instead of creating a ``Name.001`` copy, keeping modifiers,
|
|
3705
|
+
group nodes and pinned editors attached across re-runs. Only the
|
|
3706
|
+
top-level tree is rebuilt this way: nested groups are emitted as
|
|
3707
|
+
``Custom*Group`` classes whose ``create_group()`` reuses an existing
|
|
3708
|
+
tree of the same name, so edits to them only take effect through
|
|
3709
|
+
:func:`nodebpy.live.run_source`. Ignored with ``top_level="class"``
|
|
3710
|
+
(a class is rebuilt by ``run_source``, not by a header).
|
|
3797
3711
|
|
|
3798
3712
|
Returns
|
|
3799
3713
|
-------
|
|
@@ -3876,6 +3790,8 @@ def to_python(
|
|
|
3876
3790
|
# for either option.
|
|
3877
3791
|
if snapshot_positions or keep_reroutes:
|
|
3878
3792
|
ctor_args.append("arrange=None")
|
|
3793
|
+
if in_place:
|
|
3794
|
+
ctor_args.append("clear=True")
|
|
3879
3795
|
lines.append(f"with {constructor}({', '.join(ctor_args)}) as tree:")
|
|
3880
3796
|
lines.extend(_assemble_tree_body(emission))
|
|
3881
3797
|
|