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.
Files changed (85) hide show
  1. {nodebpy-520.19.0 → nodebpy-520.20.0}/PKG-INFO +1 -1
  2. {nodebpy-520.19.0 → nodebpy-520.20.0}/pyproject.toml +1 -1
  3. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/assets/_library.py +148 -86
  4. {nodebpy-520.19.0 → nodebpy-520.20.0}/README.md +0 -0
  5. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/__init__.py +0 -0
  6. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/assets/__init__.py +0 -0
  7. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/assets/__main__.py +0 -0
  8. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/assets/_codegen.py +0 -0
  9. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/__init__.py +0 -0
  10. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/_registry.py +0 -0
  11. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/_utils.py +0 -0
  12. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/accessor.py +0 -0
  13. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/asset.py +0 -0
  14. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/items.py +0 -0
  15. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/layout.py +0 -0
  16. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/mixins.py +0 -0
  17. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/node.py +0 -0
  18. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/socket.py +0 -0
  19. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/builder/tree.py +0 -0
  20. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/__init__.py +0 -0
  21. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/codegen.py +0 -0
  22. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/diagram.py +0 -0
  23. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/parity.py +0 -0
  24. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/plot.py +0 -0
  25. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/export/web_render.py +0 -0
  26. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/VENDORED.md +0 -0
  27. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  28. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  29. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  30. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  31. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  32. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  33. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  34. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  35. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  36. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  37. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  38. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  39. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  40. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/__init__.py +0 -0
  41. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/_mixins.py +0 -0
  42. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  43. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
  44. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  45. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  46. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  47. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  48. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  49. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  50. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  51. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
  52. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  53. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  54. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  55. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  56. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
  57. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  58. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  59. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  60. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
  61. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  62. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  63. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  64. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  65. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  66. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
  67. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  68. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  69. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  70. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  71. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
  72. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  73. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/assets.py +0 -0
  74. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/color.py +0 -0
  75. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  76. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  77. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/group.py +0 -0
  78. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/input.py +0 -0
  79. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  80. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/output.py +0 -0
  81. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/script.py +0 -0
  82. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  83. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  84. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  85. {nodebpy-520.19.0 → nodebpy-520.20.0}/src/nodebpy/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.19.0
3
+ Version: 520.20.0
4
4
  Summary: Build nodes trees in Blender more elegantly with code
5
5
  Author: Brady Johnston
6
6
  Author-email: Brady Johnston <brady.johnston@me.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "520.19.0"
3
+ version = "520.20.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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** referenced by the dumped trees (a Material socket or interface
30
- default) are code-generated too: each becomes a module under ``materials/``
31
- holding its shader tree as a class, a ``MATERIAL`` marker and a
32
- ``MATERIAL_PROPERTIES`` dict. Build recreates the material and runs the class
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 source files.
459
-
460
- Each asset becomes one ``.py`` module under ``<output_dir>/<tree>/``
461
- (``geometry``/``shader``/``compositor``) with an ``ASSET`` marker and
462
- metadata footers. Every group class is defined exactly once: helper groups
463
- used by a single asset are embedded in that asset's module, groups nested
464
- by several assets get their own module under ``<tree>/_shared/``, and an
465
- asset nested inside other assets keeps its class in its own module — all
466
- referenced via relative imports (``__init__.py`` package markers are
467
- written so the imports resolve). Materials referenced by the trees are
468
- code-generated into ``materials/`` modules, and other non-serialisable
469
- datablocks each module needs are recorded in its
470
- ``DATABLOCK_DEPENDENCIES`` footer. :func:`build_library` rebuilds the
471
- ``.blend`` from these files. A ``blender_assets.cats.txt`` next to the
472
- ``.blend`` is copied into ``output_dir`` so catalog assignments travel
473
- with the sources.
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; defaults to all.
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 written to.
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
- if names is not None and (missing := names - set(wanted)):
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 referenced materials) into modules
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 referenced anywhere in the dump are code-generated too: their
679
- # embedded shader tree joins the group universe as an extra root. Embedded
680
- # trees all ship named "Shader Nodetree" and the name is read-only, so
681
- # material trees are keyed synthetically ("material:<name>") and the class
682
- # name is overridden per emission instead.
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
- referenced = sorted(
686
- {name for deps in id_deps.values() for name in deps.get("materials", ())}
687
- )
688
- for mat_name in referenced:
689
- material = bpy.data.materials.get(mat_name)
690
- if material is None or material.node_tree is None: # pragma: no cover
691
- print(f" material {mat_name!r}: no shader tree, left as a dependency")
692
- continue
693
- if material.node_tree.name in trees:
694
- # A node group named like the embedded tree would be clobbered
695
- # by the per-emission class-name override.
696
- raise RuntimeError(
697
- f"A node group is named {material.node_tree.name!r}, which "
698
- f"collides with material {mat_name!r}'s embedded tree — "
699
- "rename that group to dump this library."
700
- )
701
- key = f"material:{mat_name}"
702
- trees[key] = material.node_tree
703
- id_deps[key] = _id_defaults(material.node_tree)
704
- material_trees[key] = mat_name
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
- all_written.add(write_module(key, modules[key], kind="material"))
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, builds each ``ASSET`` class via
990
- ``create_group()``, re-marks the tree as an asset, applies the dumped
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
- for field in _METADATA_FIELDS:
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 source files: "
1385
- "one .py per asset under <output>/geometry, <output>/shader and "
1386
- "<output>/compositor, holding the asset's class, an ASSET marker "
1387
- "and its metadata. Each group class is defined exactly once — "
1388
- "helpers used by a single asset are embedded in its module, "
1389
- "groups nested by several assets get their own module under "
1390
- "_shared/, and nested assets are imported from their own modules. "
1391
- "Check these files into version control as the source of truth "
1392
- "and rebuild the .blend from them with the 'build' subcommand. A "
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", nargs="+", help="Only dump these asset names (default: all)."
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, and exactly those node groups (plus dependencies) are "
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