procfunc 0.30.0__py3-none-any.whl
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.
- procfunc/__init__.py +87 -0
- procfunc/color.py +57 -0
- procfunc/compute_graph/__init__.py +28 -0
- procfunc/compute_graph/compute_graph.py +115 -0
- procfunc/compute_graph/node.py +200 -0
- procfunc/compute_graph/operators_info.py +92 -0
- procfunc/compute_graph/proxy.py +173 -0
- procfunc/compute_graph/util.py +282 -0
- procfunc/context.py +115 -0
- procfunc/control.py +174 -0
- procfunc/nodes/__init__.py +66 -0
- procfunc/nodes/bindings_util.py +196 -0
- procfunc/nodes/bpy_node_info.py +280 -0
- procfunc/nodes/compositor.py +2242 -0
- procfunc/nodes/execute/construct_nodes.py +571 -0
- procfunc/nodes/execute/construct_special_cases.py +246 -0
- procfunc/nodes/execute/execute.py +548 -0
- procfunc/nodes/execute/infer_runtime_data_type.py +195 -0
- procfunc/nodes/execute/util.py +247 -0
- procfunc/nodes/func.py +1417 -0
- procfunc/nodes/geo.py +4240 -0
- procfunc/nodes/manifest.json +8769 -0
- procfunc/nodes/math.py +644 -0
- procfunc/nodes/node_function.py +160 -0
- procfunc/nodes/shader.py +2359 -0
- procfunc/nodes/types.py +347 -0
- procfunc/ops/__init__.py +35 -0
- procfunc/ops/_util.py +275 -0
- procfunc/ops/addons.py +59 -0
- procfunc/ops/attr.py +426 -0
- procfunc/ops/collection.py +90 -0
- procfunc/ops/curve.py +18 -0
- procfunc/ops/file.py +126 -0
- procfunc/ops/manifest.json +39149 -0
- procfunc/ops/mesh.py +1510 -0
- procfunc/ops/modifier.py +603 -0
- procfunc/ops/object.py +258 -0
- procfunc/ops/primitives/__init__.py +31 -0
- procfunc/ops/primitives/camera.py +45 -0
- procfunc/ops/primitives/curve.py +71 -0
- procfunc/ops/primitives/light.py +114 -0
- procfunc/ops/primitives/mesh.py +358 -0
- procfunc/ops/uv.py +271 -0
- procfunc/random.py +247 -0
- procfunc/tracer/__init__.py +43 -0
- procfunc/tracer/decorator.py +121 -0
- procfunc/tracer/patch.py +494 -0
- procfunc/tracer/proxy.py +127 -0
- procfunc/tracer/trace.py +222 -0
- procfunc/transforms/__init__.py +49 -0
- procfunc/transforms/cleanup.py +214 -0
- procfunc/transforms/convert.py +20 -0
- procfunc/transforms/distribution.py +191 -0
- procfunc/transforms/extract_materials.py +116 -0
- procfunc/transforms/infer_distribution.py +326 -0
- procfunc/transforms/parameters.py +15 -0
- procfunc/transforms/util.py +35 -0
- procfunc/transpiler/__init__.py +24 -0
- procfunc/transpiler/bpy_to_computegraph.py +1348 -0
- procfunc/transpiler/codegen.py +919 -0
- procfunc/transpiler/identifiers.py +595 -0
- procfunc/transpiler/main.py +299 -0
- procfunc/types.py +380 -0
- procfunc/util/__init__.py +0 -0
- procfunc/util/bpy_info.py +145 -0
- procfunc/util/camera.py +0 -0
- procfunc/util/keyframe.py +70 -0
- procfunc/util/log.py +96 -0
- procfunc/util/manifest.py +121 -0
- procfunc/util/pytree.py +343 -0
- procfunc/util/teardown.py +37 -0
- procfunc-0.30.0.dist-info/METADATA +120 -0
- procfunc-0.30.0.dist-info/RECORD +76 -0
- procfunc-0.30.0.dist-info/WHEEL +5 -0
- procfunc-0.30.0.dist-info/licenses/LICENSE.md +11 -0
- procfunc-0.30.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import itertools
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable, Literal
|
|
6
|
+
|
|
7
|
+
import bpy
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
import procfunc as pf
|
|
11
|
+
from procfunc import compute_graph as cg
|
|
12
|
+
from procfunc import transforms as tr
|
|
13
|
+
from procfunc.nodes import NODE_OPERATOR_TABLE
|
|
14
|
+
from procfunc.util import pytree
|
|
15
|
+
from procfunc.util.teardown import skip_teardown_on_exit
|
|
16
|
+
|
|
17
|
+
from .bpy_to_computegraph import (
|
|
18
|
+
ParseMemo,
|
|
19
|
+
parse_material,
|
|
20
|
+
parse_node_tree,
|
|
21
|
+
parse_object,
|
|
22
|
+
)
|
|
23
|
+
from .codegen import default_func_resolution_map, to_python
|
|
24
|
+
|
|
25
|
+
logging.basicConfig(
|
|
26
|
+
format="[%(asctime)s.%(msecs)03d] [%(module)s] [%(levelname)s] | %(message)s",
|
|
27
|
+
datefmt="%H:%M:%S",
|
|
28
|
+
level=logging.INFO,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
MODIFIERS_MANIFEST: pd.DataFrame | None = None
|
|
34
|
+
NODES_MANIFEST: pd.DataFrame | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
BPY_COLLECTIONS = [
|
|
38
|
+
"scene",
|
|
39
|
+
"object",
|
|
40
|
+
"material",
|
|
41
|
+
# "collections",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
RETURN_TYPES = {
|
|
45
|
+
"object": pf.MeshObject,
|
|
46
|
+
"material": pf.Material,
|
|
47
|
+
"collection": pf.Collection,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_parser():
|
|
52
|
+
parser = argparse.ArgumentParser()
|
|
53
|
+
|
|
54
|
+
parser.add_argument("input", type=Path)
|
|
55
|
+
|
|
56
|
+
parser.add_argument("--materials", type=str, default=[], nargs="+")
|
|
57
|
+
parser.add_argument("--objects", type=str, default=[], nargs="+")
|
|
58
|
+
parser.add_argument("--node_trees", type=str, default=[], nargs="+")
|
|
59
|
+
|
|
60
|
+
parser.add_argument("--output", type=Path, default=None)
|
|
61
|
+
parser.add_argument("--transforms", type=str, default=[], nargs="+")
|
|
62
|
+
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--object_mode",
|
|
65
|
+
type=str,
|
|
66
|
+
choices=["monkey", "active", "named"],
|
|
67
|
+
default="monkey",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument("--no_version_comments", action="store_true")
|
|
70
|
+
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"-d",
|
|
73
|
+
"--debug",
|
|
74
|
+
action="store_const",
|
|
75
|
+
dest="loglevel",
|
|
76
|
+
const=logging.DEBUG,
|
|
77
|
+
default=logging.INFO,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
parser.add_argument("--add_line_comments", action="store_true")
|
|
81
|
+
parser.add_argument("--include_object_materials", action="store_true")
|
|
82
|
+
|
|
83
|
+
return parser
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_target(
|
|
87
|
+
target: bpy.types.Object | bpy.types.Material | bpy.types.NodeTree,
|
|
88
|
+
memo: ParseMemo,
|
|
89
|
+
object_mode: Literal["monkey", "active", "named"] = "monkey",
|
|
90
|
+
include_set_material: bool = True,
|
|
91
|
+
) -> cg.ComputeGraph:
|
|
92
|
+
match target:
|
|
93
|
+
# case pf.Scene:
|
|
94
|
+
# return parse_scene(bpy.context.scene, memo)
|
|
95
|
+
case bpy.types.Object():
|
|
96
|
+
return parse_object(
|
|
97
|
+
target,
|
|
98
|
+
memo,
|
|
99
|
+
object_mode=object_mode,
|
|
100
|
+
include_set_material=include_set_material,
|
|
101
|
+
)
|
|
102
|
+
case bpy.types.Material():
|
|
103
|
+
return parse_material(target, memo)
|
|
104
|
+
case bpy.types.NodeTree():
|
|
105
|
+
graph, _ = parse_node_tree(target, memo)
|
|
106
|
+
return graph
|
|
107
|
+
case x:
|
|
108
|
+
raise ValueError(f"Invalid {x=} {type(x)=}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _find_target_str(target: str, col: bpy.types.bpy_prop_collection) -> list:
|
|
112
|
+
if target.isdigit():
|
|
113
|
+
return [col[int(target)]]
|
|
114
|
+
elif "*" in target:
|
|
115
|
+
assert target.count("*") == 1
|
|
116
|
+
assert target.endswith("*") == 0
|
|
117
|
+
prefix = target.replace("*", "")
|
|
118
|
+
return [v for k, v in col.items() if k.startswith(prefix)]
|
|
119
|
+
else:
|
|
120
|
+
matches = [v for k, v in col.items() if k == target]
|
|
121
|
+
if len(matches) != 1:
|
|
122
|
+
available = " ".join(col.keys())
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"Found {len(matches)} targets for {target=} in {available=}"
|
|
125
|
+
)
|
|
126
|
+
return [matches[0]]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def transpile_targets(
|
|
130
|
+
targets: list[bpy.types.Object | bpy.types.Material | bpy.types.NodeTree],
|
|
131
|
+
transforms: list[Callable[[list[cg.ComputeGraph]], list[cg.ComputeGraph]]],
|
|
132
|
+
object_mode: Literal["monkey", "active", "named"] = "monkey",
|
|
133
|
+
add_version_comment: bool = True,
|
|
134
|
+
add_line_comments: bool = False,
|
|
135
|
+
include_set_material: bool = True,
|
|
136
|
+
) -> str:
|
|
137
|
+
memo = ParseMemo()
|
|
138
|
+
|
|
139
|
+
result_graphs = [
|
|
140
|
+
parse_target(
|
|
141
|
+
target,
|
|
142
|
+
memo,
|
|
143
|
+
object_mode=object_mode,
|
|
144
|
+
include_set_material=include_set_material,
|
|
145
|
+
)
|
|
146
|
+
for target in targets
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for tfunc in transforms:
|
|
150
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
151
|
+
logger.debug(f"Applying transform {tfunc} to {result_graphs=}")
|
|
152
|
+
result_graphs = tfunc(result_graphs)
|
|
153
|
+
|
|
154
|
+
vec = cg.FunctionCallNode(pf.nodes.shader.coord, (), {})
|
|
155
|
+
vec = cg.GetAttributeNode(vec, "generated")
|
|
156
|
+
|
|
157
|
+
result_calls = []
|
|
158
|
+
for result_graph in result_graphs:
|
|
159
|
+
kwargs = {}
|
|
160
|
+
if result_graph.name.startswith("material_"):
|
|
161
|
+
kwargs["vector"] = vec
|
|
162
|
+
|
|
163
|
+
return_type = RETURN_TYPES.get(type(result_graph), None)
|
|
164
|
+
call_node = cg.SubgraphCallNode(
|
|
165
|
+
subgraph=result_graph,
|
|
166
|
+
args=(),
|
|
167
|
+
kwargs=kwargs,
|
|
168
|
+
metadata={"known_value_type": return_type},
|
|
169
|
+
)
|
|
170
|
+
result_calls.append(call_node)
|
|
171
|
+
|
|
172
|
+
toplevel_name = "toplevel_invoke"
|
|
173
|
+
graph = cg.ComputeGraph(
|
|
174
|
+
inputs=pytree.PyTree(()),
|
|
175
|
+
outputs=pytree.PyTree({"result": result_calls}),
|
|
176
|
+
name=toplevel_name,
|
|
177
|
+
metadata={},
|
|
178
|
+
)
|
|
179
|
+
assert isinstance(graph, cg.ComputeGraph), graph
|
|
180
|
+
|
|
181
|
+
func_resolution, import_lines = default_func_resolution_map(graph)
|
|
182
|
+
for oprow in NODE_OPERATOR_TABLE:
|
|
183
|
+
func_resolution[oprow.pf_func] = oprow.operator_type
|
|
184
|
+
|
|
185
|
+
python = to_python(
|
|
186
|
+
graph,
|
|
187
|
+
func_resolution=func_resolution,
|
|
188
|
+
import_lines=import_lines,
|
|
189
|
+
add_version_comment=add_version_comment,
|
|
190
|
+
add_line_comments=add_line_comments,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return python
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def inject_into_python_file(python: list[str], file: Path, target_name: str):
|
|
197
|
+
"""
|
|
198
|
+
If def <target_name>(): is found in the python file, replace it with the python code.
|
|
199
|
+
If not, add it to end of the file.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
text = file.read_text()
|
|
203
|
+
lines = text.splitlines()
|
|
204
|
+
|
|
205
|
+
start_line = next(
|
|
206
|
+
(i for i, line in enumerate(lines) if line.startswith(f"def {target_name}")),
|
|
207
|
+
None,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if start_line is None:
|
|
211
|
+
lines.append
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _targets_from_args(
|
|
215
|
+
args: argparse.Namespace,
|
|
216
|
+
) -> list[bpy.types.Object | bpy.types.Material | bpy.types.NodeTree]:
|
|
217
|
+
targets = []
|
|
218
|
+
targets += [
|
|
219
|
+
_find_target_str(target, bpy.data.materials) for target in args.materials
|
|
220
|
+
]
|
|
221
|
+
for target in args.objects:
|
|
222
|
+
if target == "ACTIVE":
|
|
223
|
+
if bpy.context.active_object is None:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"--objects ACTIVE specified but no active object in scene"
|
|
226
|
+
)
|
|
227
|
+
targets.append([bpy.context.active_object])
|
|
228
|
+
else:
|
|
229
|
+
targets.append(_find_target_str(target, bpy.data.objects))
|
|
230
|
+
targets += [
|
|
231
|
+
_find_target_str(target, bpy.data.node_groups) for target in args.node_trees
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
return list(itertools.chain.from_iterable(targets))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
_transforms_map = {
|
|
238
|
+
"colors_to_hsv_definition": tr.map_graph_list(tr.colors_to_hsv_definition),
|
|
239
|
+
"infer_distribution_hypercube": (
|
|
240
|
+
lambda graphs: graphs + [tr.infer_distribution_hypercube(graphs)]
|
|
241
|
+
),
|
|
242
|
+
"infer_nodegroup_distributions": (
|
|
243
|
+
lambda graphs: tr.infer_nodegroup_distributions(graphs) + graphs
|
|
244
|
+
),
|
|
245
|
+
"extract_materials": tr.extract_materials_from_graphs,
|
|
246
|
+
# "infer_distribution_polytope": lambda graphs: graphs + [tr.infer_distribution_polytope(graphs)],
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def main():
|
|
251
|
+
parser = get_parser()
|
|
252
|
+
args = parser.parse_args()
|
|
253
|
+
|
|
254
|
+
for name in logging.root.manager.loggerDict:
|
|
255
|
+
logging.getLogger(name).setLevel(args.loglevel)
|
|
256
|
+
|
|
257
|
+
pf.ops.file.load_blend(args.input)
|
|
258
|
+
|
|
259
|
+
targets = _targets_from_args(args)
|
|
260
|
+
|
|
261
|
+
if len(targets) == 0:
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"No targets found for {args.input} {args.materials=} {args.objects=} {args.node_trees=}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
logger.info(f"Found targets {[x.name for x in targets]}")
|
|
267
|
+
|
|
268
|
+
transforms = [_transforms_map[x] for x in args.transforms]
|
|
269
|
+
python = transpile_targets(
|
|
270
|
+
targets,
|
|
271
|
+
transforms,
|
|
272
|
+
object_mode=args.object_mode,
|
|
273
|
+
add_version_comment=not args.no_version_comments,
|
|
274
|
+
add_line_comments=args.add_line_comments,
|
|
275
|
+
include_set_material=args.include_object_materials,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
match args.output:
|
|
279
|
+
case "print":
|
|
280
|
+
print(python)
|
|
281
|
+
case None:
|
|
282
|
+
pass
|
|
283
|
+
case x if Path(x).suffix == ".py":
|
|
284
|
+
x = Path(x)
|
|
285
|
+
x.parent.mkdir(parents=True, exist_ok=True)
|
|
286
|
+
print(f"Writing transpiled code to path {x}")
|
|
287
|
+
with x.open("w") as f:
|
|
288
|
+
f.write(python)
|
|
289
|
+
case x if ":" in x:
|
|
290
|
+
raise NotImplementedError("Not implemented")
|
|
291
|
+
module_path, target_name = x.split(":")
|
|
292
|
+
inject_into_python_file(python.splitlines(), Path(module_path), target_name)
|
|
293
|
+
case _:
|
|
294
|
+
raise ValueError(f"Invalid output type: {args.output}")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
with skip_teardown_on_exit():
|
|
299
|
+
main()
|
procfunc/types.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Callable, Generic, NamedTuple, TypeAlias, TypeVar
|
|
6
|
+
|
|
7
|
+
import bpy
|
|
8
|
+
import mathutils
|
|
9
|
+
|
|
10
|
+
from procfunc.util.pytree import register_pytree_container
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Asset",
|
|
18
|
+
"BlenderAsset",
|
|
19
|
+
"ObjectType",
|
|
20
|
+
"Object",
|
|
21
|
+
"CameraObject",
|
|
22
|
+
"MeshObject",
|
|
23
|
+
"CurveObject",
|
|
24
|
+
"EmptyObject",
|
|
25
|
+
"ArmatureObject",
|
|
26
|
+
"HairObject",
|
|
27
|
+
"LatticeObject",
|
|
28
|
+
"LightObject",
|
|
29
|
+
"LightProbeObject",
|
|
30
|
+
"MetaObject",
|
|
31
|
+
"Material",
|
|
32
|
+
"Texture",
|
|
33
|
+
"Image",
|
|
34
|
+
"Collection",
|
|
35
|
+
"VolumeObject",
|
|
36
|
+
"PointCloudObject",
|
|
37
|
+
"World",
|
|
38
|
+
"ValueRange",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
INVALIDATED_USAGE_ID = -100
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class AssetUsageTable:
|
|
46
|
+
counts: dict[int, int] = field(default_factory=lambda: defaultdict(int))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_global_usage_table = AssetUsageTable()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _bpy_data_col_for_asset(item: T) -> bpy.types.bpy_prop_collection:
|
|
53
|
+
match item.bl_rna.name:
|
|
54
|
+
case "Object":
|
|
55
|
+
return bpy.data.objects
|
|
56
|
+
case "Material":
|
|
57
|
+
return bpy.data.materials
|
|
58
|
+
case "Image Texture":
|
|
59
|
+
return bpy.data.textures
|
|
60
|
+
case _:
|
|
61
|
+
raise TypeError(
|
|
62
|
+
f"{BlenderAsset.__name__} doesnt yet support: {item} with type {item.bl_rna.name}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Asset(Generic[T]):
|
|
67
|
+
def item(self) -> T:
|
|
68
|
+
raise NotImplementedError("Subclasses of Asset must implement item()")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BlenderAsset(Asset, Generic[T]):
|
|
72
|
+
"""
|
|
73
|
+
A pythonic wrapper around a blender object,material,texture, or other bpy.data.stuffgoeshere member.
|
|
74
|
+
|
|
75
|
+
We reference count the underlying asset and delete it when no python references remain.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, item: T):
|
|
79
|
+
if isinstance(item, Asset):
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Attempted to wrap {item=} in a second {self.__class__.__name__} wrapper. This is almost certainly not intended"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self._item = item
|
|
85
|
+
_global_usage_table.counts[id(item)] += 1
|
|
86
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
87
|
+
logger.debug(
|
|
88
|
+
f"Asset ref {id(self)} created for {self._item.name}. remaining refs: {_global_usage_table.counts[id(self._item)]}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# self._dependencies: list[Asset] = []
|
|
92
|
+
|
|
93
|
+
def item(self) -> T:
|
|
94
|
+
if _global_usage_table.counts[id(self._item)] == INVALIDATED_USAGE_ID:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"Cant access .item() since asset has been explicitly invalidated by a previous operation"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return self._item
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
'''
|
|
103
|
+
def add_dependency(self, dependency: "Asset"):
|
|
104
|
+
self._dependencies.append(dependency)
|
|
105
|
+
|
|
106
|
+
def extend_dependencies(self, other: "Asset"):
|
|
107
|
+
self._dependencies.extend(other._dependencies)
|
|
108
|
+
|
|
109
|
+
def update(self, other: "Asset"):
|
|
110
|
+
"""
|
|
111
|
+
Updates this asset to point to the same underlying item as other.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
self._item = other._item
|
|
115
|
+
self._dependencies = other._dependencies
|
|
116
|
+
_global_usage_table.counts[id(self._item)] += 1
|
|
117
|
+
|
|
118
|
+
def invalidate(self):
|
|
119
|
+
self._item = None
|
|
120
|
+
self._dependencies = []
|
|
121
|
+
_global_usage_table.counts[id(self._item)] = INVALIDATED_USAGE_ID
|
|
122
|
+
|
|
123
|
+
def __del__(self):
|
|
124
|
+
# return
|
|
125
|
+
|
|
126
|
+
if self._item is None:
|
|
127
|
+
return
|
|
128
|
+
return # TODO
|
|
129
|
+
item_id = id(self._item)
|
|
130
|
+
count = _global_usage_table.counts.get(item_id, 0)
|
|
131
|
+
|
|
132
|
+
count -= 1
|
|
133
|
+
|
|
134
|
+
# new bad ref counter
|
|
135
|
+
if count < 0:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"Asset ref {id(self)} reached negative ref count for item {self._item.name}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
_global_usage_table.counts[item_id] = count
|
|
141
|
+
|
|
142
|
+
if count == 0:
|
|
143
|
+
try:
|
|
144
|
+
bpy_col = _bpy_data_col_for_asset(self._item)
|
|
145
|
+
if self._item.name in bpy_col:
|
|
146
|
+
bpy_col.remove(self._item, do_unlink=True)
|
|
147
|
+
except ReferenceError:
|
|
148
|
+
logger.warning(
|
|
149
|
+
f"{self.__class__.__name__} __del__ failed - item already deleted"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
del _global_usage_table.counts[item_id]
|
|
153
|
+
|
|
154
|
+
self._item = None
|
|
155
|
+
'''
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ObjectType(Enum):
|
|
159
|
+
ARMATURE = "ARMATURE"
|
|
160
|
+
CAMERA = "CAMERA"
|
|
161
|
+
CURVE = "CURVE"
|
|
162
|
+
EMPTY = "EMPTY"
|
|
163
|
+
FONT = "FONT"
|
|
164
|
+
HAIR = "HAIR"
|
|
165
|
+
LATTICE = "LATTICE"
|
|
166
|
+
LIGHT = "LIGHT"
|
|
167
|
+
LIGHT_PROBE = "LIGHT_PROBE"
|
|
168
|
+
MESH = "MESH"
|
|
169
|
+
META = "META"
|
|
170
|
+
POINTCLOUD = "POINTCLOUD"
|
|
171
|
+
SURFACE = "SURFACE"
|
|
172
|
+
VOLUME = "VOLUME"
|
|
173
|
+
# GPENCIL = "GPENCIL"
|
|
174
|
+
# GREASEPENCIL = "GREASEPENCIL"
|
|
175
|
+
# SPEAKER = "SPEAKER"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
Vector: TypeAlias = mathutils.Vector
|
|
179
|
+
Color: TypeAlias = mathutils.Color
|
|
180
|
+
Euler: TypeAlias = mathutils.Euler
|
|
181
|
+
Quaternion: TypeAlias = mathutils.Quaternion
|
|
182
|
+
Matrix: TypeAlias = mathutils.Matrix
|
|
183
|
+
BVHTree: TypeAlias = mathutils.bvhtree.BVHTree
|
|
184
|
+
|
|
185
|
+
NodeGroup: TypeAlias = bpy.types.NodeGroup
|
|
186
|
+
Scene: TypeAlias = bpy.types.Scene
|
|
187
|
+
ViewLayer: TypeAlias = bpy.types.ViewLayer
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class Object(BlenderAsset[bpy.types.Object]):
|
|
191
|
+
def __init__(self, obj: bpy.types.Object):
|
|
192
|
+
assert isinstance(obj, bpy.types.Object)
|
|
193
|
+
super().__init__(obj)
|
|
194
|
+
|
|
195
|
+
def __repr__(self):
|
|
196
|
+
return f"pf.{self.__class__.__name__}(bpy.data.objects[{self._item.name!r}])"
|
|
197
|
+
|
|
198
|
+
def clone(self):
|
|
199
|
+
new_obj = self._item.copy()
|
|
200
|
+
new_obj.data = self._item.data.copy()
|
|
201
|
+
bpy.context.collection.objects.link(new_obj)
|
|
202
|
+
return self.__class__(new_obj)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class CameraObject(Object):
|
|
206
|
+
def __init__(self, obj: bpy.types.Object):
|
|
207
|
+
assert obj.type == ObjectType.CAMERA.value
|
|
208
|
+
super().__init__(obj)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class MeshObject(Object):
|
|
212
|
+
def __init__(self, obj: bpy.types.Object):
|
|
213
|
+
assert obj.type == ObjectType.MESH.value
|
|
214
|
+
super().__init__(obj)
|
|
215
|
+
|
|
216
|
+
if len(obj.children) > 0:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"MeshObject {obj.name} had children {obj.children}, but this is not allowed for {self.__class__.__name__}"
|
|
219
|
+
)
|
|
220
|
+
if obj.parent is not None:
|
|
221
|
+
logger.warning(f"MeshObject {obj.name} had a parent {obj.parent}")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class CurveObject(Object):
|
|
225
|
+
def __init__(self, obj: bpy.types.Object):
|
|
226
|
+
assert obj.type == ObjectType.CURVE.value
|
|
227
|
+
super().__init__(obj)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class EmptyObject(Object):
|
|
231
|
+
def __init__(self, obj: bpy.types.Object):
|
|
232
|
+
assert obj.type == ObjectType.EMPTY.value
|
|
233
|
+
super().__init__(obj)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class ArmatureObject(Object):
|
|
237
|
+
def __init__(self, obj: bpy.types.Object):
|
|
238
|
+
assert obj.type == ObjectType.ARMATURE.value
|
|
239
|
+
super().__init__(obj)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class HairObject(Object):
|
|
243
|
+
def __init__(self, obj: bpy.types.Object):
|
|
244
|
+
assert obj.type == ObjectType.HAIR.value
|
|
245
|
+
super().__init__(obj)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class LatticeObject(Object):
|
|
249
|
+
def __init__(self, obj: bpy.types.Object):
|
|
250
|
+
assert obj.type == ObjectType.LATTICE.value
|
|
251
|
+
super().__init__(obj)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class LightObject(Object):
|
|
255
|
+
def __init__(self, obj: bpy.types.Object):
|
|
256
|
+
assert obj.type == ObjectType.LIGHT.value
|
|
257
|
+
super().__init__(obj)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class LightProbeObject(Object):
|
|
261
|
+
def __init__(self, obj: bpy.types.Object):
|
|
262
|
+
assert obj.type == ObjectType.LIGHT_PROBE.value
|
|
263
|
+
super().__init__(obj)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class MetaObject(Object):
|
|
267
|
+
def __init__(self, obj: bpy.types.Object):
|
|
268
|
+
assert obj.type == ObjectType.META.value
|
|
269
|
+
super().__init__(obj)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass
|
|
273
|
+
class Material:
|
|
274
|
+
surface: Any = None
|
|
275
|
+
displacement: Any = None
|
|
276
|
+
volume: Any = None
|
|
277
|
+
_bpy_material: Any = field(default=None, init=False, repr=False)
|
|
278
|
+
|
|
279
|
+
def item(self) -> bpy.types.Material:
|
|
280
|
+
if self._bpy_material is None:
|
|
281
|
+
from procfunc.nodes.execute.execute import _build_bpy_material
|
|
282
|
+
|
|
283
|
+
self._bpy_material = _build_bpy_material(
|
|
284
|
+
surface=self.surface,
|
|
285
|
+
displacement=self.displacement,
|
|
286
|
+
volume=self.volume,
|
|
287
|
+
)
|
|
288
|
+
return self._bpy_material
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class Texture(BlenderAsset[bpy.types.Texture]):
|
|
292
|
+
def __init__(self, tex: bpy.types.Texture):
|
|
293
|
+
assert isinstance(tex, bpy.types.Texture)
|
|
294
|
+
super().__init__(tex)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class Image(BlenderAsset[bpy.types.Image]):
|
|
298
|
+
def __init__(self, img: bpy.types.Image):
|
|
299
|
+
assert isinstance(img, bpy.types.Image)
|
|
300
|
+
super().__init__(img)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class Collection:
|
|
304
|
+
def __init__(
|
|
305
|
+
self, objects: "list[Object] | bpy.types.Collection", name: str = "collection"
|
|
306
|
+
):
|
|
307
|
+
if isinstance(objects, bpy.types.Collection):
|
|
308
|
+
self._collection = objects
|
|
309
|
+
self._objects = [Object(o) for o in objects.objects]
|
|
310
|
+
else:
|
|
311
|
+
self._objects = objects
|
|
312
|
+
self._collection = bpy.data.collections.new(name=name)
|
|
313
|
+
for obj in objects:
|
|
314
|
+
self._collection.objects.link(obj.item())
|
|
315
|
+
|
|
316
|
+
def __repr__(self):
|
|
317
|
+
return f"pf.Collection(bpy.data.collections[{self._collection.name!r}])"
|
|
318
|
+
|
|
319
|
+
def map(
|
|
320
|
+
self,
|
|
321
|
+
fn: Callable[[Object], Object],
|
|
322
|
+
skip_none: bool = True,
|
|
323
|
+
) -> "Collection":
|
|
324
|
+
objs = []
|
|
325
|
+
for obj in self._objects:
|
|
326
|
+
result = fn(obj)
|
|
327
|
+
if result is None and skip_none:
|
|
328
|
+
continue
|
|
329
|
+
objs.append(result)
|
|
330
|
+
return Collection(objs, name=self._collection.name + "_" + fn.__name__)
|
|
331
|
+
|
|
332
|
+
def __len__(self):
|
|
333
|
+
return len(self._objects)
|
|
334
|
+
|
|
335
|
+
def __iter__(self):
|
|
336
|
+
return iter(self._objects)
|
|
337
|
+
|
|
338
|
+
def item(self) -> bpy.types.Collection:
|
|
339
|
+
return self._collection
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class VolumeObject(BlenderAsset[bpy.types.Volume]):
|
|
343
|
+
def __init__(self, vol: bpy.types.Volume):
|
|
344
|
+
assert isinstance(vol, bpy.types.Volume)
|
|
345
|
+
super().__init__(vol)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class PointCloudObject(BlenderAsset[bpy.types.PointCloud]):
|
|
349
|
+
def __init__(self, vol: bpy.types.PointCloud):
|
|
350
|
+
assert isinstance(vol, bpy.types.PointCloud)
|
|
351
|
+
super().__init__(vol)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class World(BlenderAsset[bpy.types.World]):
|
|
355
|
+
def __init__(self, world: bpy.types.World):
|
|
356
|
+
assert isinstance(world, bpy.types.World)
|
|
357
|
+
super().__init__(world)
|
|
358
|
+
|
|
359
|
+
def __del__(self):
|
|
360
|
+
pass # no cleanup for these
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# TODO Font, PointCloud, Surface, Volume, Scene? World? ViewLayer?
|
|
364
|
+
|
|
365
|
+
TRangeType = TypeVar("TRangeType")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class ValueRange(NamedTuple, Generic[TRangeType]):
|
|
369
|
+
min: TRangeType | None
|
|
370
|
+
max: TRangeType | None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
register_pytree_container(
|
|
374
|
+
Material,
|
|
375
|
+
flatten_func=lambda m: ([m.surface, m.displacement, m.volume], None),
|
|
376
|
+
unflatten_func=lambda vals, _: Material(
|
|
377
|
+
surface=vals[0], displacement=vals[1], volume=vals[2]
|
|
378
|
+
),
|
|
379
|
+
names_func=lambda m: ["surface", "displacement", "volume"],
|
|
380
|
+
)
|
|
File without changes
|