feat: Mass export to several files
This commit is contained in:
parent
314a219186
commit
bd95d68ae4
14 changed files with 298 additions and 103 deletions
22
__init__.py
22
__init__.py
|
@ -16,24 +16,24 @@
|
||||||
#
|
#
|
||||||
# END GPL LICENSE BLOCK #####
|
# END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Khanat Tools",
|
"name": "Khanat tools",
|
||||||
"author": "Yann Kervran",
|
"author": "Yann Kervran",
|
||||||
"version": (1, 0, 0),
|
"version": (1, 0, 0),
|
||||||
"blender": (3, 4, 0),
|
"blender": (3, 4, 0),
|
||||||
"location": "View3D > UI > Tools",
|
"location": "View3D > UI > N Panel",
|
||||||
"description": "Toolset for Khanat project",
|
"description": "Toolset for Khanat project",
|
||||||
"doc_url": "https://git.numenaute.org/yannk/khanat-tools",
|
"doc_url": "https://git.numenaute.org/yannk/khanat-tools",
|
||||||
"category": "Generic"
|
"tracker_url": "https://git.numenaute.org/yannk/khanat-tools/-/issues",
|
||||||
|
"category": "Generic"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
from addon.register import register_addon
|
from .addon.register import register_addon
|
||||||
register_addon()
|
register_addon()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
from addon.register import unregister_addon
|
from .addon.register import unregister_addon
|
||||||
unregister_addon()
|
unregister_addon()
|
||||||
|
|
0
addon/common/__init__.py
Normal file
0
addon/common/__init__.py
Normal file
11
addon/common/addon.py
Normal file
11
addon/common/addon.py
Normal file
|
@ -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
|
16
addon/common/icons.py
Normal file
16
addon/common/icons.py
Normal file
|
@ -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")
|
11
addon/common/validate_name.py
Normal file
11
addon/common/validate_name.py
Normal file
|
@ -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
|
|
@ -1,29 +1,21 @@
|
||||||
import bpy
|
import bpy
|
||||||
import os
|
import os
|
||||||
|
|
||||||
icons_collection = None
|
from ..common import icons
|
||||||
icons_directory = os.path.dirname(__file__)
|
|
||||||
|
|
||||||
def initialize_icons_collection():
|
def initialize_icons_collection():
|
||||||
import bpy.utils.previews
|
import bpy.utils.previews
|
||||||
global icons_collection
|
icons.icons_collection = bpy.utils.previews.new()
|
||||||
print("icons_collection : {}".format(icons_collection))
|
|
||||||
icons_collection = bpy.utils.previews.new()
|
|
||||||
|
|
||||||
def unload_icons():
|
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():
|
def register_icons():
|
||||||
initialize_icons_collection()
|
initialize_icons_collection()
|
||||||
|
|
||||||
|
|
||||||
def unregister_icons():
|
def unregister_icons():
|
||||||
unload_icons()
|
unload_icons()
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import bpy
|
|
||||||
|
|
||||||
from .panel_main import KH_PT_panel_main
|
from .panel_main import KH_PT_panel_main
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
KH_PT_panel_main,
|
KH_PT_panel_main,
|
||||||
)
|
)
|
||||||
|
|
||||||
def register_menus():
|
def register_menus():
|
||||||
from bpy.utils import register_class
|
from bpy.utils import register_class
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
register_class(cls)
|
register_class(cls)
|
||||||
|
|
||||||
|
|
||||||
def unregister_menus():
|
def unregister_menus():
|
||||||
from bpy.utils import unregister_class
|
from bpy.utils import unregister_class
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
unregister_class(cls)
|
unregister_class(cls)
|
|
@ -1,26 +1,31 @@
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from ..operators import readthedocs, export2godot
|
from ..operators import readthedocs, export2godot
|
||||||
from ..icons import get_icon_id
|
from ..common import icons
|
||||||
|
|
||||||
|
|
||||||
class KH_PT_panel_main(bpy.types.Panel):
|
class KH_PT_panel_main(bpy.types.Panel):
|
||||||
bl_label = 'Khanat tools'
|
"""
|
||||||
bl_space_type = 'VIEW_3D'
|
Main panel in 3D View
|
||||||
bl_region_type = 'UI'
|
"""
|
||||||
bl_context = ''
|
|
||||||
bl_category = 'Khanat'
|
|
||||||
|
|
||||||
|
bl_label = 'Khanat tools'
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_context = ''
|
||||||
|
bl_category = 'Khanat'
|
||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.label(icon_value=get_icon_id("khanat"))
|
layout.label(icon_value=icons.get_icon_id("khanat"))
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
row = layout.row()
|
def draw(self, context):
|
||||||
row.operator(export2godot.KH_OP_export2godot.bl_idname)
|
layout = self.layout
|
||||||
layout.separator()
|
|
||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.operator(readthedocs.KH_OP_readthedocs.bl_idname, icon_value=72)
|
row.operator(export2godot.KH_OT_export2godot.bl_idname)
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.operator(readthedocs.KH_OT_readthedocs.bl_idname, icon_value=72)
|
||||||
|
layout.separator()
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
import bpy
|
from .readthedocs import KH_OT_readthedocs
|
||||||
|
from .export2godot import KH_OT_export2godot
|
||||||
from .readthedocs import KH_OP_readthedocs
|
|
||||||
from .export2godot import KH_OP_export2godot
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
KH_OP_readthedocs,
|
KH_OT_readthedocs,
|
||||||
KH_OP_export2godot
|
KH_OT_export2godot
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_operators():
|
def register_operators():
|
||||||
print(classes)
|
from bpy.utils import register_class
|
||||||
from bpy.utils import register_class
|
for cls in classes:
|
||||||
for cls in classes:
|
register_class(cls)
|
||||||
register_class(cls)
|
|
||||||
|
|
||||||
def unregister_operators():
|
def unregister_operators():
|
||||||
from bpy.utils import unregister_class
|
from bpy.utils import unregister_class
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
unregister_class(cls)
|
unregister_class(cls)
|
|
@ -1,14 +1,113 @@
|
||||||
import bpy
|
import bpy
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
class KH_OP_export2godot(bpy.types.Operator):
|
from ..common import addon
|
||||||
"""Export collections to Godot throught glTF"""
|
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_idname = "kh.export2godot"
|
||||||
bl_label = "Export to gltf"
|
bl_label = "Export to Godot project"
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
return self.execute(context)
|
return self.execute(context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
print("TOTOR IS NOT HERE")
|
prefs = addon.get_prefs()
|
||||||
return {"FINISHED"}
|
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"}
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
class KH_OT_readthedocs(bpy.types.Operator):
|
||||||
|
"""Check online documentation for development"""
|
||||||
|
|
||||||
class KH_OP_readthedocs(bpy.types.Operator):
|
bl_idname="kh.readthedocs"
|
||||||
"""Check online documentation"""
|
bl_label="Online documentation"
|
||||||
|
bl_description="Go to Khanat Development Guide"
|
||||||
bl_idname = "kh.readthedocs"
|
bl_options= {"REGISTER", "UNDO"}
|
||||||
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"}
|
|
||||||
|
|
||||||
|
|
||||||
|
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"}
|
||||||
|
|
18
addon/preferences/__init__.py
Normal file
18
addon/preferences/__init__.py
Normal file
|
@ -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)
|
50
addon/preferences/preferences.py
Normal file
50
addon/preferences/preferences.py
Normal file
|
@ -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")
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def register_addon():
|
def register_addon():
|
||||||
|
|
||||||
# Icons
|
from ..preferences import register_preferences
|
||||||
from ..icons import register_icons
|
register_preferences()
|
||||||
register_icons()
|
|
||||||
|
|
||||||
# Operators
|
|
||||||
from ..operators import register_operators
|
from ..operators import register_operators
|
||||||
register_operators()
|
register_operators()
|
||||||
|
|
||||||
# Menus
|
from ..icons import register_icons
|
||||||
|
register_icons()
|
||||||
|
|
||||||
from ..menus import register_menus
|
from ..menus import register_menus
|
||||||
register_menus()
|
register_menus()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def unregister_addon():
|
def unregister_addon():
|
||||||
|
|
||||||
# Menus
|
|
||||||
from ..menus import unregister_menus
|
from ..menus import unregister_menus
|
||||||
unregister_menus()
|
unregister_menus()
|
||||||
|
|
||||||
# Operators
|
from ..icons import unregister_icons
|
||||||
|
unregister_icons()
|
||||||
|
|
||||||
from ..operators import unregister_operators
|
from ..operators import unregister_operators
|
||||||
unregister_operators()
|
unregister_operators()
|
||||||
|
|
||||||
# Icons
|
from ..preferences import unregister_preferences
|
||||||
from ..icons import unregister_icons
|
unregister_preferences()
|
||||||
unregister_icons()
|
|
Loading…
Reference in a new issue