diff --git a/__init__.py b/__init__.py index cd10fef..4818757 100644 --- a/__init__.py +++ b/__init__.py @@ -16,24 +16,24 @@ # # END GPL LICENSE BLOCK ##### - bl_info = { - "name": "Khanat Tools", - "author": "Yann Kervran", - "version": (1, 0, 0), - "blender": (3, 4, 0), - "location": "View3D > UI > Tools", - "description": "Toolset for Khanat project", - "doc_url": "https://git.numenaute.org/yannk/khanat-tools", - "category": "Generic" + "name": "Khanat tools", + "author": "Yann Kervran", + "version": (1, 0, 0), + "blender": (3, 4, 0), + "location": "View3D > UI > N Panel", + "description": "Toolset for Khanat project", + "doc_url": "https://git.numenaute.org/yannk/khanat-tools", + "tracker_url": "https://git.numenaute.org/yannk/khanat-tools/-/issues", + "category": "Generic" } def register(): - from addon.register import register_addon + from .addon.register import register_addon register_addon() def unregister(): - from addon.register import unregister_addon + from .addon.register import unregister_addon unregister_addon() diff --git a/addon/common/__init__.py b/addon/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/addon/common/addon.py b/addon/common/addon.py new file mode 100644 index 0000000..e8f3543 --- /dev/null +++ b/addon/common/addon.py @@ -0,0 +1,11 @@ +import bpy + +""" +Parameters & functions to get information from addon +""" + +addon_name = __name__.partition('.')[0] + + +def get_prefs(): + return bpy.context.preferences.addons[addon_name].preferences diff --git a/addon/common/icons.py b/addon/common/icons.py new file mode 100644 index 0000000..67a9154 --- /dev/null +++ b/addon/common/icons.py @@ -0,0 +1,16 @@ +import bpy +import os + + +icons_collection = None +icons_directory = os.path.dirname(__file__) + + +def get_icon_id(identifier): + return get_icon(identifier).icon_id + + +def get_icon(identifier): + if identifier in icons_collection: + return icons_collection[identifier] + return icons_collection.load(identifier, os.path.join(icons_directory, identifier + ".png"), "IMAGE") diff --git a/addon/common/validate_name.py b/addon/common/validate_name.py new file mode 100644 index 0000000..7c83b4a --- /dev/null +++ b/addon/common/validate_name.py @@ -0,0 +1,11 @@ +import re + +def validate_name(name): + """Check if the name is properly formatted + Must be in kebab-case, with possible underscore for prefixes/sufixes + and only alphanumerical + A-z0-9 doesn’t seem to work in regexp, so had to write them full extent + """ + + allowed = re.match(r'^([ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]+)_?([ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-]+)_?([ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]+)$', name) + return allowed is not None \ No newline at end of file diff --git a/addon/icons/__init__.py b/addon/icons/__init__.py index 981d0be..beb7865 100644 --- a/addon/icons/__init__.py +++ b/addon/icons/__init__.py @@ -1,29 +1,21 @@ import bpy import os -icons_collection = None -icons_directory = os.path.dirname(__file__) +from ..common import icons + def initialize_icons_collection(): - import bpy.utils.previews - global icons_collection - print("icons_collection : {}".format(icons_collection)) - icons_collection = bpy.utils.previews.new() + import bpy.utils.previews + icons.icons_collection = bpy.utils.previews.new() + def unload_icons(): - bpy.utils.previews.remove(icons_collection) + bpy.utils.previews.remove(icons.icons_collection) -def get_icon_id(identifier): - # The initialize_icons_collection function needs to be called first. - return get_icon(identifier).icon_id - -def get_icon(identifier): - if identifier in icons_collection: - return icons_collection[identifier] - return icons_collection.load(identifier, os.path.join(icons_directory, identifier + ".png"), "IMAGE") def register_icons(): - initialize_icons_collection() + initialize_icons_collection() + def unregister_icons(): - unload_icons() \ No newline at end of file + unload_icons() diff --git a/addon/menus/__init__.py b/addon/menus/__init__.py index 8d21040..46a4b13 100644 --- a/addon/menus/__init__.py +++ b/addon/menus/__init__.py @@ -1,18 +1,15 @@ -import bpy - from .panel_main import KH_PT_panel_main classes = ( - KH_PT_panel_main, + KH_PT_panel_main, ) def register_menus(): - from bpy.utils import register_class - for cls in classes: - register_class(cls) - + from bpy.utils import register_class + for cls in classes: + register_class(cls) def unregister_menus(): - from bpy.utils import unregister_class - for cls in classes: - unregister_class(cls) \ No newline at end of file + from bpy.utils import unregister_class + for cls in classes: + unregister_class(cls) \ No newline at end of file diff --git a/addon/menus/panel_main.py b/addon/menus/panel_main.py index 887ec0d..0f6f2dc 100644 --- a/addon/menus/panel_main.py +++ b/addon/menus/panel_main.py @@ -1,26 +1,31 @@ import bpy + from ..operators import readthedocs, export2godot -from ..icons import get_icon_id +from ..common import icons + class KH_PT_panel_main(bpy.types.Panel): - bl_label = 'Khanat tools' - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_context = '' - bl_category = 'Khanat' + """ + Main panel in 3D View + """ + bl_label = 'Khanat tools' + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_context = '' + bl_category = 'Khanat' - def draw_header(self, context): - layout = self.layout - layout.label(icon_value=get_icon_id("khanat")) - - def draw(self, context): - layout = self.layout + def draw_header(self, context): + layout = self.layout + layout.label(icon_value=icons.get_icon_id("khanat")) - row = layout.row() - row.operator(export2godot.KH_OP_export2godot.bl_idname) - layout.separator() + def draw(self, context): + layout = self.layout - row = layout.row() - row.operator(readthedocs.KH_OP_readthedocs.bl_idname, icon_value=72) - layout.separator() \ No newline at end of file + row = layout.row() + row.operator(export2godot.KH_OT_export2godot.bl_idname) + layout.separator() + + row = layout.row() + row.operator(readthedocs.KH_OT_readthedocs.bl_idname, icon_value=72) + layout.separator() diff --git a/addon/operators/__init__.py b/addon/operators/__init__.py index ea22e08..a14a7ed 100644 --- a/addon/operators/__init__.py +++ b/addon/operators/__init__.py @@ -1,20 +1,18 @@ -import bpy - -from .readthedocs import KH_OP_readthedocs -from .export2godot import KH_OP_export2godot +from .readthedocs import KH_OT_readthedocs +from .export2godot import KH_OT_export2godot classes = ( - KH_OP_readthedocs, - KH_OP_export2godot + KH_OT_readthedocs, + KH_OT_export2godot ) + def register_operators(): - print(classes) - from bpy.utils import register_class - for cls in classes: - register_class(cls) + from bpy.utils import register_class + for cls in classes: + register_class(cls) def unregister_operators(): - from bpy.utils import unregister_class - for cls in classes: - unregister_class(cls) \ No newline at end of file + from bpy.utils import unregister_class + for cls in classes: + unregister_class(cls) \ No newline at end of file diff --git a/addon/operators/export2godot.py b/addon/operators/export2godot.py index 57f9d0a..808c39b 100644 --- a/addon/operators/export2godot.py +++ b/addon/operators/export2godot.py @@ -1,14 +1,113 @@ import bpy +import os +import re -class KH_OP_export2godot(bpy.types.Operator): - """Export collections to Godot throught glTF""" +from ..common import addon +from ..common import validate_name + +class KH_OT_export2godot(bpy.types.Operator): + """Export whole collections to Godot throught glTF format""" bl_idname = "kh.export2godot" - bl_label = "Export to gltf" + bl_label = "Export to Godot project" bl_options = {'REGISTER', 'UNDO'} - + def invoke(self, context, event): return self.execute(context) - + def execute(self, context): - print("TOTOR IS NOT HERE") - return {"FINISHED"} + prefs = addon.get_prefs() + print("Root collection : {}".format(prefs.root_collection)) + print("Godot path : {}".format(prefs.godot_project_path)) + print("Blender repository path : {}".format( + prefs.blender_repository_path)) + + def save_file_beforehand(): + self.report( + {"WARNING"}, "File must be saved first - skipping export") + bpy.ops.wm.save_mainfile('INVOKE_AREA') + return {"CANCELLED"} + + def create_destination_folder(): + """ + Define the destination folder and creates it if nonexistent. + Return the path name + """ + + final_path = bpy.data.filepath.replace( + prefs.blender_repository_path, prefs.godot_project_path) + gltf_path = os.path.splitext(final_path)[0] + if not os.path.isdir(gltf_path): + os.makedirs(gltf_path) + return gltf_path + + def recurLayerCollection(layerColl, collName): + """ Activate the selected collection for export""" + found = None + if (layerColl.name == collName): + return layerColl + for layer in layerColl.children: + found = recurLayerCollection(layer, collName) + if found: + return found + + def export_content(scene_collection): + + def check_name(collection): + is_valid = validate_name.validate_name(collection.name) + if not is_valid: + self.report({"WARNING"}, + "Name {} is not valid - skipping export".format(collection.name)) + return {"CANCELLED"} + + def export_to_file(tscn_collection): + print("Exporting {}".format(tscn_collection.name)) + + filename = tscn_collection.name + # Export collection - parameters : https://docs.blender.org/api/current/bpy.ops.export_scene.html?highlight=export_scene#bpy.ops.export_scene.gltf + print("Exporting to : {}/{}".format(gltf_path, filename)) + print("Exporting textures to : {}_{}_textures".format((os.path.splitext(os.path.basename(gltf_path)))[0], filename)) + bpy.ops.export_scene.gltf( + filepath="{}/{}".format(gltf_path, filename), + export_format="GLTF_SEPARATE", # Export glTF Separate (.gltf + .bin + textures), Exports multiple files, with separate JSON, binary and texture data + export_texture_dir="{}_{}_textures".format((os.path.splitext(os.path.basename(gltf_path)))[0], filename), # Textures folder + export_copyright=prefs.licence, + use_active_collection = True, + use_renderable = True, + export_cameras=False, + export_lights=False, + export_apply=True # Applique les modifiers + ) + + tscn_collections = [ coll for coll in bpy.data.collections if scene_collection.user_of_id(coll)] + + for tscn in tscn_collections: + print("--------------------") + check_name(tscn) + # Activate proper collection for export + layer_collection = bpy.context.view_layer.layer_collection + layerColl = recurLayerCollection(layer_collection, tscn.name) + if layerColl.exclude: + print("{} is not activated - not exported".format(tscn.name)) + else: + print("Set active collection to {}".format(tscn.name)) + bpy.context.view_layer.active_layer_collection = layerColl + export_to_file(tscn) + + + + + if not bpy.data.is_saved: + save_file_beforehand() + else: + try: + scn_col = bpy.data.collections[prefs.root_collection] + except KeyError: + self.report({"WARNING"}, + "No \"{}\" root collection in the file - skipping export".format(prefs.root_collection)) + return {"CANCELLED"} + + # Create the proper destination path + gltf_path = create_destination_folder() + + export_content(scn_col) + return {"FINISHED"} diff --git a/addon/operators/readthedocs.py b/addon/operators/readthedocs.py index 5e2b94f..c1fa0e0 100644 --- a/addon/operators/readthedocs.py +++ b/addon/operators/readthedocs.py @@ -1,20 +1,17 @@ import bpy +class KH_OT_readthedocs(bpy.types.Operator): + """Check online documentation for development""" -class KH_OP_readthedocs(bpy.types.Operator): - """Check online documentation""" - - bl_idname = "kh.readthedocs" - bl_label = "Online documentation" - bl_description = "Go to Khanat Development Guide" - bl_options = {"REGISTER", "UNDO"} - - def invoke(self, context, event): - return self.execute(context) - - - def execute(self, context): - bpy.ops.wm.url_open('INVOKE_DEFAULT', url='https://git.numenaute.org/khaganat/mmorpg_khanat/khanat_gamedev_guide') - return {"FINISHED"} - + bl_idname="kh.readthedocs" + bl_label="Online documentation" + bl_description="Go to Khanat Development Guide" + bl_options= {"REGISTER", "UNDO"} + def invoke (self, context, event): + return self.execute(context) + + def execute(self, context): + bpy.ops.wm.url_open("INVOKE_DEFAULT", url="https://git.numenaute.org/khaganat/mmorpg_khanat/khanat_gamedev_guide") + return {"FINISHED"} + \ No newline at end of file diff --git a/addon/preferences/__init__.py b/addon/preferences/__init__.py new file mode 100644 index 0000000..db56e58 --- /dev/null +++ b/addon/preferences/__init__.py @@ -0,0 +1,18 @@ +import bpy + +from .preferences import KH_Prefs + +classes = ( + KH_Prefs, +) + +def register_preferences(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + + +def unregister_preferences(): + from bpy.utils import unregister_class + for cls in classes: + unregister_class(cls) \ No newline at end of file diff --git a/addon/preferences/preferences.py b/addon/preferences/preferences.py new file mode 100644 index 0000000..42bec10 --- /dev/null +++ b/addon/preferences/preferences.py @@ -0,0 +1,50 @@ +import bpy +from bpy.props import StringProperty, IntProperty, BoolProperty + + +from ..common import addon + + +class KH_Prefs(bpy.types.AddonPreferences): + bl_idname = addon.addon_name + + # TODO EnumProperty to get list of projects to choose from + + godot_project_path: StringProperty( + name="Godot project path", + subtype='DIR_PATH', + default="//godot_project/" + ) + + blender_repository_path: StringProperty( + name="Blender repository root", + subtype='DIR_PATH', + default="//" + ) + + root_collection: StringProperty( + name="Root collection", + default="khanat", + ) + + licence: StringProperty( + name="Licence", + default="CC BY SA Khaganat", + ) + + contributor: StringProperty( + name="Contributor", + default="", + ) + + def draw(self, context): + layout = self.layout + box = layout.box() + box.prop(self, "godot_project_path") + box.prop(self, "blender_repository_path") + box.prop(self, "root_collection") + layout.split() + box = layout.box() + box.prop(self, "licence") + box.prop(self, "contributor") + diff --git a/addon/register/__init__.py b/addon/register/__init__.py index ba82caf..d979589 100644 --- a/addon/register/__init__.py +++ b/addon/register/__init__.py @@ -1,30 +1,31 @@ + + def register_addon(): - # Icons - from ..icons import register_icons - register_icons() + from ..preferences import register_preferences + register_preferences() - # Operators from ..operators import register_operators register_operators() - # Menus + from ..icons import register_icons + register_icons() + from ..menus import register_menus register_menus() - def unregister_addon(): - # Menus from ..menus import unregister_menus unregister_menus() - # Operators + from ..icons import unregister_icons + unregister_icons() + from ..operators import unregister_operators unregister_operators() - # Icons - from ..icons import unregister_icons - unregister_icons() \ No newline at end of file + from ..preferences import unregister_preferences + unregister_preferences() \ No newline at end of file