nodebpy 520.19.0__tar.gz → 520.20.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.19.0 → nodebpy-520.20.0}/PKG-INFO +1 -1
- {nodebpy-520.19.0 → nodebpy-520.20.0}/pyproject.toml +1 -1
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/assets/_library.py +148 -86
- {nodebpy-520.19.0 → nodebpy-520.20.0}/README.md +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/assets/__main__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/assets/_codegen.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/_utils.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/layout.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/tree.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/codegen.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/parity.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/plot.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/web_render.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/VENDORED.md +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/_mixins.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/assets.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/manual.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/shader.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/vector.py +0 -0
- {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/types.py +0 -0
|
@@ -26,10 +26,13 @@ nesting can never produce an import cycle. At build time
|
|
|
26
26
|
:meth:`~nodebpy.builder.NodeGroupBuilder.create_group` reuses an existing tree
|
|
27
27
|
by name, so each group is still built exactly once.
|
|
28
28
|
|
|
29
|
-
**Materials**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
``
|
|
29
|
+
**Materials** are dump roots in their own right when marked as assets, and
|
|
30
|
+
are code-generated as dependencies when referenced by the dumped trees (a
|
|
31
|
+
Material socket or interface default): either way each becomes a module under
|
|
32
|
+
``materials/`` holding its shader tree as a class, a ``MATERIAL`` marker and
|
|
33
|
+
a ``MATERIAL_PROPERTIES`` dict — an asset-marked material additionally
|
|
34
|
+
carries its asset metadata as a ``MATERIAL_ASSET_METADATA`` dict, which makes
|
|
35
|
+
build re-mark it as an asset. Build recreates the material and runs the class
|
|
33
36
|
body into ``material.node_tree`` — a material is never a node group, so its
|
|
34
37
|
class is only a recipe, never ``create_group``-ed. A material's nested shader
|
|
35
38
|
groups take part in the normal shared/embedded classification.
|
|
@@ -392,6 +395,12 @@ def _render_group_module(
|
|
|
392
395
|
props = _material_properties(material)
|
|
393
396
|
if props:
|
|
394
397
|
footer += [""] + _dict_lines("MATERIAL_PROPERTIES", props)
|
|
398
|
+
if material.asset_data is not None:
|
|
399
|
+
# The footer's presence (even empty) is what makes build re-mark
|
|
400
|
+
# the material as an asset; dependency-only materials carry none.
|
|
401
|
+
footer += [""] + _dict_lines(
|
|
402
|
+
"MATERIAL_ASSET_METADATA", _asset_metadata(material)
|
|
403
|
+
)
|
|
395
404
|
if dependencies:
|
|
396
405
|
footer = footer or ["", ""]
|
|
397
406
|
footer += [""] + _dict_lines(
|
|
@@ -455,22 +464,26 @@ def dump_library(
|
|
|
455
464
|
format: bool = True,
|
|
456
465
|
typed_api: bool = False,
|
|
457
466
|
) -> dict[str, Path]:
|
|
458
|
-
"""Dump every node-group asset in ``blend_path`` to Python
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
asset nested
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
``
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
467
|
+
"""Dump every node-group and material asset in ``blend_path`` to Python
|
|
468
|
+
source files.
|
|
469
|
+
|
|
470
|
+
Each node-group asset becomes one ``.py`` module under
|
|
471
|
+
``<output_dir>/<tree>/`` (``geometry``/``shader``/``compositor``) with an
|
|
472
|
+
``ASSET`` marker and metadata footers. Every group class is defined
|
|
473
|
+
exactly once: helper groups used by a single asset are embedded in that
|
|
474
|
+
asset's module, groups nested by several assets get their own module
|
|
475
|
+
under ``<tree>/_shared/``, and an asset nested inside other assets keeps
|
|
476
|
+
its class in its own module — all referenced via relative imports
|
|
477
|
+
(``__init__.py`` package markers are written so the imports resolve).
|
|
478
|
+
Materials marked as assets, and materials referenced by the trees, are
|
|
479
|
+
code-generated into ``materials/`` modules — asset-marked ones carry
|
|
480
|
+
their asset metadata as a ``MATERIAL_ASSET_METADATA`` footer, so a
|
|
481
|
+
rebuild re-marks them (referenced-only materials build as unmarked
|
|
482
|
+
dependencies). Other non-serialisable datablocks each module needs are
|
|
483
|
+
recorded in its ``DATABLOCK_DEPENDENCIES`` footer. :func:`build_library`
|
|
484
|
+
rebuilds the ``.blend`` from these files. A ``blender_assets.cats.txt``
|
|
485
|
+
next to the ``.blend`` is copied into ``output_dir`` so catalog
|
|
486
|
+
assignments travel with the sources.
|
|
474
487
|
|
|
475
488
|
Each asset is appended into the current session for introspection and the
|
|
476
489
|
appended groups are removed again afterwards. Run this in a session that
|
|
@@ -486,8 +499,8 @@ def dump_library(
|
|
|
486
499
|
output_dir:
|
|
487
500
|
Directory to write the per-asset modules into (created if needed).
|
|
488
501
|
names:
|
|
489
|
-
Restrict the dump to these asset (node-group) names;
|
|
490
|
-
A full dump first clears the managed subdirectories
|
|
502
|
+
Restrict the dump to these asset (node-group or material) names;
|
|
503
|
+
defaults to all. A full dump first clears the managed subdirectories
|
|
491
504
|
(``geometry``/``shader``/``compositor``/``materials``) so files from
|
|
492
505
|
renamed or deleted assets don't linger; a filtered dump leaves the
|
|
493
506
|
other assets' files in place.
|
|
@@ -505,7 +518,9 @@ def dump_library(
|
|
|
505
518
|
Code-generate materials referenced by the dumped trees into
|
|
506
519
|
``materials/`` modules (the default). With ``False`` they are only
|
|
507
520
|
recorded as ``DATABLOCK_DEPENDENCIES``, to be resolved at build time
|
|
508
|
-
like any other non-serialisable datablock.
|
|
521
|
+
like any other non-serialisable datablock. Asset-marked materials
|
|
522
|
+
are dump roots in their own right and are always code-generated,
|
|
523
|
+
regardless of this flag.
|
|
509
524
|
format:
|
|
510
525
|
Run the generated sources through ``ruff format`` when available.
|
|
511
526
|
typed_api:
|
|
@@ -525,7 +540,8 @@ def dump_library(
|
|
|
525
540
|
Returns
|
|
526
541
|
-------
|
|
527
542
|
dict[str, Path]
|
|
528
|
-
Mapping of asset name to the file it was
|
|
543
|
+
Mapping of asset (node-group or material) name to the file it was
|
|
544
|
+
written to.
|
|
529
545
|
"""
|
|
530
546
|
blend_path = Path(blend_path)
|
|
531
547
|
output_dir = Path(output_dir)
|
|
@@ -546,8 +562,16 @@ def dump_library(
|
|
|
546
562
|
str(blend_path), link=False, assets_only=True
|
|
547
563
|
) as (src, dst):
|
|
548
564
|
available = list(src.node_groups)
|
|
565
|
+
# assets_only exposes exactly the asset-marked materials: they are
|
|
566
|
+
# dump roots alongside the node-group assets.
|
|
567
|
+
available_materials = list(src.materials)
|
|
549
568
|
wanted = [n for n in available if names is None or n in names]
|
|
550
|
-
|
|
569
|
+
wanted_materials = [
|
|
570
|
+
n for n in available_materials if names is None or n in names
|
|
571
|
+
]
|
|
572
|
+
if names is not None and (
|
|
573
|
+
missing := names - set(wanted) - set(wanted_materials)
|
|
574
|
+
):
|
|
551
575
|
raise KeyError(f"Assets not found in {blend_path}: {sorted(missing)}")
|
|
552
576
|
clashes = sorted(n for n in wanted if n in bpy.data.node_groups)
|
|
553
577
|
if clashes:
|
|
@@ -557,6 +581,9 @@ def dump_library(
|
|
|
557
581
|
"dump from a fresh session (e.g. python -m nodebpy.assets dump)."
|
|
558
582
|
)
|
|
559
583
|
dst.node_groups = list(wanted)
|
|
584
|
+
# A same-named material already in the session renames the appended
|
|
585
|
+
# one — caught by the post-append renamed-datablock guard below.
|
|
586
|
+
dst.materials = list(wanted_materials)
|
|
560
587
|
added = {
|
|
561
588
|
coll: [db for db in getattr(bpy.data, coll) if db.name not in before[coll]]
|
|
562
589
|
for coll in _CLEANUP_COLLECTIONS
|
|
@@ -582,6 +609,7 @@ def dump_library(
|
|
|
582
609
|
list(dst.node_groups),
|
|
583
610
|
added["node_groups"],
|
|
584
611
|
output_dir,
|
|
612
|
+
asset_materials=[m.name for m in dst.materials],
|
|
585
613
|
nodebpy_pkg=nodebpy_pkg,
|
|
586
614
|
snapshot_positions=snapshot_positions,
|
|
587
615
|
keep_reroutes=keep_reroutes,
|
|
@@ -645,6 +673,7 @@ def _dump_appended(
|
|
|
645
673
|
appended: list,
|
|
646
674
|
output_dir: Path,
|
|
647
675
|
*,
|
|
676
|
+
asset_materials: Iterable[str] = (),
|
|
648
677
|
nodebpy_pkg: str,
|
|
649
678
|
snapshot_positions: bool,
|
|
650
679
|
keep_reroutes: bool,
|
|
@@ -653,11 +682,14 @@ def _dump_appended(
|
|
|
653
682
|
library_blend: Path | None = None,
|
|
654
683
|
clean_stale: bool = False,
|
|
655
684
|
) -> dict[str, Path]:
|
|
656
|
-
"""Partition the appended groups (and
|
|
657
|
-
and write them.
|
|
685
|
+
"""Partition the appended groups (and material roots and referenced
|
|
686
|
+
materials) into modules and write them.
|
|
658
687
|
|
|
659
688
|
``asset_trees`` are the trees dumped as assets; ``appended`` is every
|
|
660
|
-
group the load brought in (the assets plus all their dependencies)
|
|
689
|
+
group the load brought in (the assets plus all their dependencies);
|
|
690
|
+
``asset_materials`` names the asset-marked materials appended as dump
|
|
691
|
+
roots in their own right — they are always code-generated, regardless of
|
|
692
|
+
the ``materials`` flag governing referenced ones.
|
|
661
693
|
``library_blend`` (the resolved ``.blend`` path) turns on the merged
|
|
662
694
|
typed API — see ``dump_library(typed_api=...)``. ``clean_stale`` removes
|
|
663
695
|
modules under the managed subdirectories that this dump did not write
|
|
@@ -675,33 +707,35 @@ def _dump_appended(
|
|
|
675
707
|
# Non-serialisable datablocks each group references, by tree name.
|
|
676
708
|
id_deps = {g.name: _id_defaults(g) for g in appended}
|
|
677
709
|
|
|
678
|
-
# Materials
|
|
679
|
-
#
|
|
680
|
-
#
|
|
681
|
-
#
|
|
682
|
-
# name
|
|
710
|
+
# Materials marked as assets (dump roots in their own right) and materials
|
|
711
|
+
# referenced anywhere in the dump are code-generated too: their embedded
|
|
712
|
+
# shader tree joins the group universe as an extra root. Embedded trees
|
|
713
|
+
# all ship named "Shader Nodetree" and the name is read-only, so material
|
|
714
|
+
# trees are keyed synthetically ("material:<name>") and the class name is
|
|
715
|
+
# overridden per emission instead.
|
|
683
716
|
material_trees: dict[str, str] = {} # tree key → material name
|
|
717
|
+
wanted_materials = set(asset_materials)
|
|
684
718
|
if materials:
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
719
|
+
wanted_materials |= {
|
|
720
|
+
name for deps in id_deps.values() for name in deps.get("materials", ())
|
|
721
|
+
}
|
|
722
|
+
for mat_name in sorted(wanted_materials):
|
|
723
|
+
material = bpy.data.materials.get(mat_name)
|
|
724
|
+
if material is None or material.node_tree is None: # pragma: no cover
|
|
725
|
+
print(f" material {mat_name!r}: no shader tree, left as a dependency")
|
|
726
|
+
continue
|
|
727
|
+
if material.node_tree.name in trees:
|
|
728
|
+
# A node group named like the embedded tree would be clobbered
|
|
729
|
+
# by the per-emission class-name override.
|
|
730
|
+
raise RuntimeError(
|
|
731
|
+
f"A node group is named {material.node_tree.name!r}, which "
|
|
732
|
+
f"collides with material {mat_name!r}'s embedded tree — "
|
|
733
|
+
"rename that group to dump this library."
|
|
734
|
+
)
|
|
735
|
+
key = f"material:{mat_name}"
|
|
736
|
+
trees[key] = material.node_tree
|
|
737
|
+
id_deps[key] = _id_defaults(material.node_tree)
|
|
738
|
+
material_trees[key] = mat_name
|
|
705
739
|
|
|
706
740
|
# Sharing structure: which groups each root (asset or material tree)
|
|
707
741
|
# transitively reaches.
|
|
@@ -825,8 +859,14 @@ def _dump_appended(
|
|
|
825
859
|
all_written = set(written.values())
|
|
826
860
|
for name in sorted(shared):
|
|
827
861
|
all_written.add(write_module(name, modules[name], kind="shared"))
|
|
862
|
+
asset_material_names = set(asset_materials)
|
|
828
863
|
for key in sorted(material_trees):
|
|
829
|
-
|
|
864
|
+
path = write_module(key, modules[key], kind="material")
|
|
865
|
+
all_written.add(path)
|
|
866
|
+
# Material roots are assets the caller asked for, so they belong in
|
|
867
|
+
# the returned mapping; referenced-only materials are dependencies.
|
|
868
|
+
if (mat_name := material_trees[key]) in asset_material_names:
|
|
869
|
+
written[mat_name] = path
|
|
830
870
|
if clean_stale:
|
|
831
871
|
_remove_stale_modules(output_dir, all_written)
|
|
832
872
|
|
|
@@ -950,6 +990,27 @@ def _resolve_dependencies(
|
|
|
950
990
|
return placeholders
|
|
951
991
|
|
|
952
992
|
|
|
993
|
+
def _apply_asset_metadata(datablock, metadata: dict) -> None:
|
|
994
|
+
"""Mark ``datablock`` (a built tree or material) as an asset and apply the
|
|
995
|
+
dumped metadata fields and tags onto it. A field read-only in this Blender
|
|
996
|
+
version (as ``catalog_simple_name`` was) is derived, not lost — skipped
|
|
997
|
+
with a note rather than aborting the build."""
|
|
998
|
+
if datablock.asset_data is None:
|
|
999
|
+
datablock.asset_mark()
|
|
1000
|
+
asset_data = datablock.asset_data
|
|
1001
|
+
assert asset_data is not None
|
|
1002
|
+
for field in _METADATA_FIELDS:
|
|
1003
|
+
if field in metadata:
|
|
1004
|
+
try:
|
|
1005
|
+
setattr(asset_data, field, metadata[field])
|
|
1006
|
+
except AttributeError: # pragma: no cover - other Blender versions
|
|
1007
|
+
print(f" {datablock.name}: skipping read-only asset field {field!r}")
|
|
1008
|
+
existing_tags = {tag.name for tag in asset_data.tags}
|
|
1009
|
+
for tag in metadata.get("tags", ()):
|
|
1010
|
+
if tag not in existing_tags:
|
|
1011
|
+
asset_data.tags.new(tag)
|
|
1012
|
+
|
|
1013
|
+
|
|
953
1014
|
def _build_material(material_cls, name: str):
|
|
954
1015
|
"""Recreate the material ``name`` by running ``material_cls``'s
|
|
955
1016
|
``_build_group`` recipe into its embedded shader tree; an existing
|
|
@@ -986,8 +1047,10 @@ def build_library(
|
|
|
986
1047
|
|
|
987
1048
|
Imports every root module under ``source_dir`` (recursively; ``_shared``
|
|
988
1049
|
group modules are pulled in by the root modules' own imports), rebuilds
|
|
989
|
-
each ``MATERIAL`` module's material
|
|
990
|
-
|
|
1050
|
+
each ``MATERIAL`` module's material (re-marking it as an asset when the
|
|
1051
|
+
module carries a ``MATERIAL_ASSET_METADATA`` footer), builds each
|
|
1052
|
+
``ASSET`` class via ``create_group()``, re-marks the tree as an asset,
|
|
1053
|
+
applies the dumped
|
|
991
1054
|
``ASSET_METADATA``/``TREE_PROPERTIES``/``MATERIAL_PROPERTIES``, and
|
|
992
1055
|
writes those trees and materials (plus their dependencies) to
|
|
993
1056
|
``blend_path`` with ``bpy.data.libraries.write``. A
|
|
@@ -1129,6 +1192,13 @@ def build_library(
|
|
|
1129
1192
|
setattr(material, key, value)
|
|
1130
1193
|
except AttributeError:
|
|
1131
1194
|
print(f" {name}: skipping read-only material property {key!r}")
|
|
1195
|
+
# The footer's presence (even empty) marks the material as an
|
|
1196
|
+
# asset; dependency-only material modules carry none and the
|
|
1197
|
+
# material stays unmarked.
|
|
1198
|
+
asset_metadata = getattr(module, "MATERIAL_ASSET_METADATA", None)
|
|
1199
|
+
if asset_metadata is not None:
|
|
1200
|
+
assert isinstance(asset_metadata, dict)
|
|
1201
|
+
_apply_asset_metadata(material, asset_metadata)
|
|
1132
1202
|
built_materials.append(material)
|
|
1133
1203
|
|
|
1134
1204
|
trees = []
|
|
@@ -1144,26 +1214,9 @@ def build_library(
|
|
|
1144
1214
|
# own .blend; building from source is the whole point here.
|
|
1145
1215
|
with build_from_source():
|
|
1146
1216
|
tree = asset_cls.create_group()
|
|
1147
|
-
if tree.asset_data is None:
|
|
1148
|
-
tree.asset_mark()
|
|
1149
|
-
asset_data = tree.asset_data
|
|
1150
|
-
assert asset_data is not None
|
|
1151
1217
|
metadata = getattr(module, "ASSET_METADATA", {})
|
|
1152
1218
|
assert isinstance(metadata, dict)
|
|
1153
|
-
|
|
1154
|
-
if field in metadata:
|
|
1155
|
-
try:
|
|
1156
|
-
setattr(asset_data, field, metadata[field])
|
|
1157
|
-
except AttributeError: # pragma: no cover - other Blender versions
|
|
1158
|
-
# Read-only in this Blender version (as catalog_simple_name
|
|
1159
|
-
# was) — the value is derived, not lost; keep building.
|
|
1160
|
-
print(
|
|
1161
|
-
f" {tree.name}: skipping read-only asset field {field!r}"
|
|
1162
|
-
)
|
|
1163
|
-
existing_tags = {tag.name for tag in asset_data.tags}
|
|
1164
|
-
for tag in metadata.get("tags", ()):
|
|
1165
|
-
if tag not in existing_tags:
|
|
1166
|
-
asset_data.tags.new(tag)
|
|
1219
|
+
_apply_asset_metadata(tree, metadata)
|
|
1167
1220
|
properties = getattr(module, "TREE_PROPERTIES", {})
|
|
1168
1221
|
assert isinstance(properties, dict)
|
|
1169
1222
|
for key, value in properties.items():
|
|
@@ -1381,15 +1434,19 @@ def main(argv: list[str] | None = None) -> None: # pragma: no cover - CLI wrapp
|
|
|
1381
1434
|
"dump",
|
|
1382
1435
|
help="Dump every asset in a .blend to per-asset .py files.",
|
|
1383
1436
|
description=(
|
|
1384
|
-
"Dump every node-group asset in a .blend to Python
|
|
1385
|
-
"one .py per asset under
|
|
1386
|
-
"<output>/
|
|
1387
|
-
"
|
|
1388
|
-
"
|
|
1389
|
-
"
|
|
1390
|
-
"
|
|
1391
|
-
"
|
|
1392
|
-
"
|
|
1437
|
+
"Dump every node-group and material asset in a .blend to Python "
|
|
1438
|
+
"source files: one .py per node-group asset under "
|
|
1439
|
+
"<output>/geometry, <output>/shader and <output>/compositor, "
|
|
1440
|
+
"holding the asset's class, an ASSET marker and its metadata. "
|
|
1441
|
+
"Each group class is defined exactly once — helpers used by a "
|
|
1442
|
+
"single asset are embedded in its module, groups nested by "
|
|
1443
|
+
"several assets get their own module under _shared/, and nested "
|
|
1444
|
+
"assets are imported from their own modules. Asset-marked and "
|
|
1445
|
+
"referenced materials get modules under <output>/materials; "
|
|
1446
|
+
"asset-marked ones carry their metadata as "
|
|
1447
|
+
"MATERIAL_ASSET_METADATA, so a rebuild re-marks them. Check "
|
|
1448
|
+
"these files into version control as the source of truth and "
|
|
1449
|
+
"rebuild the .blend from them with the 'build' subcommand. A "
|
|
1393
1450
|
"blender_assets.cats.txt next to the .blend is copied along, so "
|
|
1394
1451
|
"catalog assignments survive the round trip."
|
|
1395
1452
|
),
|
|
@@ -1397,7 +1454,9 @@ def main(argv: list[str] | None = None) -> None: # pragma: no cover - CLI wrapp
|
|
|
1397
1454
|
dump.add_argument("blend", type=Path, help="The .blend asset library to dump.")
|
|
1398
1455
|
dump.add_argument("output", type=Path, help="Directory to write the .py files to.")
|
|
1399
1456
|
dump.add_argument(
|
|
1400
|
-
"--names",
|
|
1457
|
+
"--names",
|
|
1458
|
+
nargs="+",
|
|
1459
|
+
help="Only dump these asset (node-group or material) names (default: all).",
|
|
1401
1460
|
)
|
|
1402
1461
|
dump.add_argument(
|
|
1403
1462
|
"--nodebpy-pkg",
|
|
@@ -1438,7 +1497,8 @@ def main(argv: list[str] | None = None) -> None: # pragma: no cover - CLI wrapp
|
|
|
1438
1497
|
help=(
|
|
1439
1498
|
"Skip code-generating referenced materials into materials/ "
|
|
1440
1499
|
"modules; record them only as DATABLOCK_DEPENDENCIES, to be "
|
|
1441
|
-
"resolved at build time (session / --resources / --drop-missing)."
|
|
1500
|
+
"resolved at build time (session / --resources / --drop-missing). "
|
|
1501
|
+
"Asset-marked materials are dump roots and are always generated."
|
|
1442
1502
|
),
|
|
1443
1503
|
)
|
|
1444
1504
|
|
|
@@ -1449,7 +1509,9 @@ def main(argv: list[str] | None = None) -> None: # pragma: no cover - CLI wrapp
|
|
|
1449
1509
|
"Rebuild a .blend asset library from sources written by the "
|
|
1450
1510
|
"'dump' subcommand: every .py under <source> is executed, its "
|
|
1451
1511
|
"ASSET class is built, re-marked as an asset with its dumped "
|
|
1452
|
-
"metadata
|
|
1512
|
+
"metadata (MATERIAL modules rebuild their material, re-marked "
|
|
1513
|
+
"when they carry MATERIAL_ASSET_METADATA), and exactly those "
|
|
1514
|
+
"node groups and materials (plus dependencies) are "
|
|
1453
1515
|
"written to the .blend. Asset previews are not round-tripped; "
|
|
1454
1516
|
"regenerate them in Blender if needed. A blender_assets.cats.txt "
|
|
1455
1517
|
"in <source> is copied next to the built .blend."
|
|
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
|
|
File without changes
|
|
File without changes
|