From 566120d07396f19589e06710ee0a405eef2cc93f Mon Sep 17 00:00:00 2001 From: "kaosat.dev" Date: Wed, 14 Aug 2024 01:51:17 +0200 Subject: [PATCH] feat(Blender): a few QOL improvements to the workflow * context menu + shortcut + operators to quickly edit /and/or create new blueprints in a seperate scene * boilerplate based on the awesome work by slyedoc * "one keyboard shortcut" workflow: edit if blueprint instance is selected, create if not, stop editing if editing is already in progress * updated list of contributors --- README.md | 2 + tools/blenvy/__init__.py | 54 +++-- tools/blenvy/add_ons/bevy_components/utils.py | 12 +- tools/blenvy/core/blenvy_manager.py | 22 ++ tools/blenvy/core/helpers_collections.py | 5 +- tools/blenvy/core/ui/menus_and_shortcuts.py | 225 ++++++++++++++++++ 6 files changed, 295 insertions(+), 25 deletions(-) create mode 100644 tools/blenvy/core/ui/menus_and_shortcuts.py diff --git a/README.md b/README.md index 0f69f58..7f37b88 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ Thanks to all the contributors helping out with this project ! Big kudos to you, * [killercup](https://github.com/killercup) * [janhohenheim](https://github.com/janhohenheim) * [BUGO07](https://github.com/BUGO07) +* [ChristopherBiscardi](https://github.com/ChristopherBiscardi) +* [slyedoc](https://github.com/slyedoc) ## License diff --git a/tools/blenvy/__init__.py b/tools/blenvy/__init__.py index a3841fd..63449d8 100644 --- a/tools/blenvy/__init__.py +++ b/tools/blenvy/__init__.py @@ -52,6 +52,7 @@ from .core.blenvy_manager import BlenvyManager from .core.operators import BLENVY_OT_configuration_switch, BLENVY_OT_tooling_switch from .core.ui.ui import (BLENVY_PT_SidePanel) from .core.ui.scenes_list import BLENVY_OT_scenes_list_actions +from .core.ui.menus_and_shortcuts import BLENVY_OT_ui_blueprint_create, BLENVY_OT_ui_blueprint_edit_start, BLENVY_OT_ui_blueprint_edit_end, BLENVY_OT_ui_blueprint_create_or_edit, edit_or_create_blueprint_menu from .assets.assets_folder_browser import BLENVY_OT_assets_paths_browse @@ -126,6 +127,11 @@ classes = [ BlueprintsRegistry, BLENVY_OT_blueprint_select, BLENVY_PT_blueprints_panel, + + BLENVY_OT_ui_blueprint_create, + BLENVY_OT_ui_blueprint_edit_start, + BLENVY_OT_ui_blueprint_edit_end, + BLENVY_OT_ui_blueprint_create_or_edit ] @@ -139,40 +145,37 @@ def post_save(scene, depsgraph): @persistent def post_load(file_name): - print("POST LOAD") blenvy = bpy.context.window_manager.blenvy if blenvy is not None: blenvy.load_settings() +def init_keymaps(): + window_manager = bpy.context.window_manager + if window_manager.keyconfigs.addon: + km = window_manager.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = [ + km.keymap_items.new(BLENVY_OT_ui_blueprint_create_or_edit.bl_idname, "F", "PRESS", shift=True), + ] + return km, kmi + +addon_keymaps = [] + def register(): for cls in classes: bpy.utils.register_class(cls) - bpy.app.handlers.load_post.append(post_load) # for some reason, adding these directly to the tracker class in register() do not work reliably bpy.app.handlers.depsgraph_update_post.append(post_update) bpy.app.handlers.save_post.append(post_save) + bpy.types.VIEW3D_MT_object.append(edit_or_create_blueprint_menu) + bpy.types.VIEW3D_MT_object_context_menu.append(edit_or_create_blueprint_menu) - """ handle = object() - - subscribe_to = bpy.types.Scene, "name" # - - def notify_test(context): - #if (context.scene.type == 'MESH'): - print("Renamed", dir(context), context.scenes) - - bpy.msgbus.subscribe_rna( - key=subscribe_to, - owner=bpy, - args=(bpy.context,), - notify=notify_test, - )""" - - - #bpy.msgbus.publish_rna(key=subscribe_to) - - + if not bpy.app.background: + km, kmi = init_keymaps() + for k in kmi: + k.active = True + addon_keymaps.append((km, k)) def unregister(): @@ -181,3 +184,12 @@ def unregister(): bpy.app.handlers.load_post.remove(post_load) bpy.app.handlers.depsgraph_update_post.remove(post_update) bpy.app.handlers.save_post.remove(post_save) + + + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + addon_keymaps.clear() + + + bpy.types.VIEW3D_MT_object.remove(edit_or_create_blueprint_menu) + bpy.types.VIEW3D_MT_object_context_menu.remove(edit_or_create_blueprint_menu) diff --git a/tools/blenvy/add_ons/bevy_components/utils.py b/tools/blenvy/add_ons/bevy_components/utils.py index d3e626a..ff2be3d 100644 --- a/tools/blenvy/add_ons/bevy_components/utils.py +++ b/tools/blenvy/add_ons/bevy_components/utils.py @@ -34,7 +34,6 @@ def get_object_scene(object): return scenes_of_object[0] return None - def get_mesh_object(mesh): for object in bpy.data.objects: if isinstance(object.data, bpy.types.Mesh) and mesh.name == object.data.name: @@ -69,6 +68,12 @@ class BLENVY_OT_item_select(Operator): description="target to select's name ", ) # type: ignore + override_scene_name: StringProperty( + name="override scene name", + description="use this to override the scene selection mecanism", + default="" + ) # type: ignore + @classmethod def register(cls): @@ -96,8 +101,9 @@ class BLENVY_OT_item_select(Operator): select_area(context=context, area_name="OBJECT") elif self.item_type == 'COLLECTION': + print("selecting collection") collection = bpy.data.collections[self.target_name] - scene_of_collection = get_collection_scene(collection) + scene_of_collection = get_collection_scene(collection) if self.override_scene_name == "" else bpy.data.scenes.get(self.override_scene_name, None) if scene_of_collection is not None: bpy.ops.object.select_all(action='DESELECT') bpy.context.window.scene = scene_of_collection @@ -105,7 +111,7 @@ class BLENVY_OT_item_select(Operator): context.window_manager.blenvy_item_selected_ids = json.dumps({"name": collection.name, "type": self.item_type}) set_active_collection(bpy.context.window.scene, collection.name) - select_area(context=context, area_name="COLLECTION") + #select_area(context=context, area_name="COLLECTION") elif self.item_type == 'MESH': mesh = bpy.data.meshes[self.target_name] diff --git a/tools/blenvy/core/blenvy_manager.py b/tools/blenvy/core/blenvy_manager.py index 2fbb6b1..62db0bf 100644 --- a/tools/blenvy/core/blenvy_manager.py +++ b/tools/blenvy/core/blenvy_manager.py @@ -137,6 +137,28 @@ class BlenvyManager(PropertyGroup): get=lambda self: os.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), self.project_root_path, self.assets_path, self.animations_path)) ) # type: ignore + # Edit blueprint settings + edit_blueprint_previous_scene: StringProperty( + name="edit_blueprint_previous_scene", + description="name of the scene we started from for editing the current Blueprint", + default="", + update=save_settings + )# type: ignore + + edit_blueprint_current_scene: StringProperty( + name="edit_blueprint_current_scene", + description="name of the scene where we are currently editing a Blueprint in", + default="", + update=save_settings + )# type: ignore + + edit_blueprint_previous_mode: StringProperty( + name="edit_blueprint_previous_mode", + description="previous blenvy mode before starting editing the current blueprint", + default="", + update=save_settings + )# type: ignore + # sub ones auto_export: PointerProperty(type=AutoExportSettings) # type: ignore components: PointerProperty(type=ComponentsSettings) # type: ignore diff --git a/tools/blenvy/core/helpers_collections.py b/tools/blenvy/core/helpers_collections.py index ad5c3d6..ef4d7e5 100644 --- a/tools/blenvy/core/helpers_collections.py +++ b/tools/blenvy/core/helpers_collections.py @@ -17,7 +17,10 @@ def recurLayerCollection(layerColl, collName): return found def set_active_collection(scene, collection_name): - layer_collection = bpy.data.scenes[scene.name].view_layers['ViewLayer'].layer_collection + print("set active collection", scene, collection_name) + layer_collection = scene.view_layers['ViewLayer'].layer_collection layerColl = recurLayerCollection(layer_collection, collection_name) + + print("layerColl", layerColl) # set active collection to the collection bpy.context.view_layer.active_layer_collection = layerColl diff --git a/tools/blenvy/core/ui/menus_and_shortcuts.py b/tools/blenvy/core/ui/menus_and_shortcuts.py new file mode 100644 index 0000000..8d7413c --- /dev/null +++ b/tools/blenvy/core/ui/menus_and_shortcuts.py @@ -0,0 +1,225 @@ +import bpy +import json +from mathutils import Vector + +from ..helpers_collections import set_active_collection +from ..blenvy_manager import BlenvyManager + +""" This file contains quality of life operators/menus/shortcuts to make working with blueprints more pleasant +* based on the excellent work by slyedoc: https://github.com/slyedoc/bevy_sly_blender/tree/4223cc0ff86255f82bb555ffc8eddf65e91aa636 + +- [ ] detect editing in progress +- [x] select collection instead of objects +- [ ] if current scene (before edit) + - is library: do not create instance + - is main scene: create instance +- or alternative: sub menu to choose instance creation or not +- [x] save & restore blenvy mode +- [x] add a contextual shortcut to easilly jump in/out of editing mode +- [ ] save & reset camera +""" + +def edit_or_create_blueprint_menu(self, context): + if bpy.context.active_object and bpy.context.active_object.instance_collection: + self.layout.operator(BLENVY_OT_ui_blueprint_edit_start.bl_idname) + else: + blenvy = context.window_manager.blenvy # type: BlenvyManager + prev_scene = bpy.data.scenes.get(blenvy.edit_blueprint_previous_scene) + if prev_scene is not None: + self.layout.operator(BLENVY_OT_ui_blueprint_edit_end.bl_idname) + else: + self.layout.operator(BLENVY_OT_ui_blueprint_create.bl_idname) + + +class BLENVY_OT_ui_blueprint_create_or_edit(bpy.types.Operator): + """Create Blueprint in a new Scene""" + bl_idname = "window_manager.blenvy_blueprint_shortcut" + bl_label = "Edit Blueprint" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + blenvy = context.window_manager.blenvy # type: BlenvyManager + if bpy.context.active_object and bpy.context.active_object.instance_collection: + bpy.ops.window_manager.blenvy_edit_blueprinrt() + else: + blenvy = context.window_manager.blenvy # type: BlenvyManager + prev_scene = bpy.data.scenes.get(blenvy.edit_blueprint_previous_scene) + if prev_scene is not None: + bpy.ops.window_manager.blenvy_exit_edit_blueprint() + else: + bpy.ops.window_manager.blenvy_create_blueprint() + + return {"FINISHED"} + +class BLENVY_OT_ui_blueprint_create(bpy.types.Operator): + """Create Blueprint in a new Scene""" + bl_idname = "window_manager.blenvy_create_blueprint" + bl_label = "Create Blueprint" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + blenvy = context.window_manager.blenvy # type: BlenvyManager + + # Store original Blenvy setting + blenvy.edit_blueprint_previous_scene = bpy.context.scene.name + blenvy.edit_blueprint_previous_mode = blenvy.mode + + # set mode to components + blenvy.mode = "COMPONENTS" + + blueprint_name = "new_blueprint" + collection = bpy.data.collections.new(blueprint_name) + + library_scene_name = "Library" + if len(blenvy.library_scenes_names) > 0: + library_scene_name = blenvy.library_scenes_names[0] + else: + bpy.data.scenes.new(library_scene_name) + # automatically add it to the library : find library scene, if any, if not, create it + bpy.data.scenes[library_scene_name].collection.children.link(collection) + + + # create an instance of the + source_collection = collection + instance_obj = bpy.data.objects.new( + name=collection.name, + object_data=None + ) + instance_obj.instance_collection = source_collection + instance_obj.instance_type = 'COLLECTION' + parent_collection = bpy.context.view_layer.active_layer_collection + parent_collection.collection.objects.link(instance_obj) + + # now open the temporary scene + scene_name = f"temp:{blueprint_name}" + bpy.ops.scene.new(type="EMPTY") + new_scene = bpy.context.scene + new_scene.name = scene_name + bpy.context.window.scene = new_scene + + + new_scene.collection.children.link(collection) + + # deselect all objects then select the first object in new scene + bpy.ops.object.select_all(action='DESELECT') + + # zoom to selected + bpy.ops.view3d.view_selected() + # now that the 3d view has been adapted, we select what we actually need: the collection/blueprint + bpy.ops.blenvy.select_item(target_name=collection.name, item_type="COLLECTION", override_scene_name=scene_name) + + return {"FINISHED"} + +class BLENVY_OT_ui_blueprint_edit_start(bpy.types.Operator): + """Edit the Blueprint referenced by this Blueprint Instance in a new Scene""" + bl_idname = "window_manager.blenvy_edit_blueprinrt" + bl_label = "Start Editing Blueprint" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + blenvy = context.window_manager.blenvy # type: BlenvyManager + collection = bpy.context.active_object.instance_collection + + # Store original Blenvy setting + blenvy.edit_blueprint_previous_scene = bpy.context.scene.name + blenvy.edit_blueprint_previous_mode = blenvy.mode + + # set mode to components + blenvy.mode = "COMPONENTS" + + if not collection: + print("Active item is not a Blueprint/Collection instance") + self.report({"WARNING"}, "Active item is not a Blueprint/Collection instance") + return {"CANCELLED"} + + scene_name = f"temp:{collection.name}" + bpy.ops.scene.new(type="EMPTY") + new_scene = bpy.context.scene + new_scene.name = scene_name + bpy.context.window.scene = new_scene + new_scene.collection.children.link(collection) + + # Assuming you want to focus on the objects from the linked collection + # Switch to the new scene context + + """blenvy.edit_collection_world_texture = "checker" + if blenvy.edit_collection_world_texture != "none": + world = bpy.data.worlds.new(bpy.context.scene.name) + new_scene.world = world + world.use_nodes = True + tree = world.node_tree + + if blenvy.edit_collection_world_texture in ["checker", "checker_view"]: + checker_texture = tree.nodes.new("ShaderNodeTexChecker") + checker_texture.inputs["Scale"].default_value = 20 + checker_texture.location = Vector((-250, 0)) + if blenvy.edit_collection_world_texture == "checker_view": + coord = tree.nodes.new("ShaderNodeTexCoord") + coord.location = Vector((-500, 0)) + for op in coord.outputs: + op.hide = True + tree.links.new(coord.outputs["Window"], checker_texture.inputs["Vector"]) + tree.links.new(checker_texture.outputs["Color"], tree.nodes["Background"].inputs["Color"]) + elif blenvy.edit_collection_world_texture == "gray": + tree.nodes["Background"].inputs["Color"].default_value = (.3, .3, .3, 1)""" + + # deselect all objects then select the first object in new scene + bpy.ops.object.select_all(action='DESELECT') + + # find the root object + if len(collection.objects) > 0 : + root_obj = collection.objects[0] + while root_obj.parent: + root_obj = root_obj.parent + + # select object and children + new_scene.objects[root_obj.name].select_set(True) + # def select_children(parent): + # for child in parent.children: + # child.select_set(True) + # select_children(child) # Recursively select further descendants + # select_children(root_obj); + + # Select the view layer and view the selected objects + bpy.context.view_layer.objects.active = new_scene.objects[root_obj.name] + bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection.children[collection.name] + + # zoom to selected + bpy.ops.view3d.view_selected() + + # now that the 3d view has been adapted, we select what we actually need: the collection/blueprint + bpy.ops.blenvy.select_item(target_name=collection.name, item_type="COLLECTION", override_scene_name=scene_name) + + return {"FINISHED"} + +class BLENVY_OT_ui_blueprint_edit_end(bpy.types.Operator): + bl_idname = "window_manager.blenvy_exit_edit_blueprint" + bl_label = "Done editing blueprint" + bl_options = {"UNDO"} + + def execute(self, context): + blenvy = context.window_manager.blenvy # type: BlenvyManager + # = context.window_manager.bevy # type: BlenvyManager + + current_scene = bpy.context.scene + prev_scene = bpy.data.scenes.get(blenvy.edit_blueprint_previous_scene) + + # we are done editing the blueprint, reset settings to the way they were before + blenvy.edit_blueprint_previous_scene = "" + blenvy.mode = blenvy.edit_blueprint_previous_mode + + + if prev_scene is None: + print("No scene to return to") + return {'CANCELLED'} + + if current_scene.name.startswith("temp:"): + bpy.data.scenes.remove(bpy.context.scene) + bpy.context.window.scene = prev_scene + else: + #if + print("Not in temp scene") + return {'CANCELLED'} + + return {'FINISHED'} +