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 2db9dc7..32f5f31 100644 --- a/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/__init__.py @@ -13,7 +13,9 @@ import bpy +from .context_menu import draw_tree_clipper_context_menu from .operators_export import ( + SCENE_OT_Tree_Clipper_Copy_Magic_Node_String, SCENE_OT_Tree_Clipper_Export_Cache, SCENE_OT_Tree_Clipper_Export_Modal, SCENE_OT_Tree_Clipper_Export_Prepare, @@ -38,6 +40,7 @@ SCENE_OT_Tree_Clipper_Export_Cache, SCENE_OT_Tree_Clipper_Export_Modal, SCENE_OT_Tree_Clipper_Export_Prepare, + SCENE_OT_Tree_Clipper_Copy_Magic_Node_String, Tree_Clipper_External_Import_Item, Tree_Clipper_External_Import_Items, SCENE_UL_Tree_Clipper_External_Import_List, @@ -58,9 +61,11 @@ def register() -> None: bpy.types.Scene.tree_clipper_external_import_items = bpy.props.PointerProperty( # ty: ignore[unresolved-attribute] type=Tree_Clipper_External_Import_Items ) + bpy.types.NODE_MT_context_menu.append(draw_tree_clipper_context_menu) def unregister() -> None: + bpy.types.NODE_MT_context_menu.remove(draw_tree_clipper_context_menu) 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/context_menu.py b/packages/tree_clipper_addon/src/tree_clipper_addon/context_menu.py new file mode 100644 index 0000000..90752c7 --- /dev/null +++ b/packages/tree_clipper_addon/src/tree_clipper_addon/context_menu.py @@ -0,0 +1,35 @@ +from typing import Any + +import bpy + +from .operators_export import SCENE_OT_Tree_Clipper_Copy_Magic_Node_String +from .operators_import import SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare + + +def draw_tree_clipper_context_menu(self: Any, context: bpy.types.Context) -> None: + space = context.space_data + if not isinstance(space, bpy.types.SpaceNodeEditor): + return + + selected_nodes = getattr(context, "selected_nodes", ()) + if space.tree_type == "GeometryNodeTree" and not selected_nodes: + self.layout.separator() + self.layout.operator( + SCENE_OT_Tree_Clipper_Import_Clipboard_Prepare.bl_idname, + text="Paste Magic Node String", + icon="PASTEDOWN", + ) + return + + active_node = getattr(context, "active_node", None) + if ( + active_node is not None + and active_node.type == "GROUP" + and active_node.node_tree is not None + ): + self.layout.separator() + self.layout.operator( + SCENE_OT_Tree_Clipper_Copy_Magic_Node_String.bl_idname, + text="Copy Magic Node String", + icon="COPYDOWN", + ) 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 4f62822..77881d1 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 @@ -18,6 +18,22 @@ _INTERMEDIATE_EXPORT_CACHE = None +def _copy_export_to_clipboard( + context: bpy.types.Context, intermediate: ExportIntermediate +) -> None: + string = intermediate.export_to_str(compress=True, json_indent=0) + + # https://github.com/Algebraic-UG/tree_clipper/issues/134 + if len(string.encode("utf-8")) > get_max_clipboard_bytes(): + raise RuntimeError( + f"The export exceeds the clipboard limit ({get_max_clipboard_bytes()}) set in the addon preferences." + ) + + window_manager = context.window_manager + assert window_manager is not None + window_manager.clipboard = string + + class SCENE_OT_Tree_Clipper_Export_Prepare(bpy.types.Operator): bl_idname = "scene.tree_clipper_export_prepare" bl_label = "Export" @@ -67,6 +83,47 @@ 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_Copy_Magic_Node_String(bpy.types.Operator): + bl_idname = "scene.tree_clipper_copy_magic_node_string" + bl_label = "Copy Magic Node String" + bl_description = "Copy this node group's compressed Tree Clipper string" + bl_options: ClassVar[set[str]] = {"REGISTER"} # ty:ignore[invalid-attribute-override] + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + node = getattr(context, "active_node", None) + return ( + isinstance(context.space_data, bpy.types.SpaceNodeEditor) + and node is not None + and node.type == "GROUP" + and node.node_tree is not None + ) + + def execute( + self, context: bpy.types.Context + ) -> set["rna_enums.OperatorReturnItems"]: + global _INTERMEDIATE_EXPORT_CACHE + node = context.active_node + assert node is not None and node.type == "GROUP" + node_tree = node.node_tree # ty:ignore[unresolved-attribute] + assert node_tree is not None + _INTERMEDIATE_EXPORT_CACHE = ExportIntermediate( + ExportParameters( + is_material=False, + name=node_tree.name, + specific_handlers=BUILT_IN_EXPORTER, + export_sub_trees=True, + debug_prints=False, + write_from_roots=False, + ) + ) + + bpy.ops.scene.tree_clipper_export_modal( # ty:ignore[unresolved-attribute] + "INVOKE_DEFAULT", direct_to_clipboard=True + ) + return {"FINISHED"} + + class SCENE_OT_Tree_Clipper_Export_Modal(bpy.types.Operator): bl_idname = "scene.tree_clipper_export_modal" bl_label = "Export Modal" @@ -74,6 +131,8 @@ class SCENE_OT_Tree_Clipper_Export_Modal(bpy.types.Operator): _timer = None + direct_to_clipboard: bpy.props.BoolProperty(default=False) # type: ignore + def invoke( self, context: bpy.types.Context, event: bpy.types.Event ) -> set["rna_enums.OperatorReturnItems"]: @@ -108,8 +167,18 @@ def modal(self, context, event): for warning in _INTERMEDIATE_EXPORT_CACHE.exporter.report.warnings: self.report({"WARNING"}, warning) - # seems impossible to use bl_idname here - bpy.ops.scene.tree_clipper_export_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] + if self.direct_to_clipboard: + try: + _copy_export_to_clipboard(context, _INTERMEDIATE_EXPORT_CACHE) + except RuntimeError as error: + self.report({"ERROR"}, str(error)) + _INTERMEDIATE_EXPORT_CACHE = None + return {"CANCELLED"} + _INTERMEDIATE_EXPORT_CACHE = None + self.report({"INFO"}, "Magic Node string copied to clipboard") + else: + # seems impossible to use bl_idname here + bpy.ops.scene.tree_clipper_export_cache("INVOKE_DEFAULT") # ty: ignore[unresolved-attribute] return {"FINISHED"} @@ -191,19 +260,20 @@ def execute( if not external_item.skip ) if clipboard: - string = _INTERMEDIATE_EXPORT_CACHE.export_to_str( - compress=compress, - json_indent=self.json_indent, - ) - - # https://github.com/Algebraic-UG/tree_clipper/issues/134 - utf8 = string.encode("utf-8") - if len(utf8) > get_max_clipboard_bytes(): - raise RuntimeError( - f"The export exceeds the clipboard limit ({get_max_clipboard_bytes()}) set in the addon preferences." + if compress: + _copy_export_to_clipboard(context, _INTERMEDIATE_EXPORT_CACHE) + else: + string = _INTERMEDIATE_EXPORT_CACHE.export_to_str( + compress=False, + json_indent=self.json_indent, ) - - bpy.context.window_manager.clipboard = utf8 # ty:ignore[invalid-assignment] + if len(string.encode("utf-8")) > get_max_clipboard_bytes(): + raise RuntimeError( + f"The export exceeds the clipboard limit ({get_max_clipboard_bytes()}) set in the addon preferences." + ) + window_manager = context.window_manager + assert window_manager is not None + window_manager.clipboard = string else: _INTERMEDIATE_EXPORT_CACHE.export_to_file( file_path=Path(self.output_file),