Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions packages/tree_clipper/src/tree_clipper/import_to_asset_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import uuid
from pathlib import Path

import bpy

from .import_nodes import ImportIntermediate, ImportParameters, ImportReport


def _collect_created_datablocks(report: ImportReport) -> set[bpy.types.ID]:
datablocks: set[bpy.types.ID] = {
bpy.data.node_groups[name] for name in report.renames_node_group.values()
}
if report.rename_material is not None:
_original_name, name = report.rename_material
datablocks.add(bpy.data.materials[name])
return datablocks


def _remove_created_datablocks(datablocks: set[bpy.types.ID]) -> None:
for datablock in datablocks:
if isinstance(datablock, bpy.types.NodeTree):
bpy.data.node_groups.remove(datablock)
else:
assert isinstance(datablock, bpy.types.Material)
bpy.data.materials.remove(datablock)


def move_import_to_asset_file(
*,
report: ImportReport,
asset_directory: Path | None = None,
asset_file_path: Path | None = None,
path_remap: str = "NONE",
mark_as_asset: bool = True,
fake_user: bool = False,
compress: bool = False,
) -> Path:
datablocks = _collect_created_datablocks(report)

if report.rename_material is not None:
_original_name, root_name = report.rename_material
bpy.data.materials[root_name].asset_mark()
else:
root_name = report.last_getter().name # ty:ignore[unresolved-attribute, call-non-callable]
bpy.data.node_groups[root_name].asset_mark()

if asset_file_path is None:
assert asset_directory is not None
asset_file_path = (
asset_directory
/ f"{bpy.path.clean_name(root_name)}-{uuid.uuid4().hex[:8]}.blend"
)

bpy.data.libraries.write(
str(asset_file_path),
datablocks,
path_remap=path_remap,
fake_user=fake_user,
compress=compress,
)

# bpy.data.libraries.write() expands indirectly referenced datablocks (e.g. externals)
# into the asset file on its own; only the ones tree_clipper created here get moved out.
_remove_created_datablocks(datablocks)

return asset_file_path


def import_to_asset_file(
*,
import_intermediate: ImportIntermediate,
parameters: ImportParameters,
asset_directory: Path | None = None,
asset_file_path: Path | None = None,
path_remap: str = "NONE",
mark_as_asset: bool = True,
fake_user: bool = False,
compress: bool = False,
) -> tuple[ImportReport, Path]:
report = import_intermediate.import_all(parameters)

asset_file_path = move_import_to_asset_file(
report=report,
asset_file_path=asset_file_path,
path_remap=path_remap,
fake_user=fake_user,
compress=compress,
)

return report, asset_file_path
82 changes: 82 additions & 0 deletions packages/tree_clipper/tests/test_import_to_asset_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from pathlib import Path

import bpy
from tree_clipper.export_nodes import ExportIntermediate, ExportParameters
from tree_clipper.import_nodes import ImportIntermediate, ImportParameters
from tree_clipper.import_to_asset_file import import_to_asset_file
from tree_clipper.specific_handlers import BUILT_IN_EXPORTER, BUILT_IN_IMPORTER

from tests.util import export_to_string, make_test_node_tree


def test_import_to_asset_file(tmp_path: Path):
tree = make_test_node_tree()
original_name = tree.name

before = export_to_string(original_name)

# start fresh, as if importing into a different file
bpy.data.node_groups.remove(tree)

asset_file_path = tmp_path / "asset.blend"

import_intermediate = ImportIntermediate(string=before)
report, _file_path = import_to_asset_file(
import_intermediate=import_intermediate,
parameters=ImportParameters(
specific_handlers=BUILT_IN_IMPORTER,
debug_prints=False,
),
asset_file_path=asset_file_path,
)

imported_name = report.renames_node_group[original_name]

assert imported_name not in bpy.data.node_groups
assert asset_file_path.exists()

with bpy.data.libraries.load(str(asset_file_path)) as (data_from, _data_to):
assert imported_name in data_from.node_groups


def test_import_to_asset_file_material(tmp_path: Path):
mat = bpy.data.materials.new("test_material")
mat.use_nodes = True
original_name = mat.name

export_intermediate = ExportIntermediate(
parameters=ExportParameters(
is_material=True,
name=original_name,
specific_handlers=BUILT_IN_EXPORTER,
export_sub_trees=True,
debug_prints=False,
write_from_roots=False,
)
)
while export_intermediate.step():
pass
before = export_intermediate.export_to_str(compress=False, json_indent=4)

bpy.data.materials.remove(mat)

asset_file_path = tmp_path / "asset.blend"

import_intermediate = ImportIntermediate(string=before)
report, _file_path = import_to_asset_file(
import_intermediate=import_intermediate,
parameters=ImportParameters(
specific_handlers=BUILT_IN_IMPORTER,
debug_prints=False,
),
asset_file_path=asset_file_path,
)

assert report.rename_material is not None
_original_name, imported_name = report.rename_material

assert imported_name not in bpy.data.materials
assert asset_file_path.exists()

with bpy.data.libraries.load(str(asset_file_path)) as (data_from, _data_to):
assert imported_name in data_from.materials
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
ImportParameters,
preliminary_check,
)
from ._vendor.tree_clipper.import_to_asset_file import move_import_to_asset_file
from ._vendor.tree_clipper.specific_handlers import (
BUILT_IN_IMPORTER,
)
from .post_import import post_import
from .preferences import get_show_advanced_options
from .preferences import (
get_asset_directory,
get_import_as_asset,
get_show_advanced_options,
)

_INTERMEDIATE_IMPORT_CACHE = None
TIMER = None
Expand Down Expand Up @@ -255,8 +260,39 @@ def modal(self, context: bpy.types.Context, event: bpy.types.Event):
for warning in report.warnings:
self.report({"WARNING"}, warning)

if not get_import_as_asset():
post_import(
context=context, event=event, imported_root=report.last_getter()
)
return {"FINISHED"}

root_is_material = report.rename_material is not None
if root_is_material:
_original_name, root_name = report.rename_material
else:
root_name = report.last_getter().name

asset_file_path = move_import_to_asset_file(
report=report, asset_directory=Path(get_asset_directory())
)

with bpy.data.libraries.load(str(asset_file_path), link=True, pack=True) as (
_data_from,
data_to,
):
if root_is_material:
data_to.materials = [root_name]
else:
data_to.node_groups = [root_name]

imported_root = (
data_to.materials[0].node_tree
if root_is_material
else data_to.node_groups[0]
)

_INTERMEDIATE_IMPORT_CACHE = None

post_import(context=context, event=event, report=report)
post_import(context=context, event=event, imported_root=imported_root)

return {"FINISHED"}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import bpy

from ._vendor.tree_clipper.import_nodes import ImportReport

TREE_TYPE_TO_GROUP_TYPE = {
bpy.types.CompositorNodeTree: bpy.types.CompositorNodeGroup,
bpy.types.GeometryNodeTree: bpy.types.GeometryNodeGroup,
Expand All @@ -14,7 +12,7 @@ def post_import(
*,
context: bpy.types.Context,
event: bpy.types.Event,
report: ImportReport,
imported_root: bpy.types.NodeTree,
) -> None:
def add_as_group() -> str | None:
if not isinstance(context.space_data, bpy.types.SpaceNodeEditor):
Expand All @@ -24,9 +22,6 @@ def add_as_group() -> str | None:
if node_tree is None:
return "No active tree to attach to."

assert report.last_getter is not None
imported_root = report.last_getter()

if node_tree.bl_rna.identifier != imported_root.bl_rna.identifier: # ty:ignore[unresolved-attribute]
return f"Editor type is {node_tree.bl_rna.identifier}, but imported {imported_root.bl_rna.identifier}." # ty:ignore[unresolved-attribute]

Expand Down
43 changes: 34 additions & 9 deletions packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,46 @@ class TreeClipperPreferences(bpy.types.AddonPreferences):
default=False,
) # type: ignore

import_as_asset: bpy.props.BoolProperty(
name="Import as Asset",
description="""Instead of having the imported trees in the current .blend file,
store the imports in a local asset library.") # type: ignore""",
default=True,
) # type :ignore

asset_directory: bpy.props.StringProperty(
name="Asset Directory",
description="When importing as asset, store the asset files in this directory.",
default=bpy.utils.extension_path_user(
__package__, # ty:ignore[invalid-argument-type]
path="assets",
create=True,
),
subtype="DIR_PATH",
) # ty:ignore[invalid-type-form]

def draw(self, context: bpy.types.Context) -> None:
self.layout.prop(self, "max_clipboard_megabyte")
self.layout.prop(self, "show_advanced_options")
self.layout.prop(self, "import_as_asset")
self.layout.prop(self, "asset_directory")


def _get_preferences():
return bpy.context.preferences.addons.get(__package__).preferences # ty:ignore[unresolved-attribute]


def get_max_clipboard_bytes():
return (
1_000_000
* bpy.context.preferences.addons.get( # ty:ignore[possibly-missing-attribute]
__package__ # ty:ignore[invalid-argument-type]
).preferences.max_clipboard_megabyte
) # ty:ignore[possibly-missing-attribute]
return 1_000_000 * _get_preferences().max_clipboard_megabyte


def get_show_advanced_options():
return bpy.context.preferences.addons.get( # ty:ignore[possibly-missing-attribute]
__package__ # ty:ignore[invalid-argument-type]
).preferences.show_advanced_options # ty:ignore[possibly-missing-attribute]
return _get_preferences().show_advanced_options


def get_import_as_asset():
return _get_preferences().import_as_asset


def get_asset_directory():
return _get_preferences().asset_directory
Loading