From 5a0cfe1fe84ba56250a5cb3e07ede577a410fcaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:06:32 +0200 Subject: [PATCH 1/7] rename to extended --- README.md | 4 +- .../src/tree_clipper_addon/__init__.py | 64 ++++++++--------- .../tree_clipper_addon/blender_manifest.toml | 4 +- .../tree_clipper_addon/operators_export.py | 26 +++---- .../tree_clipper_addon/operators_import.py | 68 +++++++++---------- .../src/tree_clipper_addon/panel.py | 18 ++--- .../src/tree_clipper_addon/preferences.py | 2 +- 7 files changed, 94 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 6b77335..9eaabf9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Featured Image -# Tree Clipper +# Tree Clipper Extended + +> A fork of [Tree Clipper](https://github.com/Algebraic-UG/tree_clipper) renamed so it can be installed alongside the original without colliding (distinct extension id, operators, panel and properties). Easier version control and sharing of node trees via `.json` or copy-pasteable strings. diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py b/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py index bfb20f9..cac0a98 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py @@ -14,56 +14,56 @@ import bpy from .operators_export import ( - Tree_Clipper_External_Export_Item, - SCENE_UL_Tree_Clipper_External_Export_List, - SCENE_OT_Tree_Clipper_Export_Cache, - SCENE_OT_Tree_Clipper_Export_Modal, - SCENE_OT_Tree_Clipper_Export_Prepare, + Tree_Clipper_Extended_External_Export_Item, + SCENE_UL_Tree_Clipper_Extended_External_Export_List, + SCENE_OT_Tree_Clipper_Extended_Export_Cache, + SCENE_OT_Tree_Clipper_Extended_Export_Modal, + SCENE_OT_Tree_Clipper_Extended_Export_Prepare, ) from .operators_import import ( - Tree_Clipper_External_Import_Item, - Tree_Clipper_External_Import_Items, - SCENE_UL_Tree_Clipper_External_Import_List, - SCENE_OT_Tree_Clipper_Import_Cache, - SCENE_OT_Tree_Clipper_Import_File_Prepare, - SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare, - SCENE_OT_Tree_Clipper_Import_Modal, + Tree_Clipper_Extended_External_Import_Item, + Tree_Clipper_Extended_External_Import_Items, + SCENE_UL_Tree_Clipper_Extended_External_Import_List, + SCENE_OT_Tree_Clipper_Extended_Import_Cache, + SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, + SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, + SCENE_OT_Tree_Clipper_Extended_Import_Modal, ) -from .panel import SCENE_PT_Tree_Clipper_Panel +from .panel import SCENE_PT_Tree_Clipper_Extended_Panel -from .preferences import TreeClipperPreferences +from .preferences import TreeClipperExtendedPreferences classes = [ - Tree_Clipper_External_Export_Item, - SCENE_UL_Tree_Clipper_External_Export_List, - SCENE_OT_Tree_Clipper_Export_Cache, - SCENE_OT_Tree_Clipper_Export_Modal, - SCENE_OT_Tree_Clipper_Export_Prepare, - Tree_Clipper_External_Import_Item, - Tree_Clipper_External_Import_Items, - SCENE_UL_Tree_Clipper_External_Import_List, - SCENE_OT_Tree_Clipper_Import_Modal, - SCENE_OT_Tree_Clipper_Import_Cache, - SCENE_OT_Tree_Clipper_Import_File_Prepare, - SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare, - SCENE_PT_Tree_Clipper_Panel, - TreeClipperPreferences, + Tree_Clipper_Extended_External_Export_Item, + SCENE_UL_Tree_Clipper_Extended_External_Export_List, + SCENE_OT_Tree_Clipper_Extended_Export_Cache, + SCENE_OT_Tree_Clipper_Extended_Export_Modal, + SCENE_OT_Tree_Clipper_Extended_Export_Prepare, + Tree_Clipper_Extended_External_Import_Item, + Tree_Clipper_Extended_External_Import_Items, + SCENE_UL_Tree_Clipper_Extended_External_Import_List, + SCENE_OT_Tree_Clipper_Extended_Import_Modal, + SCENE_OT_Tree_Clipper_Extended_Import_Cache, + SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, + SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, + SCENE_PT_Tree_Clipper_Extended_Panel, + TreeClipperExtendedPreferences, ] def register() -> None: - print("Registering Tree Clipper") + print("Registering Tree Clipper Extended") for cls in classes: bpy.utils.register_class(cls) # the pointer properties in the items make it impossible to store on the operator - bpy.types.Scene.tree_clipper_external_import_items = bpy.props.PointerProperty( # ty: ignore[unresolved-attribute] - type=Tree_Clipper_External_Import_Items + bpy.types.Scene.tree_clipper_extended_external_import_items = bpy.props.PointerProperty( # ty: ignore[unresolved-attribute] + type=Tree_Clipper_Extended_External_Import_Items ) def unregister() -> None: - del bpy.types.Scene.tree_clipper_external_import_items # ty: ignore[unresolved-attribute] + del bpy.types.Scene.tree_clipper_extended_external_import_items # ty: ignore[unresolved-attribute] for cls in reversed(classes): bpy.utils.unregister_class(cls) diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml b/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml index 60aeac5..4bdf803 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml @@ -2,9 +2,9 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension -id = "tree_clipper" +id = "tree_clipper_extended" version = "0.1.6" # should match tree_clipper (core logic) -name = "Tree Clipper" +name = "Tree Clipper Extended" tagline = "Export and import Blender node trees as JSON" maintainer = "Algebraic " # Supported types: "add-on", "theme" diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_export.py b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_export.py index cf97589..671c79a 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_export.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_export.py @@ -20,8 +20,8 @@ _INTERMEDIATE_EXPORT_CACHE = None -class SCENE_OT_Tree_Clipper_Export_Prepare(bpy.types.Operator): - bl_idname = "scene.tree_clipper_export_prepare" +class SCENE_OT_Tree_Clipper_Extended_Export_Prepare(bpy.types.Operator): + bl_idname = "scene.tree_clipper_extended_export_prepare" bl_label = "Export" bl_options = {"REGISTER"} @@ -56,7 +56,7 @@ def execute( ) # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_export_modal("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_extended_export_modal("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} def draw(self, context: bpy.types.Context) -> None: @@ -69,8 +69,8 @@ def draw(self, context: bpy.types.Context) -> None: self.layout.prop(self, "write_from_roots") # ty:ignore[possibly-missing-attribute] -class SCENE_OT_Tree_Clipper_Export_Modal(bpy.types.Operator): - bl_idname = "scene.tree_clipper_export_modal" +class SCENE_OT_Tree_Clipper_Extended_Export_Modal(bpy.types.Operator): + bl_idname = "scene.tree_clipper_extended_export_modal" bl_label = "Export Modal" bl_options = set() @@ -111,11 +111,11 @@ def modal(self, context, event): self.report({"WARNING"}, warning) # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_export_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_extended_export_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} -class SCENE_UL_Tree_Clipper_External_Export_List(bpy.types.UIList): +class SCENE_UL_Tree_Clipper_Extended_External_Export_List(bpy.types.UIList): def draw_item( self, context: bpy.types.Context, @@ -129,7 +129,7 @@ def draw_item( flt_flag: int | None, ) -> None: assert isinstance(_INTERMEDIATE_EXPORT_CACHE, ExportIntermediate) - assert isinstance(item, Tree_Clipper_External_Export_Item) + assert isinstance(item, Tree_Clipper_Extended_External_Export_Item) external = _INTERMEDIATE_EXPORT_CACHE.get_external()[item.external_id] pointer = external.pointed_to_by row = layout.row() @@ -138,7 +138,7 @@ def draw_item( row.prop(item, "skip") -class Tree_Clipper_External_Export_Item(bpy.types.PropertyGroup): +class Tree_Clipper_Extended_External_Export_Item(bpy.types.PropertyGroup): external_id: bpy.props.IntProperty() # type: ignore description: bpy.props.StringProperty(name="", default=DEFAULT_HINT) # type: ignore skip: bpy.props.BoolProperty(name="Hide in Import", default=False) # type: ignore @@ -150,8 +150,8 @@ class Tree_Clipper_External_Export_Item(bpy.types.PropertyGroup): _FILE = "File" -class SCENE_OT_Tree_Clipper_Export_Cache(bpy.types.Operator): - bl_idname = "scene.tree_clipper_export_cache" +class SCENE_OT_Tree_Clipper_Extended_Export_Cache(bpy.types.Operator): + bl_idname = "scene.tree_clipper_extended_export_cache" bl_label = "Export Cache" bl_options = set() @@ -165,7 +165,7 @@ class SCENE_OT_Tree_Clipper_Export_Cache(bpy.types.Operator): compress_or_json: bpy.props.EnumProperty(items=[(_COMPRESS,) * 3, (_JSON,) * 3]) # type: ignore json_indent: bpy.props.IntProperty(name="JSON Indent", default=4, min=0) # type: ignore - external_items: bpy.props.CollectionProperty(type=Tree_Clipper_External_Export_Item) # type: ignore + external_items: bpy.props.CollectionProperty(type=Tree_Clipper_Extended_External_Export_Item) # type: ignore selected_external_item: bpy.props.IntProperty() # type: ignore def invoke( @@ -235,7 +235,7 @@ def draw(self, context: bpy.types.Context) -> None: self.layout.label(text="References to External:") # ty:ignore[possibly-missing-attribute] self.layout.template_list( # ty:ignore[possibly-missing-attribute] - listtype_name="SCENE_UL_Tree_Clipper_External_Export_List", + listtype_name="SCENE_UL_Tree_Clipper_Extended_External_Export_List", list_id="", dataptr=self, propname="external_items", diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py index e3a20e6..587122d 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py @@ -24,8 +24,8 @@ TIMER = None -class SCENE_OT_Tree_Clipper_Import_File_Prepare(bpy.types.Operator): - bl_idname = "scene.tree_clipper_import_file_prepare" +class SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare(bpy.types.Operator): + bl_idname = "scene.tree_clipper_extended_import_file_prepare" bl_label = "Import File" bl_options = {"REGISTER"} @@ -47,12 +47,12 @@ def execute( _INTERMEDIATE_IMPORT_CACHE = ImportIntermediate(file_path=Path(self.input_file)) # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_extended_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} -class SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare(bpy.types.Operator): - bl_idname = "scene.tree_clipper_import_clipboard_prepare" +class SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare(bpy.types.Operator): + bl_idname = "scene.tree_clipper_extended_import_clipboard_prepare" bl_label = "Import Clipboard" bl_options = {"REGISTER"} @@ -65,11 +65,11 @@ def execute( ) # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_extended_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} -class SCENE_UL_Tree_Clipper_External_Import_List(bpy.types.UIList): +class SCENE_UL_Tree_Clipper_Extended_External_Import_List(bpy.types.UIList): def draw_item( self, context: bpy.types.Context, @@ -82,28 +82,28 @@ def draw_item( index: int | None, flt_flag: int | None, ) -> None: - assert isinstance(item, Tree_Clipper_External_Import_Item) + assert isinstance(item, Tree_Clipper_Extended_External_Import_Item) row = layout.row() row.label(text=item.description) row.prop(item, item.get_active_pointer_identifier(), text="") -class Tree_Clipper_External_Import_Item(bpy.types.PropertyGroup): +class Tree_Clipper_Extended_External_Import_Item(bpy.types.PropertyGroup): external_id: bpy.props.IntProperty() # type: ignore description: bpy.props.StringProperty() # type: ignore # note that this adds the member functions set_active_pointer_type and get_active_pointer_identifier -add_all_known_pointer_properties(cls=Tree_Clipper_External_Import_Item, prefix="ptr_") +add_all_known_pointer_properties(cls=Tree_Clipper_Extended_External_Import_Item, prefix="ptr_") -class Tree_Clipper_External_Import_Items(bpy.types.PropertyGroup): - items: bpy.props.CollectionProperty(type=Tree_Clipper_External_Import_Item) # type: ignore +class Tree_Clipper_Extended_External_Import_Items(bpy.types.PropertyGroup): + items: bpy.props.CollectionProperty(type=Tree_Clipper_Extended_External_Import_Item) # type: ignore selected: bpy.props.IntProperty() # type: ignore -class SCENE_OT_Tree_Clipper_Import_Cache(bpy.types.Operator): - bl_idname = "scene.tree_clipper_import_cache" +class SCENE_OT_Tree_Clipper_Extended_Import_Cache(bpy.types.Operator): + bl_idname = "scene.tree_clipper_extended_import_cache" bl_label = "Import Cache" bl_options = set() @@ -113,25 +113,25 @@ def invoke( self, context: bpy.types.Context, event: bpy.types.Event ) -> set["rna_enums.OperatorReturnItems"]: assert isinstance(_INTERMEDIATE_IMPORT_CACHE, ImportIntermediate) - assert hasattr(context.scene, "tree_clipper_external_import_items") + assert hasattr(context.scene, "tree_clipper_extended_external_import_items") assert isinstance( - context.scene.tree_clipper_external_import_items, - Tree_Clipper_External_Import_Items, + context.scene.tree_clipper_extended_external_import_items, + Tree_Clipper_Extended_External_Import_Items, ) - context.scene.tree_clipper_external_import_items.items.clear() + context.scene.tree_clipper_extended_external_import_items.items.clear() for ( external_id, external_item, ) in _INTERMEDIATE_IMPORT_CACHE.get_external().items(): if external_item["description"] is None: continue - item = context.scene.tree_clipper_external_import_items.items.add() + item = context.scene.tree_clipper_extended_external_import_items.items.add() item.external_id = int(external_id) item.description = external_item["description"] item.set_active_pointer_type(external_item["fixed_type_name"]) if ( - len(context.scene.tree_clipper_external_import_items.items) != 0 + len(context.scene.tree_clipper_extended_external_import_items.items) != 0 or get_show_advanced_options() ): return context.window_manager.invoke_props_dialog(self) # ty:ignore[possibly-missing-attribute] @@ -143,10 +143,10 @@ def execute( ) -> set["rna_enums.OperatorReturnItems"]: global _INTERMEDIATE_IMPORT_CACHE assert isinstance(_INTERMEDIATE_IMPORT_CACHE, ImportIntermediate) - assert hasattr(context.scene, "tree_clipper_external_import_items") + assert hasattr(context.scene, "tree_clipper_extended_external_import_items") assert isinstance( - context.scene.tree_clipper_external_import_items, - Tree_Clipper_External_Import_Items, + context.scene.tree_clipper_extended_external_import_items, + Tree_Clipper_Extended_External_Import_Items, ) # collect what is set from the UI @@ -155,7 +155,7 @@ def execute( external_item.external_id, external_item.get_active_pointer(), ) - for external_item in context.scene.tree_clipper_external_import_items.items + for external_item in context.scene.tree_clipper_extended_external_import_items.items ) _INTERMEDIATE_IMPORT_CACHE.start_import( @@ -168,34 +168,34 @@ def execute( # seems impossible to use bl_idname here global TIMER TIMER = time.time() - bpy.ops.scene.tree_clipper_import_modal("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_extended_import_modal("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} def draw(self, context: bpy.types.Context) -> None: - assert hasattr(context.scene, "tree_clipper_external_import_items") + assert hasattr(context.scene, "tree_clipper_extended_external_import_items") assert isinstance( - context.scene.tree_clipper_external_import_items, - Tree_Clipper_External_Import_Items, + context.scene.tree_clipper_extended_external_import_items, + Tree_Clipper_Extended_External_Import_Items, ) if get_show_advanced_options(): self.layout.prop(self, "debug_prints") # ty:ignore[possibly-missing-attribute] - if len(context.scene.tree_clipper_external_import_items.items) == 0: + if len(context.scene.tree_clipper_extended_external_import_items.items) == 0: return self.layout.label(text="References to External:") # ty:ignore[possibly-missing-attribute] self.layout.template_list( # ty:ignore[possibly-missing-attribute] - listtype_name="SCENE_UL_Tree_Clipper_External_Import_List", + listtype_name="SCENE_UL_Tree_Clipper_Extended_External_Import_List", list_id="", - dataptr=context.scene.tree_clipper_external_import_items, + dataptr=context.scene.tree_clipper_extended_external_import_items, propname="items", - active_dataptr=context.scene.tree_clipper_external_import_items, + active_dataptr=context.scene.tree_clipper_extended_external_import_items, active_propname="selected", ) -class SCENE_OT_Tree_Clipper_Import_Modal(bpy.types.Operator): - bl_idname = "scene.tree_clipper_import_modal" +class SCENE_OT_Tree_Clipper_Extended_Import_Modal(bpy.types.Operator): + bl_idname = "scene.tree_clipper_extended_import_modal" bl_label = "Import Modal" bl_options = {"UNDO"} diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py b/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py index bb6793f..d3ca845 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py @@ -1,23 +1,23 @@ import bpy -from .operators_export import SCENE_OT_Tree_Clipper_Export_Prepare +from .operators_export import SCENE_OT_Tree_Clipper_Extended_Export_Prepare from .operators_import import ( - SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare, - SCENE_OT_Tree_Clipper_Import_File_Prepare, + SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, + SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, ) -class SCENE_PT_Tree_Clipper_Panel(bpy.types.Panel): - bl_label = "Tree Clipper" +class SCENE_PT_Tree_Clipper_Extended_Panel(bpy.types.Panel): + bl_label = "Tree Clipper Extended" bl_space_type = "NODE_EDITOR" bl_region_type = "UI" - bl_category = "Tree Clipper" + bl_category = "Tree Clipper Extended" def draw(self, context: bpy.types.Context) -> None: assert isinstance(context.space_data, bpy.types.SpaceNodeEditor) export_col = self.layout.column() # ty:ignore[possibly-missing-attribute] - export_op = export_col.operator(SCENE_OT_Tree_Clipper_Export_Prepare.bl_idname) + export_op = export_col.operator(SCENE_OT_Tree_Clipper_Extended_Export_Prepare.bl_idname) node_tree = context.space_data.node_tree if node_tree is None: @@ -39,5 +39,5 @@ def draw(self, context: bpy.types.Context) -> None: self.layout.separator() # ty:ignore[possibly-missing-attribute] import_col = self.layout.column() # ty:ignore[possibly-missing-attribute] - import_col.operator(SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare.bl_idname) - import_col.operator(SCENE_OT_Tree_Clipper_Import_File_Prepare.bl_idname) + import_col.operator(SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare.bl_idname) + import_col.operator(SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare.bl_idname) diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py b/packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py index 35558fd..460bad0 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py @@ -1,7 +1,7 @@ import bpy -class TreeClipperPreferences(bpy.types.AddonPreferences): +class TreeClipperExtendedPreferences(bpy.types.AddonPreferences): bl_idname = __package__ max_clipboard_megabyte: bpy.props.IntProperty( From 3d32b2deda6b35989a64789fced2e69d7b2b746b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:23:15 +0200 Subject: [PATCH 2/7] paste node tree --- .../src/tree_clipper_addon/__init__.py | 2 + .../tree_clipper_addon/operators_import.py | 39 ++++++++++++-- .../src/tree_clipper_addon/panel.py | 2 + .../src/tree_clipper_addon/post_import.py | 51 ++++++++++++++++++- uv.lock | 9 ++-- 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py b/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py index cac0a98..445740b 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py @@ -28,6 +28,7 @@ SCENE_OT_Tree_Clipper_Extended_Import_Cache, SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, + SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes, SCENE_OT_Tree_Clipper_Extended_Import_Modal, ) @@ -48,6 +49,7 @@ SCENE_OT_Tree_Clipper_Extended_Import_Cache, SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, + SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes, SCENE_PT_Tree_Clipper_Extended_Panel, TreeClipperExtendedPreferences, ] diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py index 587122d..9612fa7 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py @@ -23,6 +23,10 @@ _INTERMEDIATE_IMPORT_CACHE = None TIMER = None +# Whether the next import should be unpacked directly into the active node tree +# (loose nodes on the canvas) instead of being wrapped in a single group node. +_UNPACK_INTO_ACTIVE_TREE = False + class SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare(bpy.types.Operator): bl_idname = "scene.tree_clipper_extended_import_file_prepare" @@ -43,8 +47,9 @@ def invoke( def execute( self, context: bpy.types.Context ) -> set["rna_enums.OperatorReturnItems"]: - global _INTERMEDIATE_IMPORT_CACHE + global _INTERMEDIATE_IMPORT_CACHE, _UNPACK_INTO_ACTIVE_TREE _INTERMEDIATE_IMPORT_CACHE = ImportIntermediate(file_path=Path(self.input_file)) + _UNPACK_INTO_ACTIVE_TREE = False # seems impossible to use bl_idname here bpy.ops.scene.tree_clipper_extended_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] @@ -59,10 +64,34 @@ class SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare(bpy.types.Operator def execute( self, context: bpy.types.Context ) -> set["rna_enums.OperatorReturnItems"]: - global _INTERMEDIATE_IMPORT_CACHE + global _INTERMEDIATE_IMPORT_CACHE, _UNPACK_INTO_ACTIVE_TREE + _INTERMEDIATE_IMPORT_CACHE = ImportIntermediate( + string=bpy.context.window_manager.clipboard # ty:ignore[possibly-missing-attribute] + ) + _UNPACK_INTO_ACTIVE_TREE = False + + # seems impossible to use bl_idname here + bpy.ops.scene.tree_clipper_extended_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + return {"FINISHED"} + + +class SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes(bpy.types.Operator): + bl_idname = "scene.tree_clipper_extended_paste_as_nodes" + bl_label = "Paste as Nodes" + bl_description = ( + "Import the node tree from the clipboard and unpack it directly into the " + "active node tree, instead of wrapping it in a single group node" + ) + bl_options = {"REGISTER"} + + def execute( + self, context: bpy.types.Context + ) -> set["rna_enums.OperatorReturnItems"]: + global _INTERMEDIATE_IMPORT_CACHE, _UNPACK_INTO_ACTIVE_TREE _INTERMEDIATE_IMPORT_CACHE = ImportIntermediate( string=bpy.context.window_manager.clipboard # ty:ignore[possibly-missing-attribute] ) + _UNPACK_INTO_ACTIVE_TREE = True # seems impossible to use bl_idname here bpy.ops.scene.tree_clipper_extended_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] @@ -250,6 +279,10 @@ def modal(self, context: bpy.types.Context, event: bpy.types.Event): _INTERMEDIATE_IMPORT_CACHE = None - post_import(context=context, event=event, report=report) + global _UNPACK_INTO_ACTIVE_TREE + unpack = _UNPACK_INTO_ACTIVE_TREE + _UNPACK_INTO_ACTIVE_TREE = False + + post_import(context=context, event=event, report=report, unpack=unpack) return {"FINISHED"} diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py b/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py index d3ca845..e32046f 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py @@ -3,6 +3,7 @@ from .operators_export import SCENE_OT_Tree_Clipper_Extended_Export_Prepare from .operators_import import ( SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, + SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes, SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, ) @@ -40,4 +41,5 @@ def draw(self, context: bpy.types.Context) -> None: import_col = self.layout.column() # ty:ignore[possibly-missing-attribute] import_col.operator(SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare.bl_idname) + import_col.operator(SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes.bl_idname) import_col.operator(SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare.bl_idname) diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py b/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py index 3761661..1035394 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py @@ -15,7 +15,56 @@ def post_import( context: bpy.types.Context, event: bpy.types.Event, report: ImportReport, + unpack: bool = False, ) -> None: + def add_unpacked() -> str | None: + if not isinstance(context.space_data, bpy.types.SpaceNodeEditor): + return "Not a node editor." + + space = context.space_data + target_tree = space.edit_tree + if target_tree is None: + return "No active tree to attach to." + + assert report.last_getter is not None + imported_root = report.last_getter() + + if target_tree.bl_rna.identifier != imported_root.bl_rna.identifier: # ty:ignore[unresolved-attribute] + return f"Editor type is {target_tree.bl_rna.identifier}, but imported {imported_root.bl_rna.identifier}." # ty:ignore[unresolved-attribute] + + # Only a reusable node-group datablock can be unpacked; an embedded + # tree (e.g. a material's node tree) has no entry here and stays grouped. + if imported_root.name not in bpy.data.node_groups: + return "Imported tree is embedded (not a node group); cannot unpack it onto the canvas." + + # Reproduce the imported nodes in the active tree via Blender's own node + # clipboard, so links, nested groups and every property come across + # faithfully. We briefly push the imported root onto the editor path to + # copy from it, then pop back so the user's current location is restored. + try: + space.path.append(imported_root) # ty:ignore[possibly-missing-attribute] + try: + for node in imported_root.nodes: # ty:ignore[unresolved-attribute] + node.select = True + bpy.ops.node.clipboard_copy() + finally: + space.path.pop() # ty:ignore[possibly-missing-attribute] + except RuntimeError as exception: + return f"Could not copy imported nodes: {exception}" + + for node in target_tree.nodes: # ty:ignore[unresolved-attribute] + node.select = False + + bpy.ops.node.clipboard_paste() + + # The root group only existed to ferry the nodes across; the pasted nodes + # are now independent in the active tree. Nested groups are referenced, not + # owned by it, so they survive its removal. + bpy.data.node_groups.remove(imported_root) # ty:ignore[invalid-argument-type] + + # let the user position the freshly pasted (and selected) nodes + bpy.ops.node.translate_attach_remove_on_cancel("INVOKE_DEFAULT") + def add_as_group() -> str | None: if not isinstance(context.space_data, bpy.types.SpaceNodeEditor): return "Not a node editor." @@ -50,7 +99,7 @@ def add_as_group() -> str | None: bpy.ops.node.translate_attach_remove_on_cancel("INVOKE_DEFAULT") - failure_reason = add_as_group() + failure_reason = add_unpacked() if unpack else add_as_group() if failure_reason is not None: def warn_popup(): diff --git a/uv.lock b/uv.lock index 96b71b4..b337a4d 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,9 @@ version = 1 revision = 3 requires-python = "==3.13.*" +[options] +prerelease-mode = "allow" + [manifest] members = [ "tree-clipper", @@ -31,9 +34,9 @@ dependencies = [ { name = "zstandard" }, ] wheels = [ - { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-05/bpy-5.2.0b0-cp313-cp313-macosx_11_0_arm64.whl" }, - { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-05/bpy-5.2.0b0-cp313-cp313-manylinux_2_39_x86_64.whl" }, - { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-05/bpy-5.2.0b0-cp313-cp313-win_amd64.whl" }, + { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-13/bpy-5.2.0b0-cp313-cp313-macosx_11_0_arm64.whl" }, + { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-13/bpy-5.2.0b0-cp313-cp313-manylinux_2_39_x86_64.whl" }, + { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-13/bpy-5.2.0b0-cp313-cp313-win_amd64.whl" }, ] [[package]] From 11e92e0e557a08b65e5cc97f30f2baa7ab196c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:38:26 +0200 Subject: [PATCH 3/7] drop node outputs --- .../tree_clipper_addon/operators_import.py | 63 ++++++++++++++++--- .../src/tree_clipper_addon/post_import.py | 41 +++++++++++- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py index 9612fa7..16ce747 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py @@ -28,6 +28,44 @@ _UNPACK_INTO_ACTIVE_TREE = False +def _prepare_import_cache( + operator: bpy.types.Operator, + *, + string: str | None = None, + file_path: "Path | None" = None, +) -> bool: + """Build the import cache from the clipboard or a file. + + Reports a readable error (instead of raising a traceback) when the input is + empty or doesn't contain a valid Tree Clipper node tree. Returns True on + success, False if the calling operator should cancel. + """ + global _INTERMEDIATE_IMPORT_CACHE + source = "clipboard" if string is not None else "file" + + if string is not None and not string.strip(): + _INTERMEDIATE_IMPORT_CACHE = None + operator.report( + {"ERROR"}, + "The clipboard is empty - copy a node tree (or a Tree Clipper string) first.", + ) + return False + + try: + _INTERMEDIATE_IMPORT_CACHE = ImportIntermediate( + string=string, file_path=file_path + ) + except (ValueError, KeyError, RuntimeError, OSError) as exception: + _INTERMEDIATE_IMPORT_CACHE = None + operator.report( + {"ERROR"}, + f"Could not read a Tree Clipper node tree from the {source}: {exception}", + ) + return False + + return True + + class SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare(bpy.types.Operator): bl_idname = "scene.tree_clipper_extended_import_file_prepare" bl_label = "Import File" @@ -47,8 +85,9 @@ def invoke( def execute( self, context: bpy.types.Context ) -> set["rna_enums.OperatorReturnItems"]: - global _INTERMEDIATE_IMPORT_CACHE, _UNPACK_INTO_ACTIVE_TREE - _INTERMEDIATE_IMPORT_CACHE = ImportIntermediate(file_path=Path(self.input_file)) + global _UNPACK_INTO_ACTIVE_TREE + if not _prepare_import_cache(self, file_path=Path(self.input_file)): + return {"CANCELLED"} _UNPACK_INTO_ACTIVE_TREE = False # seems impossible to use bl_idname here @@ -64,10 +103,12 @@ class SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare(bpy.types.Operator def execute( self, context: bpy.types.Context ) -> set["rna_enums.OperatorReturnItems"]: - global _INTERMEDIATE_IMPORT_CACHE, _UNPACK_INTO_ACTIVE_TREE - _INTERMEDIATE_IMPORT_CACHE = ImportIntermediate( - string=bpy.context.window_manager.clipboard # ty:ignore[possibly-missing-attribute] - ) + global _UNPACK_INTO_ACTIVE_TREE + if not _prepare_import_cache( + self, + string=bpy.context.window_manager.clipboard, # ty:ignore[possibly-missing-attribute] + ): + return {"CANCELLED"} _UNPACK_INTO_ACTIVE_TREE = False # seems impossible to use bl_idname here @@ -87,10 +128,12 @@ class SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes(bpy.types.Operator): def execute( self, context: bpy.types.Context ) -> set["rna_enums.OperatorReturnItems"]: - global _INTERMEDIATE_IMPORT_CACHE, _UNPACK_INTO_ACTIVE_TREE - _INTERMEDIATE_IMPORT_CACHE = ImportIntermediate( - string=bpy.context.window_manager.clipboard # ty:ignore[possibly-missing-attribute] - ) + global _UNPACK_INTO_ACTIVE_TREE + if not _prepare_import_cache( + self, + string=bpy.context.window_manager.clipboard, # ty:ignore[possibly-missing-attribute] + ): + return {"CANCELLED"} _UNPACK_INTO_ACTIVE_TREE = True # seems impossible to use bl_idname here diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py b/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py index 1035394..b33a380 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py @@ -37,6 +37,13 @@ def add_unpacked() -> str | None: if imported_root.name not in bpy.data.node_groups: return "Imported tree is embedded (not a node group); cannot unpack it onto the canvas." + # The group interface nodes only make sense inside a group; once the + # contents are pasted loose into an existing tree they're just noise, so + # drop them (and their now-dangling links) before copying. + for node in list(imported_root.nodes): # ty:ignore[unresolved-attribute] + if node.bl_idname in {"NodeGroupInput", "NodeGroupOutput"}: + imported_root.nodes.remove(node) # ty:ignore[unresolved-attribute] + # Reproduce the imported nodes in the active tree via Blender's own node # clipboard, so links, nested groups and every property come across # faithfully. We briefly push the imported root onto the editor path to @@ -57,12 +64,44 @@ def add_unpacked() -> str | None: bpy.ops.node.clipboard_paste() + # Move the freshly pasted (and selected) nodes so their center sits at the + # mouse cursor. We only shift parentless nodes so framed nodes aren't moved + # twice. The region->view conversion and ui_scale division mirror the + # single-group placement in add_as_group (node space = view space / ui_scale). + pasted_roots = [ + node + for node in target_tree.nodes # ty:ignore[unresolved-attribute] + if node.select and node.parent is None + ] + if pasted_roots: + target = context.region.view2d.region_to_view( # ty:ignore[possibly-missing-attribute] + event.mouse_region_x, event.mouse_region_y + ) + ui_scale = context.preferences.system.ui_scale # ty:ignore[possibly-missing-attribute] + target_x = target[0] / ui_scale + target_y = target[1] / ui_scale + + center_x = ( + min(node.location.x for node in pasted_roots) + + max(node.location.x for node in pasted_roots) + ) / 2 + center_y = ( + min(node.location.y for node in pasted_roots) + + max(node.location.y for node in pasted_roots) + ) / 2 + + offset_x = target_x - center_x + offset_y = target_y - center_y + for node in pasted_roots: + node.location.x += offset_x + node.location.y += offset_y + # The root group only existed to ferry the nodes across; the pasted nodes # are now independent in the active tree. Nested groups are referenced, not # owned by it, so they survive its removal. bpy.data.node_groups.remove(imported_root) # ty:ignore[invalid-argument-type] - # let the user position the freshly pasted (and selected) nodes + # leave the user in a grab so they can reposition before dropping bpy.ops.node.translate_attach_remove_on_cancel("INVOKE_DEFAULT") def add_as_group() -> str | None: From a4c7a9ffc78bbf5f4b083da437fd6c5acee8a8f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:41:35 +0200 Subject: [PATCH 4/7] still import node when context is unavailable --- .../src/tree_clipper/import_nodes.py | 17 ++++++++++++++--- uv.lock | 6 +++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/tree_clipper/src/tree_clipper/import_nodes.py b/packages/tree_clipper/src/tree_clipper/import_nodes.py index d020c57..b0c2ab3 100644 --- a/packages/tree_clipper/src/tree_clipper/import_nodes.py +++ b/packages/tree_clipper/src/tree_clipper/import_nodes.py @@ -195,8 +195,13 @@ def _import_property_simple( ): if self.debug_prints: print(f"{from_root.to_str()}: defer setting enum default for now") + location = from_root.to_str() self.set_socket_enum_defaults.append( - lambda: setattr(getter(), identifier, serialization) + ( + lambda: setattr(getter(), identifier, serialization), + location, + serialization, + ) ) return @@ -518,8 +523,14 @@ def getter() -> bpy.types.ShaderNodeTree: ) self.current_tree = None - for func in self.set_socket_enum_defaults: - func() + for func, location, value in self.set_socket_enum_defaults: + try: + func() + except (TypeError, ValueError) as e: + self.report.warnings.append( + f"{location}: could not set enum default value {value!r} " + f"(valid options may be unavailable in this context): {e}" + ) self.set_socket_enum_defaults.clear() self.report.last_getter = getter diff --git a/uv.lock b/uv.lock index b337a4d..29b32dd 100644 --- a/uv.lock +++ b/uv.lock @@ -34,9 +34,9 @@ dependencies = [ { name = "zstandard" }, ] wheels = [ - { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-13/bpy-5.2.0b0-cp313-cp313-macosx_11_0_arm64.whl" }, - { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-13/bpy-5.2.0b0-cp313-cp313-manylinux_2_39_x86_64.whl" }, - { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-13/bpy-5.2.0b0-cp313-cp313-win_amd64.whl" }, + { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-16/bpy-5.2.0b0-cp313-cp313-macosx_11_0_arm64.whl" }, + { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-16/bpy-5.2.0b0-cp313-cp313-manylinux_2_39_x86_64.whl" }, + { url = "https://github.com/BradyAJohnston/dailybpy/releases/download/daily-2026-06-16/bpy-5.2.0b0-cp313-cp313-win_amd64.whl" }, ] [[package]] From 96f14530ea054b64bb7e29471cb3049cfb6cf07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:24:57 +0200 Subject: [PATCH 5/7] distinguish group vs subtree --- .../src/tree_clipper_addon/post_import.py | 213 +++++++++++++++++- 1 file changed, 207 insertions(+), 6 deletions(-) diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py b/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py index b33a380..d97ffbf 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/post_import.py @@ -9,6 +9,97 @@ bpy.types.TextureNodeTree: bpy.types.TextureNodeGroup, } +GROUP_IO_IDNAMES = {"NodeGroupInput", "NodeGroupOutput"} + + +def _merge_interface( + src_tree: bpy.types.NodeTree, dst_tree: bpy.types.NodeTree +) -> dict[str, str]: + """Copy ``src_tree``'s interface onto ``dst_tree`` and return a map from each + source interface item identifier to the matching identifier on ``dst_tree``. + + Blender's node clipboard cannot carry a tree interface, so when the imported + nodes are pasted loose into the active tree their Group Input/Output nodes + would otherwise bind to the target tree's interface. We append the imported + sockets (reusing pre-existing ones so a second paste doesn't pile up + duplicates) and hand back the mapping so the group-io links can be rebuilt. + + We deliberately work with identifier strings rather than item references: + adding interface items can invalidate previously fetched Python wrappers. + """ + src_iface = src_tree.interface + dst_iface = dst_tree.interface + assert src_iface is not None and dst_iface is not None + + # Snapshot the sockets that already existed on the target as plain data and + # consume each match once, so repeated names (e.g. several "Scale" inputs) + # still map to distinct sockets. + existing = [ + (item.identifier, (item.name, item.in_out, item.socket_type)) + for item in dst_iface.items_tree + if item.item_type == "SOCKET" + ] + consumed: set[str] = set() + + def match_existing(key: tuple) -> str | None: + for identifier, existing_key in existing: + if identifier in consumed: + continue + if existing_key == key: + consumed.add(identifier) + return identifier + return None + + def find_panel(identifier: str) -> bpy.types.NodeTreeInterfacePanel | None: + for item in dst_iface.items_tree: + if item.item_type == "PANEL" and item.identifier == identifier: + return item # ty:ignore[invalid-return-type, unresolved-attribute] + return None + + iface_map: dict[str, str] = {} + panel_map: dict[str, str] = {} # src panel identifier -> dst panel identifier + + for item in src_iface.items_tree: + parent_dst_id: str | None = None + if item.parent is not None and item.parent.index >= 0: + parent_dst_id = panel_map.get(item.parent.identifier) # ty:ignore[unresolved-attribute] + + if item.item_type == "PANEL": + new_panel = dst_iface.new_panel( + name=item.name, + description=item.description, + default_closed=item.default_closed, + ) + new_identifier = new_panel.identifier # ty:ignore[unresolved-attribute] + if parent_dst_id is not None: + parent = find_panel(parent_dst_id) + if parent is not None: + dst_iface.move_to_parent( + item=new_panel, + parent=parent, + to_position=len(parent.interface_items), + ) + panel_map[item.identifier] = new_identifier + iface_map[item.identifier] = new_identifier + continue + + matched = match_existing((item.name, item.in_out, item.socket_type)) + if matched is not None: + iface_map[item.identifier] = matched + continue + + parent = find_panel(parent_dst_id) if parent_dst_id is not None else None + new_socket = dst_iface.new_socket( + name=item.name, + description=item.description, + in_out=item.in_out, + socket_type=item.socket_type, + parent=parent, + ) + iface_map[item.identifier] = new_socket.identifier + + return iface_map + def post_import( *, @@ -37,12 +128,62 @@ def add_unpacked() -> str | None: if imported_root.name not in bpy.data.node_groups: return "Imported tree is embedded (not a node group); cannot unpack it onto the canvas." - # The group interface nodes only make sense inside a group; once the - # contents are pasted loose into an existing tree they're just noise, so - # drop them (and their now-dangling links) before copying. - for node in list(imported_root.nodes): # ty:ignore[unresolved-attribute] - if node.bl_idname in {"NodeGroupInput", "NodeGroupOutput"}: - imported_root.nodes.remove(node) # ty:ignore[unresolved-attribute] + def is_group_io(node: bpy.types.Node) -> bool: + return node.bl_idname in GROUP_IO_IDNAMES + + # A self-contained group carries its own interface *and* Group + # Input/Output nodes. Pasted loose it would duplicate the host tree's + # interface nodes, blend unrelated interface sockets together and leave a + # dead "Unused Output", so such content belongs in a group node: hand it + # to the grouped path instead of unpacking it. + interface = imported_root.interface # ty:ignore[unresolved-attribute] + if ( + interface is not None + and len(interface.items_tree) > 0 + and any(is_group_io(node) for node in imported_root.nodes) # ty:ignore[unresolved-attribute] + ): + return add_as_group() + + # Selection is expressed by what's present in the magic string: the web + # renderer filters unselected nodes out on copy, so any Group Input/Output + # node that survived into the import was meant to come across. We keep them + # (with the group's interface) whenever they're present, and otherwise + # skip the interface merge so we don't pollute the target tree. + keep_group_io = any( + is_group_io(node) + for node in imported_root.nodes # ty:ignore[unresolved-attribute] + ) + + # The clipboard can't carry a tree interface or its Group Input/Output + # links, so when we keep those nodes we capture both before copying. We + # tag every imported node with a unique sentinel name (the clipboard + # preserves unique names) to pair it with its pasted copy afterwards, and + # record each link touching a group-io node to rebuild once the interface + # is in place. + sentinel_prefix = "_tc_unpack_sentinel_" + original_names: dict[str, str] = {} + # (from_sentinel, from_socket_id, from_via_iface, + # to_sentinel, to_socket_id, to_via_iface) + group_io_links: list[tuple[str, str, bool, str, str, bool]] = [] + if keep_group_io: + for index, node in enumerate(imported_root.nodes): # ty:ignore[unresolved-attribute] + sentinel = f"{sentinel_prefix}{index}" + original_names[sentinel] = node.name + node.name = sentinel + + for link in imported_root.links: # ty:ignore[unresolved-attribute] + if not (is_group_io(link.from_node) or is_group_io(link.to_node)): + continue + group_io_links.append( + ( + link.from_node.name, + link.from_socket.identifier, + is_group_io(link.from_node), + link.to_node.name, + link.to_socket.identifier, + is_group_io(link.to_node), + ) + ) # Reproduce the imported nodes in the active tree via Blender's own node # clipboard, so links, nested groups and every property come across @@ -57,6 +198,8 @@ def add_unpacked() -> str | None: finally: space.path.pop() # ty:ignore[possibly-missing-attribute] except RuntimeError as exception: + for sentinel, name in original_names.items(): + imported_root.nodes[sentinel].name = name # ty:ignore[unresolved-attribute] return f"Could not copy imported nodes: {exception}" for node in target_tree.nodes: # ty:ignore[unresolved-attribute] @@ -64,6 +207,64 @@ def add_unpacked() -> str | None: bpy.ops.node.clipboard_paste() + # Bring the imported group's interface onto the target tree so the pasted + # Group Input/Output nodes expose the right sockets, then rebuild the + # links the clipboard dropped. Sentinel names still identify the freshly + # pasted nodes at this point; we restore the original names afterwards. + if keep_group_io: + iface_map = _merge_interface(imported_root, target_tree) # ty:ignore[invalid-argument-type] + pasted_by_sentinel = { + node.name: node + for node in target_tree.nodes # ty:ignore[unresolved-attribute] + if node.name in original_names + } + + def resolve_socket( + node: bpy.types.Node, + socket_id: str, + *, + want_input: bool, + via_iface: bool, + ) -> bpy.types.NodeSocket | None: + if via_iface: + mapped = iface_map.get(socket_id) + if mapped is None: + return None + socket_id = mapped + sockets = node.inputs if want_input else node.outputs + for socket in sockets: + if socket.identifier == socket_id: + return socket + return None + + for ( + from_sentinel, + from_socket_id, + from_via_iface, + to_sentinel, + to_socket_id, + to_via_iface, + ) in group_io_links: + from_node = pasted_by_sentinel.get(from_sentinel) + to_node = pasted_by_sentinel.get(to_sentinel) + if from_node is None or to_node is None: + continue + from_socket = resolve_socket( + from_node, from_socket_id, want_input=False, via_iface=from_via_iface + ) + to_socket = resolve_socket( + to_node, to_socket_id, want_input=True, via_iface=to_via_iface + ) + if from_socket is not None and to_socket is not None: + target_tree.links.new(from_socket, to_socket) # ty:ignore[unresolved-attribute] + + # Hand the user-facing names back to the pasted nodes (Blender + # resolves any collisions with existing nodes automatically). + for sentinel, name in original_names.items(): + node = pasted_by_sentinel.get(sentinel) + if node is not None: + node.name = name + # Move the freshly pasted (and selected) nodes so their center sits at the # mouse cursor. We only shift parentless nodes so framed nodes aren't moved # twice. The region->view conversion and ui_scale division mirror the From 13890f191d35c05afb5d3eb972f4631b4016515d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:34:03 +0200 Subject: [PATCH 6/7] use original names again --- README.md | 4 +- .../src/tree_clipper_addon/__init__.py | 68 ++++++++--------- .../tree_clipper_addon/blender_manifest.toml | 4 +- .../tree_clipper_addon/operators_export.py | 26 +++---- .../tree_clipper_addon/operators_import.py | 74 +++++++++---------- .../src/tree_clipper_addon/panel.py | 22 +++--- .../src/tree_clipper_addon/preferences.py | 2 +- 7 files changed, 99 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 9eaabf9..6b77335 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ Featured Image -# Tree Clipper Extended - -> A fork of [Tree Clipper](https://github.com/Algebraic-UG/tree_clipper) renamed so it can be installed alongside the original without colliding (distinct extension id, operators, panel and properties). +# Tree Clipper Easier version control and sharing of node trees via `.json` or copy-pasteable strings. diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py b/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py index 445740b..4eed79a 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py @@ -14,58 +14,58 @@ import bpy from .operators_export import ( - Tree_Clipper_Extended_External_Export_Item, - SCENE_UL_Tree_Clipper_Extended_External_Export_List, - SCENE_OT_Tree_Clipper_Extended_Export_Cache, - SCENE_OT_Tree_Clipper_Extended_Export_Modal, - SCENE_OT_Tree_Clipper_Extended_Export_Prepare, + Tree_Clipper_External_Export_Item, + SCENE_UL_Tree_Clipper_External_Export_List, + SCENE_OT_Tree_Clipper_Export_Cache, + SCENE_OT_Tree_Clipper_Export_Modal, + SCENE_OT_Tree_Clipper_Export_Prepare, ) from .operators_import import ( - Tree_Clipper_Extended_External_Import_Item, - Tree_Clipper_Extended_External_Import_Items, - SCENE_UL_Tree_Clipper_Extended_External_Import_List, - SCENE_OT_Tree_Clipper_Extended_Import_Cache, - SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, - SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, - SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes, - SCENE_OT_Tree_Clipper_Extended_Import_Modal, + Tree_Clipper_External_Import_Item, + Tree_Clipper_External_Import_Items, + SCENE_UL_Tree_Clipper_External_Import_List, + SCENE_OT_Tree_Clipper_Import_Cache, + SCENE_OT_Tree_Clipper_Import_File_Prepare, + SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare, + SCENE_OT_Tree_Clipper_Paste_As_Nodes, + SCENE_OT_Tree_Clipper_Import_Modal, ) -from .panel import SCENE_PT_Tree_Clipper_Extended_Panel +from .panel import SCENE_PT_Tree_Clipper_Panel -from .preferences import TreeClipperExtendedPreferences +from .preferences import TreeClipperPreferences classes = [ - Tree_Clipper_Extended_External_Export_Item, - SCENE_UL_Tree_Clipper_Extended_External_Export_List, - SCENE_OT_Tree_Clipper_Extended_Export_Cache, - SCENE_OT_Tree_Clipper_Extended_Export_Modal, - SCENE_OT_Tree_Clipper_Extended_Export_Prepare, - Tree_Clipper_Extended_External_Import_Item, - Tree_Clipper_Extended_External_Import_Items, - SCENE_UL_Tree_Clipper_Extended_External_Import_List, - SCENE_OT_Tree_Clipper_Extended_Import_Modal, - SCENE_OT_Tree_Clipper_Extended_Import_Cache, - SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, - SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, - SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes, - SCENE_PT_Tree_Clipper_Extended_Panel, - TreeClipperExtendedPreferences, + Tree_Clipper_External_Export_Item, + SCENE_UL_Tree_Clipper_External_Export_List, + SCENE_OT_Tree_Clipper_Export_Cache, + SCENE_OT_Tree_Clipper_Export_Modal, + SCENE_OT_Tree_Clipper_Export_Prepare, + Tree_Clipper_External_Import_Item, + Tree_Clipper_External_Import_Items, + SCENE_UL_Tree_Clipper_External_Import_List, + SCENE_OT_Tree_Clipper_Import_Modal, + SCENE_OT_Tree_Clipper_Import_Cache, + SCENE_OT_Tree_Clipper_Import_File_Prepare, + SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare, + SCENE_OT_Tree_Clipper_Paste_As_Nodes, + SCENE_PT_Tree_Clipper_Panel, + TreeClipperPreferences, ] def register() -> None: - print("Registering Tree Clipper Extended") + print("Registering Tree Clipper") for cls in classes: bpy.utils.register_class(cls) # the pointer properties in the items make it impossible to store on the operator - bpy.types.Scene.tree_clipper_extended_external_import_items = bpy.props.PointerProperty( # ty: ignore[unresolved-attribute] - type=Tree_Clipper_Extended_External_Import_Items + bpy.types.Scene.tree_clipper_external_import_items = bpy.props.PointerProperty( # ty: ignore[unresolved-attribute] + type=Tree_Clipper_External_Import_Items ) def unregister() -> None: - del bpy.types.Scene.tree_clipper_extended_external_import_items # ty: ignore[unresolved-attribute] + del bpy.types.Scene.tree_clipper_external_import_items # ty: ignore[unresolved-attribute] for cls in reversed(classes): bpy.utils.unregister_class(cls) diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml b/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml index 4bdf803..60aeac5 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml @@ -2,9 +2,9 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension -id = "tree_clipper_extended" +id = "tree_clipper" version = "0.1.6" # should match tree_clipper (core logic) -name = "Tree Clipper Extended" +name = "Tree Clipper" tagline = "Export and import Blender node trees as JSON" maintainer = "Algebraic " # Supported types: "add-on", "theme" diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_export.py b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_export.py index 671c79a..cf97589 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_export.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_export.py @@ -20,8 +20,8 @@ _INTERMEDIATE_EXPORT_CACHE = None -class SCENE_OT_Tree_Clipper_Extended_Export_Prepare(bpy.types.Operator): - bl_idname = "scene.tree_clipper_extended_export_prepare" +class SCENE_OT_Tree_Clipper_Export_Prepare(bpy.types.Operator): + bl_idname = "scene.tree_clipper_export_prepare" bl_label = "Export" bl_options = {"REGISTER"} @@ -56,7 +56,7 @@ def execute( ) # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_extended_export_modal("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_export_modal("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} def draw(self, context: bpy.types.Context) -> None: @@ -69,8 +69,8 @@ def draw(self, context: bpy.types.Context) -> None: self.layout.prop(self, "write_from_roots") # ty:ignore[possibly-missing-attribute] -class SCENE_OT_Tree_Clipper_Extended_Export_Modal(bpy.types.Operator): - bl_idname = "scene.tree_clipper_extended_export_modal" +class SCENE_OT_Tree_Clipper_Export_Modal(bpy.types.Operator): + bl_idname = "scene.tree_clipper_export_modal" bl_label = "Export Modal" bl_options = set() @@ -111,11 +111,11 @@ def modal(self, context, event): self.report({"WARNING"}, warning) # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_extended_export_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_export_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} -class SCENE_UL_Tree_Clipper_Extended_External_Export_List(bpy.types.UIList): +class SCENE_UL_Tree_Clipper_External_Export_List(bpy.types.UIList): def draw_item( self, context: bpy.types.Context, @@ -129,7 +129,7 @@ def draw_item( flt_flag: int | None, ) -> None: assert isinstance(_INTERMEDIATE_EXPORT_CACHE, ExportIntermediate) - assert isinstance(item, Tree_Clipper_Extended_External_Export_Item) + assert isinstance(item, Tree_Clipper_External_Export_Item) external = _INTERMEDIATE_EXPORT_CACHE.get_external()[item.external_id] pointer = external.pointed_to_by row = layout.row() @@ -138,7 +138,7 @@ def draw_item( row.prop(item, "skip") -class Tree_Clipper_Extended_External_Export_Item(bpy.types.PropertyGroup): +class Tree_Clipper_External_Export_Item(bpy.types.PropertyGroup): external_id: bpy.props.IntProperty() # type: ignore description: bpy.props.StringProperty(name="", default=DEFAULT_HINT) # type: ignore skip: bpy.props.BoolProperty(name="Hide in Import", default=False) # type: ignore @@ -150,8 +150,8 @@ class Tree_Clipper_Extended_External_Export_Item(bpy.types.PropertyGroup): _FILE = "File" -class SCENE_OT_Tree_Clipper_Extended_Export_Cache(bpy.types.Operator): - bl_idname = "scene.tree_clipper_extended_export_cache" +class SCENE_OT_Tree_Clipper_Export_Cache(bpy.types.Operator): + bl_idname = "scene.tree_clipper_export_cache" bl_label = "Export Cache" bl_options = set() @@ -165,7 +165,7 @@ class SCENE_OT_Tree_Clipper_Extended_Export_Cache(bpy.types.Operator): compress_or_json: bpy.props.EnumProperty(items=[(_COMPRESS,) * 3, (_JSON,) * 3]) # type: ignore json_indent: bpy.props.IntProperty(name="JSON Indent", default=4, min=0) # type: ignore - external_items: bpy.props.CollectionProperty(type=Tree_Clipper_Extended_External_Export_Item) # type: ignore + external_items: bpy.props.CollectionProperty(type=Tree_Clipper_External_Export_Item) # type: ignore selected_external_item: bpy.props.IntProperty() # type: ignore def invoke( @@ -235,7 +235,7 @@ def draw(self, context: bpy.types.Context) -> None: self.layout.label(text="References to External:") # ty:ignore[possibly-missing-attribute] self.layout.template_list( # ty:ignore[possibly-missing-attribute] - listtype_name="SCENE_UL_Tree_Clipper_Extended_External_Export_List", + listtype_name="SCENE_UL_Tree_Clipper_External_Export_List", list_id="", dataptr=self, propname="external_items", diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py index 16ce747..69adad1 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/operators_import.py @@ -66,8 +66,8 @@ def _prepare_import_cache( return True -class SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare(bpy.types.Operator): - bl_idname = "scene.tree_clipper_extended_import_file_prepare" +class SCENE_OT_Tree_Clipper_Import_File_Prepare(bpy.types.Operator): + bl_idname = "scene.tree_clipper_import_file_prepare" bl_label = "Import File" bl_options = {"REGISTER"} @@ -91,12 +91,12 @@ def execute( _UNPACK_INTO_ACTIVE_TREE = False # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_extended_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} -class SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare(bpy.types.Operator): - bl_idname = "scene.tree_clipper_extended_import_clipboard_prepare" +class SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare(bpy.types.Operator): + bl_idname = "scene.tree_clipper_import_clipboard_prepare" bl_label = "Import Clipboard" bl_options = {"REGISTER"} @@ -112,12 +112,12 @@ def execute( _UNPACK_INTO_ACTIVE_TREE = False # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_extended_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} -class SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes(bpy.types.Operator): - bl_idname = "scene.tree_clipper_extended_paste_as_nodes" +class SCENE_OT_Tree_Clipper_Paste_As_Nodes(bpy.types.Operator): + bl_idname = "scene.tree_clipper_paste_as_nodes" bl_label = "Paste as Nodes" bl_description = ( "Import the node tree from the clipboard and unpack it directly into the " @@ -137,11 +137,11 @@ def execute( _UNPACK_INTO_ACTIVE_TREE = True # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_extended_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_import_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} -class SCENE_UL_Tree_Clipper_Extended_External_Import_List(bpy.types.UIList): +class SCENE_UL_Tree_Clipper_External_Import_List(bpy.types.UIList): def draw_item( self, context: bpy.types.Context, @@ -154,28 +154,28 @@ def draw_item( index: int | None, flt_flag: int | None, ) -> None: - assert isinstance(item, Tree_Clipper_Extended_External_Import_Item) + assert isinstance(item, Tree_Clipper_External_Import_Item) row = layout.row() row.label(text=item.description) row.prop(item, item.get_active_pointer_identifier(), text="") -class Tree_Clipper_Extended_External_Import_Item(bpy.types.PropertyGroup): +class Tree_Clipper_External_Import_Item(bpy.types.PropertyGroup): external_id: bpy.props.IntProperty() # type: ignore description: bpy.props.StringProperty() # type: ignore # note that this adds the member functions set_active_pointer_type and get_active_pointer_identifier -add_all_known_pointer_properties(cls=Tree_Clipper_Extended_External_Import_Item, prefix="ptr_") +add_all_known_pointer_properties(cls=Tree_Clipper_External_Import_Item, prefix="ptr_") -class Tree_Clipper_Extended_External_Import_Items(bpy.types.PropertyGroup): - items: bpy.props.CollectionProperty(type=Tree_Clipper_Extended_External_Import_Item) # type: ignore +class Tree_Clipper_External_Import_Items(bpy.types.PropertyGroup): + items: bpy.props.CollectionProperty(type=Tree_Clipper_External_Import_Item) # type: ignore selected: bpy.props.IntProperty() # type: ignore -class SCENE_OT_Tree_Clipper_Extended_Import_Cache(bpy.types.Operator): - bl_idname = "scene.tree_clipper_extended_import_cache" +class SCENE_OT_Tree_Clipper_Import_Cache(bpy.types.Operator): + bl_idname = "scene.tree_clipper_import_cache" bl_label = "Import Cache" bl_options = set() @@ -185,25 +185,25 @@ def invoke( self, context: bpy.types.Context, event: bpy.types.Event ) -> set["rna_enums.OperatorReturnItems"]: assert isinstance(_INTERMEDIATE_IMPORT_CACHE, ImportIntermediate) - assert hasattr(context.scene, "tree_clipper_extended_external_import_items") + assert hasattr(context.scene, "tree_clipper_external_import_items") assert isinstance( - context.scene.tree_clipper_extended_external_import_items, - Tree_Clipper_Extended_External_Import_Items, + context.scene.tree_clipper_external_import_items, + Tree_Clipper_External_Import_Items, ) - context.scene.tree_clipper_extended_external_import_items.items.clear() + context.scene.tree_clipper_external_import_items.items.clear() for ( external_id, external_item, ) in _INTERMEDIATE_IMPORT_CACHE.get_external().items(): if external_item["description"] is None: continue - item = context.scene.tree_clipper_extended_external_import_items.items.add() + item = context.scene.tree_clipper_external_import_items.items.add() item.external_id = int(external_id) item.description = external_item["description"] item.set_active_pointer_type(external_item["fixed_type_name"]) if ( - len(context.scene.tree_clipper_extended_external_import_items.items) != 0 + len(context.scene.tree_clipper_external_import_items.items) != 0 or get_show_advanced_options() ): return context.window_manager.invoke_props_dialog(self) # ty:ignore[possibly-missing-attribute] @@ -215,10 +215,10 @@ def execute( ) -> set["rna_enums.OperatorReturnItems"]: global _INTERMEDIATE_IMPORT_CACHE assert isinstance(_INTERMEDIATE_IMPORT_CACHE, ImportIntermediate) - assert hasattr(context.scene, "tree_clipper_extended_external_import_items") + assert hasattr(context.scene, "tree_clipper_external_import_items") assert isinstance( - context.scene.tree_clipper_extended_external_import_items, - Tree_Clipper_Extended_External_Import_Items, + context.scene.tree_clipper_external_import_items, + Tree_Clipper_External_Import_Items, ) # collect what is set from the UI @@ -227,7 +227,7 @@ def execute( external_item.external_id, external_item.get_active_pointer(), ) - for external_item in context.scene.tree_clipper_extended_external_import_items.items + for external_item in context.scene.tree_clipper_external_import_items.items ) _INTERMEDIATE_IMPORT_CACHE.start_import( @@ -240,34 +240,34 @@ def execute( # seems impossible to use bl_idname here global TIMER TIMER = time.time() - bpy.ops.scene.tree_clipper_extended_import_modal("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + bpy.ops.scene.tree_clipper_import_modal("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} def draw(self, context: bpy.types.Context) -> None: - assert hasattr(context.scene, "tree_clipper_extended_external_import_items") + assert hasattr(context.scene, "tree_clipper_external_import_items") assert isinstance( - context.scene.tree_clipper_extended_external_import_items, - Tree_Clipper_Extended_External_Import_Items, + context.scene.tree_clipper_external_import_items, + Tree_Clipper_External_Import_Items, ) if get_show_advanced_options(): self.layout.prop(self, "debug_prints") # ty:ignore[possibly-missing-attribute] - if len(context.scene.tree_clipper_extended_external_import_items.items) == 0: + if len(context.scene.tree_clipper_external_import_items.items) == 0: return self.layout.label(text="References to External:") # ty:ignore[possibly-missing-attribute] self.layout.template_list( # ty:ignore[possibly-missing-attribute] - listtype_name="SCENE_UL_Tree_Clipper_Extended_External_Import_List", + listtype_name="SCENE_UL_Tree_Clipper_External_Import_List", list_id="", - dataptr=context.scene.tree_clipper_extended_external_import_items, + dataptr=context.scene.tree_clipper_external_import_items, propname="items", - active_dataptr=context.scene.tree_clipper_extended_external_import_items, + active_dataptr=context.scene.tree_clipper_external_import_items, active_propname="selected", ) -class SCENE_OT_Tree_Clipper_Extended_Import_Modal(bpy.types.Operator): - bl_idname = "scene.tree_clipper_extended_import_modal" +class SCENE_OT_Tree_Clipper_Import_Modal(bpy.types.Operator): + bl_idname = "scene.tree_clipper_import_modal" bl_label = "Import Modal" bl_options = {"UNDO"} diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py b/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py index e32046f..2ba3900 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/panel.py @@ -1,24 +1,24 @@ import bpy -from .operators_export import SCENE_OT_Tree_Clipper_Extended_Export_Prepare +from .operators_export import SCENE_OT_Tree_Clipper_Export_Prepare from .operators_import import ( - SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare, - SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes, - SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare, + SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare, + SCENE_OT_Tree_Clipper_Paste_As_Nodes, + SCENE_OT_Tree_Clipper_Import_File_Prepare, ) -class SCENE_PT_Tree_Clipper_Extended_Panel(bpy.types.Panel): - bl_label = "Tree Clipper Extended" +class SCENE_PT_Tree_Clipper_Panel(bpy.types.Panel): + bl_label = "Tree Clipper" bl_space_type = "NODE_EDITOR" bl_region_type = "UI" - bl_category = "Tree Clipper Extended" + bl_category = "Tree Clipper" def draw(self, context: bpy.types.Context) -> None: assert isinstance(context.space_data, bpy.types.SpaceNodeEditor) export_col = self.layout.column() # ty:ignore[possibly-missing-attribute] - export_op = export_col.operator(SCENE_OT_Tree_Clipper_Extended_Export_Prepare.bl_idname) + export_op = export_col.operator(SCENE_OT_Tree_Clipper_Export_Prepare.bl_idname) node_tree = context.space_data.node_tree if node_tree is None: @@ -40,6 +40,6 @@ def draw(self, context: bpy.types.Context) -> None: self.layout.separator() # ty:ignore[possibly-missing-attribute] import_col = self.layout.column() # ty:ignore[possibly-missing-attribute] - import_col.operator(SCENE_OT_Tree_Clipper_Extended_Import_Clipboard_Prepare.bl_idname) - import_col.operator(SCENE_OT_Tree_Clipper_Extended_Paste_As_Nodes.bl_idname) - import_col.operator(SCENE_OT_Tree_Clipper_Extended_Import_File_Prepare.bl_idname) + import_col.operator(SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare.bl_idname) + import_col.operator(SCENE_OT_Tree_Clipper_Paste_As_Nodes.bl_idname) + import_col.operator(SCENE_OT_Tree_Clipper_Import_File_Prepare.bl_idname) diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py b/packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py index 460bad0..35558fd 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/preferences.py @@ -1,7 +1,7 @@ import bpy -class TreeClipperExtendedPreferences(bpy.types.AddonPreferences): +class TreeClipperPreferences(bpy.types.AddonPreferences): bl_idname = __package__ max_clipboard_megabyte: bpy.props.IntProperty( From cbc8a10bb494a112e814cb9f92aa54786a18a506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Hendrik=20M=C3=BCller?= <44469195+kolibril13@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:53:47 +0200 Subject: [PATCH 7/7] Bump tree-clipper to 0.2.0 - Update core package version in pyproject.toml and common.py - Update addon manifest version to match - Ignore all .zip build artifacts Co-Authored-By: Claude Haiku 4.5 --- .gitignore | 5 ++++- packages/tree_clipper/pyproject.toml | 2 +- .../tree_clipper/src/tree_clipper/common.py | 2 +- .../tree_clipper_addon/blender_manifest.toml | 2 +- .../tree_clipper_addon/tree_clipper-0.2.0.zip | Bin 0 -> 58994 bytes uv.lock | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 packages/tree_clipper_addon/src/tree_clipper_addon/tree_clipper-0.2.0.zip diff --git a/.gitignore b/.gitignore index 7b54387..e7ac114 100644 --- a/.gitignore +++ b/.gitignore @@ -215,4 +215,7 @@ __marimo__/ # Streamlit .streamlit/secrets.toml -packages/tree_clipper_addon/src/tree_clipper_addon/_vendor/ \ No newline at end of file +packages/tree_clipper_addon/src/tree_clipper_addon/_vendor/ + +# Build artifacts +*.zip \ No newline at end of file diff --git a/packages/tree_clipper/pyproject.toml b/packages/tree_clipper/pyproject.toml index 1a2c7e9..f6bf457 100644 --- a/packages/tree_clipper/pyproject.toml +++ b/packages/tree_clipper/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tree-clipper" -version = "0.1.6" +version = "0.2.0" dependencies = [] maintainers = [ { name="Algebraic", email="tree.clipper@algebraic.games" }, diff --git a/packages/tree_clipper/src/tree_clipper/common.py b/packages/tree_clipper/src/tree_clipper/common.py index 34bc622..6d533d2 100644 --- a/packages/tree_clipper/src/tree_clipper/common.py +++ b/packages/tree_clipper/src/tree_clipper/common.py @@ -13,7 +13,7 @@ # these fields are in the top level JSON object BLENDER_VERSION = "blender_version" TREE_CLIPPER_VERSION = "tree_clipper_version" -CURRENT_TREE_CLIPPER_VERSION = "0.1.6" # tested to match pyproject.toml +CURRENT_TREE_CLIPPER_VERSION = "0.2.0" # tested to match pyproject.toml MATERIAL_NAME = "name" TREES = "node_trees" EXTERNAL = "external" diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml b/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml index 60aeac5..6fb989d 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension id = "tree_clipper" -version = "0.1.6" # should match tree_clipper (core logic) +version = "0.2.0" # should match tree_clipper (core logic) name = "Tree Clipper" tagline = "Export and import Blender node trees as JSON" maintainer = "Algebraic " diff --git a/packages/tree_clipper_addon/src/tree_clipper_addon/tree_clipper-0.2.0.zip b/packages/tree_clipper_addon/src/tree_clipper_addon/tree_clipper-0.2.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..e649001108a477b1331c1f5d4e359fd9c6ee736a GIT binary patch literal 58994 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 E0IS`tB>(^b literal 0 HcmV?d00001 diff --git a/uv.lock b/uv.lock index 29b32dd..0b933a0 100644 --- a/uv.lock +++ b/uv.lock @@ -278,7 +278,7 @@ wheels = [ [[package]] name = "tree-clipper" -version = "0.1.6" +version = "0.2.0" source = { editable = "packages/tree_clipper" } [package.optional-dependencies]