diff --git a/assets/advanced/advanced.blend b/assets/advanced/advanced.blend index 4f374fe..6ce38c0 100644 Binary files a/assets/advanced/advanced.blend and b/assets/advanced/advanced.blend differ diff --git a/assets/advanced/assets_game.assets.ron b/assets/advanced/assets_game.assets.ron index 6efe66d..f21a754 100644 --- a/assets/advanced/assets_game.assets.ron +++ b/assets/advanced/assets_game.assets.ron @@ -1,5 +1,5 @@ ({ - "world":File (path: "advanced/models/world.glb#Scene0"), + "world":File (path: "advanced/models/World.glb#Scene0"), "models": Folder ( path: "advanced/models/library", ), diff --git a/assets/advanced/models/World.glb b/assets/advanced/models/World.glb new file mode 100644 index 0000000..8918e96 Binary files /dev/null and b/assets/advanced/models/World.glb differ diff --git a/assets/advanced/models/world.glb b/assets/advanced/models/world.glb deleted file mode 100644 index 47eeea6..0000000 Binary files a/assets/advanced/models/world.glb and /dev/null differ diff --git a/docs/blender_addon_add_scene.png b/docs/blender_addon_add_scene.png new file mode 100644 index 0000000..9ee832b Binary files /dev/null and b/docs/blender_addon_add_scene.png differ diff --git a/docs/blender_addon_add_scene2.png b/docs/blender_addon_add_scene2.png new file mode 100644 index 0000000..b439ed2 Binary files /dev/null and b/docs/blender_addon_add_scene2.png differ diff --git a/docs/blender_addon_add_scene3.png b/docs/blender_addon_add_scene3.png new file mode 100644 index 0000000..835cc5c Binary files /dev/null and b/docs/blender_addon_add_scene3.png differ diff --git a/docs/blender_addon_use3.png b/docs/blender_addon_use3.png index 6555e13..dbc8948 100644 Binary files a/docs/blender_addon_use3.png and b/docs/blender_addon_use3.png differ diff --git a/tools/gltf_auto_export/README.md b/tools/gltf_auto_export/README.md index ede7351..54e9232 100644 --- a/tools/gltf_auto_export/README.md +++ b/tools/gltf_auto_export/README.md @@ -2,8 +2,15 @@ For convenience I also added this [Blender addon](./gltf_auto_export.py) that - automatically exports your level/world from Blender to gltf whenever you save your Blend file. -- it also supports automatical exports of used collections as [Gltf blueprints](../../crates/bevy_gltf_blueprints/README.md) & more ! - +- in Blueprints mode (highly recommended !) : + - supports automatic exports of used collections as [Gltf blueprints](../../crates/bevy_gltf_blueprints/README.md) + - supports any number of main/level scenes + - Blender scenes where you define your levels, and all collection instances are replaced with "pointers" to other gltf files (all automatic) + - supports any number of library scenes + - Blender scenes where you define the assets that you use in your levels, in the form of collections + - automatic export of **changed** objects & collections only ! a sort of "incremental export", where only the changed collections (if in use) + get exported when you save your blend file + ## Installation: @@ -25,9 +32,26 @@ For convenience I also added this [Blender addon](./gltf_auto_export.py) that ![blender addon use](../../docs/blender_addon_use.png) -* set the autoexport parameters : output path, name of your main scene etc in the **auto export** panel +* set the autoexport parameters in the **auto export** panel: + + ![blender addon use3](../../docs/blender_addon_use3.png) + + + - export folder + - pick your main (level) scenes and library scenes (see the chapter about Blueprints below) + - click in the scene picker & select your scene + + ![select scene](../../docs/blender_addon_add_scene.png) + + - click on the "+" icon + + ![select scene2](../../docs/blender_addon_add_scene2.png) + + - your scene is added to the list + + ![select scene3](../../docs/blender_addon_add_scene3.png) + -![blender addon use3](../../docs/blender_addon_use3.png) * and your standard gltf export parameters in the **gltf** panel @@ -84,12 +108,6 @@ and what actually gets exported for the main scene/world/level all collections instances replaced with empties, and all those collections exported to gltf files as seen above - -### TODO: - -- [ ] add ability to have multiple main & library scenes -- [ ] detect which objects have been changed to only re-export those - ## License This tool, all its code, contents & assets is Dual-licensed under either of diff --git a/tools/gltf_auto_export/gltf_auto_export.py b/tools/gltf_auto_export/gltf_auto_export.py index 1ab5232..7e758ca 100644 --- a/tools/gltf_auto_export/gltf_auto_export.py +++ b/tools/gltf_auto_export/gltf_auto_export.py @@ -1,7 +1,7 @@ bl_info = { "name": "gltf_auto_export", "author": "kaosigh", - "version": (0, 3), + "version": (0, 5), "blender": (3, 4, 0), "location": "File > Import-Export", "description": "glTF/glb auto-export", @@ -13,6 +13,7 @@ bl_info = { import os import bpy +import traceback from bpy.types import Operator, AddonPreferences from bpy.app.handlers import persistent from bpy_extras.io_utils import ExportHelper @@ -23,39 +24,177 @@ from bpy.props import (BoolProperty, CollectionProperty ) +bpy.context.window_manager['changed_objects_per_scene'] = {} +bpy.context.window_manager['previous_params'] = {} +bpy.context.window_manager['__gltf_auto_export_initialized'] = False +bpy.context.window_manager['__gltf_auto_export_gltf_params_changed'] = False + +scene_key = "auto_gltfExportSettings" +################## +### internals too + +class SceneLink(bpy.types.PropertyGroup): + name: bpy.props.StringProperty(name="") + scene: bpy.props.PointerProperty(type=bpy.types.Scene) + +class SceneLinks(bpy.types.PropertyGroup): + name = bpy.props.StringProperty(name="List of scenes to export", default="Unknown") + items: bpy.props.CollectionProperty(type = SceneLink) + +class CUSTOM_PG_sceneName(bpy.types.PropertyGroup): + name: bpy.props.StringProperty() + display: bpy.props.BoolProperty() + + +################ + +# TODO: move this out +class CUSTOM_OT_actions(Operator): + """Move items up and down, add and remove""" + bl_idname = "scene_list.list_action" + bl_label = "List Actions" + bl_description = "Move items up and down, add and remove" + bl_options = {'REGISTER'} + + action: bpy.props.EnumProperty( + items=( + ('UP', "Up", ""), + ('DOWN', "Down", ""), + ('REMOVE', "Remove", ""), + ('ADD', "Add", ""))) + + + scene_type: bpy.props.StringProperty()#TODO: replace with enum + + def invoke(self, context, event): + print("INVOKE", self.scene_type, __name__) + source = bpy.context.preferences.addons[__name__].preferences + target_name = "library_scenes" + target_index = "library_scenes_index" + if self.scene_type == "level": + target_name = "main_scenes" + target_index = "main_scenes_index" + + target = getattr(source, target_name) + idx = getattr(source, target_index) + + try: + item = target[idx] + except IndexError: + pass + else: + if self.action == 'DOWN' and idx < len(target) - 1: + item_next = target[idx + 1].name + target.move(idx, idx + 1) + source[target_index] += 1 + info = 'Item "%s" moved to position %d' % (item.name, source[target_index] + 1) + self.report({'INFO'}, info) + + elif self.action == 'UP' and idx >= 1: + item_prev = target[idx - 1].name + target.move(idx, idx - 1) + source[target_index] -= 1 + info = 'Item "%s" moved to position %d' % (item.name, source[target_index] + 1) + self.report({'INFO'}, info) + + elif self.action == 'REMOVE': + info = 'Item "%s" removed from list' % (target[idx].name) + source[target_index] -= 1 + target.remove(idx) + self.report({'INFO'}, info) + + if self.action == 'ADD': + new_scene_name = None + if self.scene_type == "level": + if context.scene.main_scene: + new_scene_name = context.scene.main_scene.name + else: + if context.scene.library_scene: + new_scene_name = context.scene.library_scene.name + if new_scene_name: + item = target.add() + item.name = new_scene_name#f"Rule {idx +1}" + + if self.scene_type == "level": + context.scene.main_scene = None + else: + context.scene.library_scene = None + + #name = f"Rule {idx +1}" + #target.append({"name": name}) + + source[target_index] = len(target) - 1 + info = '"%s" added to list' % (item.name) + self.report({'INFO'}, info) + + return {"FINISHED"} + + + +############# #see here for original gltf exporter infos https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/__init__.py @persistent def deps_update_handler(scene, depsgraph): - print("depsgraph_update_post", scene.name) - print("-------------") - changed_objects = [] - for obj in depsgraph.updates: - if isinstance(obj.id, bpy.types.Object): - print("object changed, amazing", obj.id, obj.id.name) - changed_objects.append(obj.id.name) - - changed = scene.name or "" + if scene.name != "temp_scene": # actually do we care about anything else than the main scene(s) ? + print("depsgraph_update_post", scene.name) + print("-------------") + changed = scene.name or "" - bpy.context.window_manager.changedObjects.clear() - for obj in changed_objects: - new_entry = bpy.context.window_manager.changedObjects.add() - new_entry.name = obj - #bpy.context.window_manager.changedObjects.add(obj) + # depsgraph = bpy.context.evaluated_depsgraph_get() + if not 'changed_objects_per_scene' in bpy.context.window_manager: + bpy.context.window_manager['changed_objects_per_scene'] = {} - print("changed objects", bpy.context.window_manager.changedObjects) - - bpy.context.window_manager.changedScene = changed + if not changed in bpy.context.window_manager['changed_objects_per_scene']: + bpy.context.window_manager['changed_objects_per_scene'][changed] = {} + + for obj in depsgraph.updates: + if isinstance(obj.id, bpy.types.Object): + # get the actual object + object = bpy.data.objects[obj.id.name] + bpy.context.window_manager['changed_objects_per_scene'][scene.name][obj.id.name] = object + + bpy.context.window_manager.changedScene = changed @persistent def save_handler(dummy): print("-------------") print("saved", bpy.data.filepath) - auto_export() + if not 'changed_objects_per_scene' in bpy.context.window_manager: + bpy.context.window_manager['changed_objects_per_scene'] = {} + changes_per_scene = bpy.context.window_manager['changed_objects_per_scene'] + #determine changed parameters + addon_prefs = bpy.context.preferences.addons[__name__].preferences + + prefs = {} + for (k,v) in addon_prefs.items(): + if k not in AutoExportGltfPreferenceNames: + prefs[k] = v + + set1 = set(bpy.context.window_manager['previous_params'].items()) + set2 = set(prefs.items()) + difference = dict(set1 ^ set2) + + changed_param_names = list(set(difference.keys())- set(AutoExportGltfPreferenceNames)) + changed_parameters = len(changed_param_names) > 0 + # do the export + auto_export(changes_per_scene, changed_parameters) + + + # save the parameters + # todo add back + for (k, v) in prefs.items(): + bpy.context.window_manager['previous_params'][k] = v + + # reset a few things after exporting + # reset wether the gltf export paramters were changed since the last save + bpy.context.window_manager['__gltf_auto_export_gltf_params_changed'] = False + # reset whether there have been changed objects since the last save + bpy.context.window_manager['changed_objects_per_scene'] = {} def get_changedScene(self): return self["changedScene"] @@ -219,12 +358,12 @@ def get_used_collections(scene): collection_names = set() used_collections = [] for object in scene_objects: - print("object ", object) + #print("object ", object) if object.instance_type == 'COLLECTION': - print("THIS OBJECT IS A COLLECTION") + #print("THIS OBJECT IS A COLLECTION") # print("instance_type" ,object.instance_type) collection_name = object.instance_collection.name - print("instance collection", object.instance_collection.name) + #print("instance collection", object.instance_collection.name) #object.instance_collection.users_scene # del object['blueprint'] # object['BlueprintName'] = '"'+collection_name+'"' @@ -232,7 +371,7 @@ def get_used_collections(scene): collection_names.add(collection_name) used_collections.append(object.instance_collection) - print("scene objects", scene_objects) + #print("scene objects", scene_objects) return (collection_names, used_collections) @@ -243,7 +382,7 @@ def traverse_tree(t): # gets all collections that should ALWAYS be exported to their respective gltf files, even if they are not used in the main scene/level def get_marked_collections(scene): - print("checking library for marked collections") + # print("checking library for marked collections") root_collection = scene.collection marked_collections = [] collection_names = [] @@ -290,39 +429,73 @@ def generate_gltf_export_preferences(addon_prefs): for key in addon_prefs.__annotations__.keys(): if str(key) not in AutoExportGltfPreferenceNames: - print("overriding setting", key, "value", getattr(addon_prefs,key)) + #print("overriding setting", key, "value", getattr(addon_prefs,key)) gltf_export_preferences[key] = getattr(addon_prefs,key) return gltf_export_preferences + +# get exportable collections from lists of mains scenes and lists of library scenes +def get_exportable_collections(main_scenes, library_scenes): + all_collections = [] + for main_scene in main_scenes: + (collection_names, _) = get_used_collections(main_scene) + all_collections = all_collections + list(collection_names) + for library_scene in library_scenes: + marked_collections = get_marked_collections(library_scene) + all_collections = all_collections + marked_collections[0] + return all_collections + +def check_if_blueprints_exist(collections, folder_path, extension): + not_found_blueprints = [] + for collection_name in collections: + gltf_output_path = os.path.join(folder_path, collection_name + extension) + print("gltf_output_path", gltf_output_path) + found = os.path.exists(gltf_output_path) and os.path.isfile(gltf_output_path) + if not found: + not_found_blueprints.append(collection_name) + return not_found_blueprints + ###################################################### #### Export logic ##### +# find which of the library scenes the given collection stems from +# TODO: does not seem efficient at all ? +def get_source_scene(collection_name, library_scenes): + match = None + for scene in library_scenes: + root_collection = scene.collection + found = False + for cur_collection in traverse_tree(root_collection): + if cur_collection.name == collection_name: + found = True + break + if found: + match = scene + break + return match + +def get_collections_per_scene(collection_names, library_scenes): + collections_per_scene = {} + for scene in library_scenes: + root_collection = scene.collection + for cur_collection in traverse_tree(root_collection): + if cur_collection.name in collection_names: + if not scene.name in collections_per_scene: + collections_per_scene[scene.name] = [] + collections_per_scene[scene.name].append(cur_collection.name) + + return collections_per_scene + # export collections: all the collections that have an instance in the main scene AND any marked collections, even if they do not have instances -def export_collections(scene, folder_path, addon_prefs, gltf_export_preferences): - (collection_names, used_collections) = get_used_collections(scene) - library_scene = getattr(addon_prefs, "export_library_scene_name") - marked_collections = get_marked_collections(bpy.data.scenes[library_scene]) - print("used collection names", collection_names, used_collections) - print("marked collection names", marked_collections[0]) - +def export_collections(collections, folder_path, library_scene, addon_prefs, gltf_export_preferences): # set active scene to be the library scene (hack for now) - bpy.context.window.scene = bpy.data.scenes[library_scene] + bpy.context.window.scene = library_scene # save current active collection active_collection = bpy.context.view_layer.active_layer_collection - all_collections = list(collection_names) + marked_collections[0] - # we save this list of collections in the context - bpy.context.window_manager.exportedCollections.clear() - - for collection_name in all_collections: + for collection_name in collections: print("exporting collection", collection_name) - - #TODO: add error handling for this - ui_info = bpy.context.window_manager.exportedCollections.add() - ui_info.name = collection_name - - layer_collection = bpy.context.view_layer.layer_collection layerColl = recurLayerCollection(layer_collection, collection_name) # set active collection to the collection @@ -337,29 +510,34 @@ def export_collections(scene, folder_path, addon_prefs, gltf_export_preferences) bpy.context.view_layer.active_layer_collection = active_collection -def export_main(scene, folder_path, addon_prefs): - output_name = getattr(addon_prefs,"export_main_output_name") +def export_blueprints_from_collections(collections, library_scene, folder_path, addon_prefs): + export_output_folder = getattr(addon_prefs,"export_output_folder") gltf_export_preferences = generate_gltf_export_preferences(addon_prefs) - print("exporting to", folder_path, output_name) + export_blueprints_path = os.path.join(folder_path, export_output_folder, getattr(addon_prefs,"export_blueprints_path")) if getattr(addon_prefs,"export_blueprints_path") != '' else folder_path + + #print("-----EXPORTING BLUEPRINTS----") + #print("LIBRARY EXPORT", export_blueprints_path ) + + try: + export_collections(collections, export_blueprints_path, library_scene, addon_prefs, gltf_export_preferences) + except Exception as error: + print("failed to export collections to gltf: ", error) + # TODO : rethrow + + +# export all main scenes +def export_main_scenes(scenes, folder_path, addon_prefs): + for scene in scenes: + export_main_scene(scene, folder_path, addon_prefs) + +def export_main_scene(scene, folder_path, addon_prefs): + export_output_folder = getattr(addon_prefs,"export_output_folder") + gltf_export_preferences = generate_gltf_export_preferences(addon_prefs) + print("exporting to", folder_path, export_output_folder) export_blueprints = getattr(addon_prefs,"export_blueprints") - export_blueprints_path = os.path.join(folder_path, getattr(addon_prefs,"export_blueprints_path")) if getattr(addon_prefs,"export_blueprints_path") != '' else folder_path - - # backup current active scene - old_current_scene = bpy.context.scene - # backup current selections - old_selections = bpy.context.selected_objects - - + if export_blueprints : - print("-----EXPORTING BLUEPRINTS----") - print("LIBRARY EXPORT", export_blueprints_path ) - - try: - export_collections(scene, export_blueprints_path, addon_prefs, gltf_export_preferences) - except Exception as error: - print("failed to export collections to gltf: ", error) - (hollow_scene, object_names) = generate_hollow_scene(scene) #except Exception: # print("failed to create hollow scene") @@ -367,7 +545,7 @@ def export_main(scene, folder_path, addon_prefs): # set active scene to be the given scene bpy.context.window.scene = hollow_scene - gltf_output_path = os.path.join(folder_path, output_name) + gltf_output_path = os.path.join(folder_path, export_output_folder, scene.name) export_settings = { **gltf_export_preferences, 'use_active_scene': True, @@ -382,42 +560,135 @@ def export_main(scene, folder_path, addon_prefs): if export_blueprints : clear_hollow_scene(hollow_scene, scene, object_names) - # reset current scene from backup - bpy.context.window.scene = old_current_scene - # reset selections - for obj in old_selections: - obj.select_set(True) - - """Main function""" -def auto_export(): - file_path = bpy.data.filepath - # Get the folder - folder_path = os.path.dirname(file_path) - # get the preferences for our addon +def auto_export(changes_per_scene, changed_export_parameters): addon_prefs = bpy.context.preferences.addons[__name__].preferences - print("last changed", bpy.context.window_manager.changedScene) + # a semi_hack to ensure we have the latest version of the settings + initialized = bpy.context.window_manager['__gltf_auto_export_initialized'] if '__gltf_auto_export_initialized' in bpy.context.window_manager else False + if not initialized: + print("not initialized, fetching settings if any") + # semi_hack to restore the correct settings if the add_on was installed before + settings = bpy.context.scene.get(scene_key) + if settings: + print("loading settings") + try: + # Update filter if user saved settings + #if hasattr(self, 'export_format'): + # self.filter_glob = '*.glb' if self.export_format == 'GLB' else '*.gltf' + for (k, v) in settings.items(): + setattr(addon_prefs, k, v) + except Exception as error: + print("error setting preferences from saved settings", error) + bpy.context.window_manager['__gltf_auto_export_initialized'] = True - # optimised variation - last_changed = bpy.context.window_manager.changedScene + # have the export parameters (not auto export, just gltf export) have changed: if yes (for example switch from glb to gltf, compression or not, animations or not etc), we need to re-export everything + print ("changed_export_parameters", changed_export_parameters) + try: + file_path = bpy.data.filepath + # Get the folder + folder_path = os.path.dirname(file_path) + # get the preferences for our addon - export_main_scene_name = getattr(addon_prefs,"export_main_scene_name") - export_on_library_changes = getattr(addon_prefs,"export_on_library_changes") - export_library_scene_name = getattr(addon_prefs,"export_library_scene_name") + export_blueprints = getattr(addon_prefs,"export_blueprints") + export_output_folder = getattr(addon_prefs,"export_output_folder") - # export the main game world - game_scene = bpy.data.scenes[export_main_scene_name] + main_scene_names= list(map(lambda scene: scene.name, getattr(addon_prefs,"main_scenes"))) + library_scene_names = list(map(lambda scene: scene.name, getattr(addon_prefs,"library_scenes"))) + print("main scenes", main_scene_names, "library_scenes", library_scene_names) + print("export_output_folder", export_output_folder) + + # export the main game world + level_scenes = list(map(lambda name: bpy.data.scenes[name], main_scene_names)) + library_scenes = list(map(lambda name: bpy.data.scenes[name], library_scene_names)) + + # export everything everytime + if export_blueprints: + print("EXPORTING") + # get a list of all collections actually in use + collections = get_exportable_collections(level_scenes, library_scenes) + # first check if all collections have already been exported before (if this is the first time the exporter is run + # in your current Blender session for example) + export_blueprints_path = os.path.join(folder_path, export_output_folder, getattr(addon_prefs,"export_blueprints_path")) if getattr(addon_prefs,"export_blueprints_path") != '' else folder_path + + gltf_extension = getattr(addon_prefs, "export_format") + gltf_extension = '.glb' if gltf_extension == 'GLB' else '.gltf' + collections_not_on_disk = check_if_blueprints_exist(collections, export_blueprints_path, gltf_extension) + changed_collections = [] + + print('changes_per_scene', changes_per_scene.items(), changes_per_scene.keys()) + for scene, bla in changes_per_scene.items(): + print(" changed scene", scene) + for obj_name, obj in bla.items(): + object_collections = list(obj.users_collection) + object_collection_names = list(map(lambda collection: collection.name, object_collections)) + if len(object_collection_names) > 1: + print("ERRROR, objects in multiple collections not supported") + else: + object_collection_name = object_collection_names[0] if len(object_collection_names) > 0 else None + print(" object ", obj, object_collection_name) + if object_collection_name in collections: + changed_collections.append(object_collection_name) + + collections_to_export = list(set(changed_collections + collections_not_on_disk)) + + # we need to re_export everything if the export parameters have been changed + collections_to_export = collections if changed_export_parameters else collections_to_export + collections_per_scene = get_collections_per_scene(collections_to_export, library_scenes) + + print("--------------") + print("collections: all:", collections) + print("collections: changed:", changed_collections) + print("collections: not found on disk:", collections_not_on_disk) + print("collections: to export:", collections_to_export) + print("collections: per_scene:", collections_per_scene) + + # backup current active scene + old_current_scene = bpy.context.scene + # backup current selections + old_selections = bpy.context.selected_objects + + # first export any main/level/world scenes + print("export MAIN scenes") + for scene_name in main_scene_names: + do_export_main_scene = changed_export_parameters or (scene_name in changes_per_scene.keys() and len(changes_per_scene[scene_name].keys()) > 0) + if do_export_main_scene: + print(" exporting scene:", scene_name) + export_main_scene(bpy.data.scenes[scene_name], folder_path, addon_prefs) + + + # now deal with blueprints/collections + do_export_library_scene = changed_export_parameters or len(collections_to_export) > 0 # export_library_scene_name in changes_per_scene.keys() + print("export LIBRARY") + if do_export_library_scene: + # we only want to go through the library scenes where our collections to export are present + for (scene_name, collections_to_export) in collections_per_scene.items(): + print(" exporting collections from scene:", scene_name) + print(" collections to export", collections_to_export) + library_scene = bpy.data.scenes[scene_name] + export_blueprints_from_collections(collections_to_export, library_scene, folder_path, addon_prefs) + + + # reset current scene from backup + bpy.context.window.scene = old_current_scene + # reset selections + for obj in old_selections: + obj.select_set(True) + + + else: + print("dsfsfsdf") + for scene_name in main_scene_names: + export_main_scene(bpy.data.scenes[scene_name], folder_path, addon_prefs) + + except Exception as error: + traceback.print_stack() + def error_message(self, context): + self.layout.label(text="Failure during auto_export: please check your main scene name & make sure your output folder exists. Error: "+ str(error)) + + bpy.context.window_manager.popup_menu(error_message, title="Error", icon='ERROR') - # most recent change was in the main scene (game world/ level) - if last_changed == export_main_scene_name: - print("game world changed, exporting game gltf only") - export_main(game_scene, folder_path, addon_prefs) - # if the library has changed, so will likely the game world that uses the library assets - if last_changed == export_library_scene_name and export_library_scene_name != "" and export_on_library_changes: - print("library changed") - export_main(game_scene, folder_path, addon_prefs) ###################################################### @@ -426,18 +697,23 @@ def auto_export(): AutoExportGltfPreferenceNames = [ 'auto_export', 'export_main_scene_name', - 'export_main_output_name', - 'export_on_library_changes', + 'export_output_folder', 'export_library_scene_name', 'export_blueprints', - 'export_blueprints_path' + 'export_blueprints_path', + + 'main_scenes', + 'library_scenes', + 'main_scenes_index', + 'library_scenes_index' ] class AutoExportGltfAddonPreferences(AddonPreferences): # this must match the add-on name, use '__package__' # when defining this in a submodule of a python package. bl_idname = __name__ - + bl_options = {'PRESET'} + auto_export: BoolProperty( name='Auto export', description='Automatically export to gltf on save', @@ -448,15 +724,10 @@ class AutoExportGltfAddonPreferences(AddonPreferences): description='The name of the main scene/level/world to auto export', default='Scene' ) - export_main_output_name: StringProperty( - name='Glb output name', - description='The glb output name for the main scene to auto export', - default='world' - ) - export_on_library_changes: BoolProperty( - name='Export on library changes', - description='Export main scene on library changes', - default=False + export_output_folder: StringProperty( + name='Export folder (relative)', + description='The root folder for all exports(relative to current file) Defaults to current folder', + default='' ) export_library_scene_name: StringProperty( name='Library scene', @@ -467,14 +738,19 @@ class AutoExportGltfAddonPreferences(AddonPreferences): export_blueprints: BoolProperty( name='Export Blueprints', description='Replaces collection instances with an Empty with a BlueprintName custom property', - default=False + default=True ) export_blueprints_path: StringProperty( name='Blueprints path', - description='path to export the blueprints to (relative to this Blend file)', - default='' + description='path to export the blueprints to (relative to the Export folder)', + default='library' ) + main_scenes: CollectionProperty(name="main scenes", type=CUSTOM_PG_sceneName) + main_scenes_index: IntProperty(name = "Index for main scenes list", default = 0) + + library_scenes: CollectionProperty(name="library scenes", type=CUSTOM_PG_sceneName) + library_scenes_index: IntProperty(name = "Index for library scenes list", default = 0) ##### export_format: EnumProperty( @@ -726,7 +1002,7 @@ class AutoExportGltfAddonPreferences(AddonPreferences): default=False ) -class AutoExportGLTF(Operator, ExportHelper): +class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): """test""" bl_idname = "export_scenes.auto_gltf" bl_label = "Apply settings" @@ -739,50 +1015,109 @@ class AutoExportGLTF(Operator, ExportHelper): default='*.glb;*.gltf', options={'HIDDEN'} ) - """ - auto_export: BoolProperty( - name='Auto export', - description='Automatically export to gltf on save', + + will_save_settings: BoolProperty( + name='Remember Export Settings', + description='Store glTF export settings in the Blender project', default=True ) - export_main_scene_name: StringProperty( - name='Main scene', - description='The name of the main scene/level/world to auto export', - default='Scene' - ) - export_main_output_name: StringProperty( - name='Glb output name', - description='The glb output name for the main scene to auto export', - default='world' - ) - export_on_library_changes: BoolProperty( - name='Export on library changes', - description='Export main scene on library changes', - default=False - ) - export_library_scene_name: StringProperty( - name='Library scene', - description='The name of the library scene to auto export', - default='Library' - ) - # blueprint settings - export_blueprints: BoolProperty( - name='Export Blueprints', - description='Replaces collection instances with an Empty with a BlueprintName custom property', - default=False - ) - export_blueprints_path: StringProperty( - name='Blueprints path', - description='path to export the blueprints to (relative to this Blend file)', - default='' - )""" - - def draw(self, context): - pass + + # Custom scene property for saving settings + scene_key = "auto_gltfExportSettings" + + def save_settings(self, context): + # find all props to save + exceptional = [ + # options that don't start with 'export_' + 'main_scenes', + 'library_scenes' + ] + all_props = self.properties + export_props = { + x: getattr(self, x) for x in dir(all_props) + if (x.startswith("export_") or x in exceptional) and all_props.get(x) is not None + } + print("saving settings", export_props)#, self.properties, dir(self.properties)) + context.scene[self.scene_key] = export_props + + + def apply_settings_to_preferences(self, context): + # find all props to save + exceptional = [ + # options that don't start with 'export_' + 'main_scenes', + 'library_scenes' + ] + all_props = self.properties + export_props = { + x: getattr(self, x) for x in dir(all_props) + if (x.startswith("export_") or x in exceptional) and all_props.get(x) is not None + } + addon_prefs = bpy.context.preferences.addons[__name__].preferences + + for (k, v) in export_props.items(): + setattr(addon_prefs, k, v) + def execute(self, context): - preferences = context.preferences + if self.will_save_settings: + self.save_settings(context) + # apply the operator properties to the addon preferences + self.apply_settings_to_preferences(context) + return {'FINISHED'} + + def invoke(self, context, event): + settings = context.scene.get(self.scene_key) + self.will_save_settings = False + if settings: + print("loading settings") + try: + for (k, v) in settings.items(): + print("loading setting", k, v) + setattr(self, k, v) + self.will_save_settings = True + + # Update filter if user saved settings + if hasattr(self, 'export_format'): + self.filter_glob = '*.glb' if self.export_format == 'GLB' else '*.gltf' + + except (AttributeError, TypeError): + self.report({"ERROR"}, "Loading export settings failed. Removed corrupted settings") + del context.scene[self.scene_key] + + + for (k, v) in self.properties.items(): + print("PROPERTIES", k, v) + + addon_prefs = bpy.context.preferences.addons[__name__].preferences + + main_scene_names= list(map(lambda scene: scene.name, getattr(addon_prefs,"main_scenes"))) + library_scene_names = list(map(lambda scene: scene.name, getattr(addon_prefs,"library_scenes"))) + level_scenes = list(map(lambda name: bpy.data.scenes[name], main_scene_names)) + library_scenes = list(map(lambda name: bpy.data.scenes[name], library_scene_names)) + + collections = get_exportable_collections(level_scenes, library_scenes) + + try: + # we save this list of collections in the context + bpy.context.window_manager.exportedCollections.clear() + #TODO: add error handling for this + for collection_name in collections: + ui_info = bpy.context.window_manager.exportedCollections.add() + ui_info.name = collection_name + except Exception as error: + self.report({"ERROR"}, "Failed to populate list of exported collections/blueprints") + + + + wm = context.window_manager + wm.fileselect_add(self) + return {'RUNNING_MODAL'} + # return self.execute(context) + + def draw(self, context): + pass class GLTF_PT_auto_export_main(bpy.types.Panel): bl_space_type = 'FILE_BROWSER' @@ -817,7 +1152,6 @@ class GLTF_PT_auto_export_root(bpy.types.Panel): def poll(cls, context): sfile = context.space_data operator = sfile.active_operator - return operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf" def draw_header(self, context): @@ -831,16 +1165,69 @@ class GLTF_PT_auto_export_root(bpy.types.Panel): layout.use_property_decorate = False # No animation. sfile = context.space_data - #operator = sfile.active_operator - operator = bpy.context.preferences.addons[__name__].preferences + operator = sfile.active_operator layout.active = operator.auto_export - layout.prop(operator, "export_main_scene_name") - layout.prop(operator, "export_library_scene_name") + layout.prop(operator, 'will_save_settings') + layout.prop(operator, "export_output_folder") - layout.prop(operator, "export_main_output_name") - layout.prop(operator, "export_on_library_changes") - + # scene selectors + row = layout.row() + col = row.column(align=True) + col.separator() + + source = bpy.context.preferences.addons[__name__].preferences + + rows = 2 + + # main/level scenes + layout.label(text="main scenes") + layout.prop(context.scene, "main_scene", text='') + + row = layout.row() + row.template_list("GLTF_auto_export_UL_SCENES", "level scenes", source, "main_scenes", source, "main_scenes_index", rows=rows) + + col = row.column(align=True) + sub_row = col.row() + add_operator = sub_row.operator("scene_list.list_action", icon='ADD', text="") + add_operator.action = 'ADD' + add_operator.scene_type = 'level' + #add_operator.source = operator + sub_row.enabled = context.scene.main_scene is not None + + sub_row = col.row() + remove_operator = sub_row.operator("scene_list.list_action", icon='REMOVE', text="") + remove_operator.action = 'REMOVE' + remove_operator.scene_type = 'level' + col.separator() + + #up_operator = col.operator("scene_list.list_action", icon='TRIA_UP', text="") + #up_operator.action = 'UP' + #col.operator("scene_list.list_action", icon='TRIA_DOWN', text="").action = 'DOWN' + + # library scenes + layout.label(text="library scenes") + layout.prop(context.scene, "library_scene", text='') + + row = layout.row() + row.template_list("GLTF_auto_export_UL_SCENES", "library scenes", source, "library_scenes", source, "library_scenes_index", rows=rows) + + col = row.column(align=True) + sub_row = col.row() + add_operator = sub_row.operator("scene_list.list_action", icon='ADD', text="") + add_operator.action = 'ADD' + add_operator.scene_type = 'library' + sub_row.enabled = context.scene.library_scene is not None + + + sub_row = col.row() + remove_operator = sub_row.operator("scene_list.list_action", icon='REMOVE', text="") + remove_operator.action = 'REMOVE' + remove_operator.scene_type = 'library' + col.separator() + + + class GLTF_PT_auto_export_blueprints(bpy.types.Panel): bl_space_type = 'FILE_BROWSER' bl_region_type = 'TOOL_PROPS' @@ -861,11 +1248,10 @@ class GLTF_PT_auto_export_blueprints(bpy.types.Panel): sfile = context.space_data operator = sfile.active_operator - addon_prefs = bpy.context.preferences.addons[__name__].preferences - - layout.prop(addon_prefs, "export_blueprints") - layout.prop(addon_prefs, "export_blueprints_path") + # addon_prefs = bpy.context.preferences.addons[__name__].preferences + layout.prop(operator, "export_blueprints") + layout.prop(operator, "export_blueprints_path") class GLTF_PT_auto_export_collections_list(bpy.types.Panel): bl_space_type = 'FILE_BROWSER' @@ -913,6 +1299,9 @@ class GLTF_PT_auto_export_gltf(bpy.types.Panel): addon_prefs = preferences.addons[__name__].preferences layout = self.layout + sfile = context.space_data + operator = sfile.active_operator + #preferences = context.preferences #print("ADDON PREFERENCES ", list(preferences.addons.keys())) #print("standard blender gltf prefs", list(preferences.addons["io_scene_gltf2"].preferences.keys())) @@ -921,28 +1310,55 @@ class GLTF_PT_auto_export_gltf(bpy.types.Panel): #addon_prefs = preferences.addons[__name__].preferences - # print("KEYS", dir(addon_prefs)) + # print("KEYS", operator.properties.keys()) #print("BLAS", addon_prefs.__annotations__) #print(addon_prefs.__dict__) for key in addon_prefs.__annotations__.keys(): if key not in AutoExportGltfPreferenceNames: - layout.prop(addon_prefs, key) - #for key in addon_prefs_gltf.__annotations__.keys(): - # layout.prop(addon_prefs_gltf, key) + layout.prop(operator, key) + + + + +class GLTF_auto_export_UL_SCENES(bpy.types.UIList): + # The draw_item function is called for each item of the collection that is visible in the list. + # data is the RNA object containing the collection, + # item is the current drawn item of the collection, + # icon is the "computed" icon for the item (as an integer, because some objects like materials or textures + # have custom icons ID, which are not available as enum items). + # active_data is the RNA object containing the active property for the collection (i.e. integer pointing to the + # active item of the collection). + # active_propname is the name of the active property (use 'getattr(active_data, active_propname)'). + # index is index of the current item in the collection. + # flt_flag is the result of the filtering process for this item. + # Note: as index and flt_flag are optional arguments, you do not have to use/declare them here if you don't + # need them. + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + ob = data + # draw_item must handle the three layout types... Usually 'DEFAULT' and 'COMPACT' can share the same code. + if self.layout_type in {'DEFAULT', 'COMPACT'}: + # You should always start your row layout by a label (icon + text), or a non-embossed text field, + # this will also make the row easily selectable in the list! The later also enables ctrl-click rename. + # We use icon_value of label, as our given icon is an integer value, not an enum ID. + # Note "data" names should never be translated! + #if ma: + # layout.prop(ma, "name", text="", emboss=False, icon_value=icon) + #else: + # layout.label(text="", translate=False, icon_value=icon) + layout.label(text=item.name, icon_value=icon) + #layout.prop(item, "name", text="", emboss=False, icon_value=icon) + # 'GRID' layout type should be as compact as possible (typically a single icon!). + elif self.layout_type == 'GRID': + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon) + + def menu_func_import(self, context): self.layout.operator(AutoExportGLTF.bl_idname, text="glTF auto Export (.glb/gltf)") ###################################################### # internals -class ChangedObject(bpy.types.PropertyGroup): - name: bpy.props.StringProperty(name="") - -class ChangedObjects(bpy.types.PropertyGroup): - name = bpy.props.StringProperty(name="List of changed objects", default="Unknown") - items: bpy.props.CollectionProperty(type = ChangedObject) - - class CollectionToExport(bpy.types.PropertyGroup): name: bpy.props.StringProperty(name="") @@ -951,15 +1367,20 @@ class CollectionsToExport(bpy.types.PropertyGroup): items: bpy.props.CollectionProperty(type = CollectionToExport) + + ###################################################### classes = [ + SceneLink, + SceneLinks, + CUSTOM_PG_sceneName, + GLTF_auto_export_UL_SCENES, + CUSTOM_OT_actions, + AutoExportGLTF, AutoExportGltfAddonPreferences, - ChangedObject, - ChangedObjects, - CollectionToExport, CollectionsToExport, @@ -971,22 +1392,53 @@ classes = [ ] + + +def is_scene_ok(self, scene): + prefs = bpy.context.preferences.addons[__name__].preferences + return scene.name not in prefs.main_scenes and scene.name not in prefs.library_scenes + + def register(): for cls in classes: bpy.utils.register_class(cls) + bpy.types.Scene.main_scene = bpy.props.PointerProperty(type=bpy.types.Scene, name="main scene", description="main_scene_chooser", poll=is_scene_ok) + bpy.types.Scene.library_scene = bpy.props.PointerProperty(type=bpy.types.Scene, name="library scene", description="library_scene_picker", poll=is_scene_ok) + # setup handlers for updates & saving bpy.app.handlers.depsgraph_update_post.append(deps_update_handler) bpy.app.handlers.save_post.append(save_handler) bpy.types.WindowManager.changedScene = bpy.props.StringProperty(get=get_changedScene, set=set_changedScene) - bpy.types.WindowManager.changedObjects = bpy.props.CollectionProperty(type=ChangedObjects) bpy.types.WindowManager.exportedCollections = bpy.props.CollectionProperty(type=CollectionsToExport) # add our addon to the toolbar bpy.types.TOPBAR_MT_file_export.append(menu_func_import) + + ## just experiments + bpy.types.Scene.main_scenes_list_index = IntProperty(name = "Index for main scenes list", default = 0) + bpy.types.Scene.library_scenes_list_index = IntProperty(name = "Index for library scenes list", default = 0) + + + mock_main_scenes = []#["World", "level2"] + main_scenes = bpy.context.preferences.addons[__name__].preferences.main_scenes + for item_name in mock_main_scenes: + item = main_scenes.add() + item.name = item_name + + mock_library_scenes = [] #["Library", "Library2"] + library_scenes = bpy.context.preferences.addons[__name__].preferences.library_scenes + for item_name in mock_library_scenes: + item = library_scenes.add() + item.name = item_name + + bpy.context.preferences.addons[__name__].preferences.main_scenes_index = 0 + bpy.context.preferences.addons[__name__].preferences.library_scenes_index = 0 + + def unregister(): for cls in classes: bpy.utils.unregister_class(cls) @@ -998,8 +1450,15 @@ def unregister(): bpy.app.handlers.save_post.remove(save_handler) del bpy.types.WindowManager.changedScene - del bpy.types.WindowManager.changedObjects del bpy.types.WindowManager.exportedCollections + del bpy.types.Scene.main_scene + del bpy.types.Scene.library_scene + + del bpy.types.Scene.main_scenes_list_index + del bpy.types.Scene.library_scenes_list_index + + + if __name__ == "__main__": register() \ No newline at end of file diff --git a/tools/gltf_auto_export/old.py b/tools/gltf_auto_export/old.py deleted file mode 100644 index 1a67c9a..0000000 --- a/tools/gltf_auto_export/old.py +++ /dev/null @@ -1,1141 +0,0 @@ -bl_info = { - "name": "gltf_auto_export_gltf", - "author": "kaosigh", - "version": (0, 1), - "blender": (3, 4, 0), - "location": "File > Import-Export", - "description": "glTF/glb auto-export", - "warning": "", - "wiki_url": "", - "tracker_url": "", - "category": "Import-Export" - } - -import os -import bpy -from bpy.types import Operator, AddonPreferences -from bpy.app.handlers import persistent -from bpy_extras.io_utils import ExportHelper -from bpy.props import (BoolProperty, - IntProperty, - StringProperty, - EnumProperty, - CollectionProperty - ) - -#see here for original gltf exporter infos https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/__init__.py -@persistent -def deps_update_handler(scene): - print("-------------") - print("depsgraph_update_post", scene.name) - - changed = scene.name or "" - print("changed", changed) - bpy.context.scene.changedScene = changed - #set_ChangedScene(changed) - #auto_export() - - - -#https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf -def export_gltf (path, export_settings): - settings = {**export_settings, "filepath": path} - bpy.ops.export_scene.gltf(**settings) - -def get_collection_hierarchy(root_col, levels=1): - """Read hierarchy of the collections in the scene""" - level_lookup = {} - def recurse(root_col, parent, depth): - if depth > levels: - return - if isinstance(parent, bpy.types.Collection): - level_lookup.setdefault(parent, []).append(root_col) - for child in root_col.children: - recurse(child, root_col, depth + 1) - recurse(root_col, root_col.children, 0) - return level_lookup - -""" -This exports the library's collections into seperate gltf files -""" -def export_library_split(scene, folder_path, gltf_export_preferences): - # backup current active scene - old_current_scene = bpy.context.scene - # set active scene to be the given scene - bpy.context.window.scene = scene - - export_settings = { **gltf_export_preferences, 'use_active_scene': True} - - root_collection = scene.collection - collections_lookup = get_collection_hierarchy(root_collection, 3) - - children_to_parent_collections = {i : k for k, v in collections_lookup.items() for i in v} - scene_objects = [o for o in root_collection.objects] - candidates = [x for v in collections_lookup.values() for x in v] - """ - print("prt_col", children_to_parent_collections) - print("scene objects", scene_objects) - print("candidate", candidates) - """ - - if not candidates: - # self.report({'INFO'}, "Nothing to export") - # reset current scene from backup - bpy.context.window.scene = old_current_scene - return #{'CANCELLED'} - - # Unlink all Collections and objects - for canditate in candidates: - children_to_parent_collections.get(canditate).children.unlink(canditate) - for object in scene_objects: - scene_objects.objects.unlink(object) - - # (Re-)link collections of choice to root level and export - for canditate in candidates: - root_collection.children.link(canditate) - collection_name = canditate.name - gltf_output_path = os.path.join(folder_path, collection_name) - export_gltf(gltf_output_path, export_settings) - print("exporting", collection_name, "to", gltf_output_path) - root_collection.children.unlink(canditate) - - # Reset all back - for object in scene_objects: - scene_objects.objects.link(object) - for canditate in candidates: - children_to_parent_collections.get(canditate).children.link(canditate) - - # reset current scene from backup - bpy.context.window.scene = old_current_scene - -def debug_test(scene): - root_collection = scene.collection - collections_lookup = get_collection_hierarchy(root_collection, 1) - - children_to_parent_collections = {i : k for k, v in collections_lookup.items() for i in v} - scene_objects = [o for o in root_collection.objects] - candidates = [x for v in collections_lookup.values() for x in v] - print("prt_col", children_to_parent_collections) - print("scene objects", scene_objects) - print("candidates", candidates) - -""" -export the library into only a few gltf files ie - scene_collection - asset_pack1 - asset_a - asset_b - asset_c - - asset_pack2 - asset_d - asset_e - -would export 2 gltf files - asset_pack1.glb - with three scenes: - asset_a - asset_b - asset_c - - asset_pack2.glb - with two scenes: - asset_d - asset_f -""" -def export_library_merged(scene, folder_path, gltf_export_preferences): - # backup current active scene - old_current_scene = bpy.context.scene - # set active scene to be the given scene - bpy.context.window.scene = scene - - export_settings = { - **gltf_export_preferences, - 'use_active_scene': False, - 'use_visible': False, - } - - root_collection = scene.collection - collections_lookup = get_collection_hierarchy(root_collection, 3) - - children_to_parent_collections = {i : k for k, v in collections_lookup.items() for i in v} - scene_objects = [o for o in root_collection.objects] - candidates = [x for v in collections_lookup.values() for x in v] - - """ - print("prt_col", children_to_parent_collections) - print("scene objects", scene_objects) - print("candidate", candidates) - """ - - virtual_scenes = [] - if not candidates: - # self.report({'INFO'}, "Nothing to export") - return #{'CANCELLED'} - - for canditate in candidates: - #print("candidate collection", canditate) - virtual_scene = bpy.data.scenes.new(name=canditate.name) - virtual_scenes.append(virtual_scene) - virtual_scene.collection.children.link(canditate) - try: - gltf_output_path = os.path.join(folder_path, "library") - export_gltf(gltf_output_path, export_settings) - except Exception: - print("failed to export to gltf") - - for virtual_scene in virtual_scenes: - bpy.data.scenes.remove(virtual_scene) - - # TODO: we want to exclude the library and the game scene ??? - """ - #backup test - backup = bpy.data.scenes["toto"] - ## add back the scene - #bpy.data.scenes["toto"] = backup - collection = "" - try: - collection = bpy.data.collections["virtual"] - bpy.data.collections.remove(collection) - collection = bpy.data.collections.new("virtual") - except Exception: - collection = bpy.data.collections.new("virtual") - raise - - #bpy.data.scenes["toto"].collection.children.unlink(bpy.data.scenes["toto"].collection) - #print("copy", collection) - - # nuke the scene - toto = bpy.data.scenes["toto"] - print(" toto.collection", toto.collection.children) - for child in toto.collection.children: - print("child ", child) - for child in toto.collection.objects: - print("child ", child) - - collection.children.link(toto.collection) - - for child in collection.children: - print("child 2 ", child) - - for child in toto.collection.objects: - print("child ", child) - toto.collection.objects.unlink(child) - - #toto.collection.children.unlink(toto.collection) - - bpy.data.scenes.remove(toto) - - # now recreate it - toto = bpy.data.scenes.new(name="toto") - toto.collection.children.link(collection) - for child in collection.objects: - print("adding back child ", child) - toto.collection.objects.link(child) - """ - # reset current scene from backup - bpy.context.window.scene = old_current_scene - - -# the active collection is a View Layer concept, so you actually have to find the active LayerCollection -# which must be done recursively -def find_layer_collection_recursive(find, col): - for c in col.children: - if c.collection == find: - return c - return None - - -# Makes an empty, at location, stores it in existing collection, from https://blender.stackexchange.com/questions/51290/how-to-add-empty-object-not-using-bpy-ops -def make_empty(name, location, coll_name): #string, vector, string of existing coll - empty_obj = bpy.data.objects.new( "empty", None, ) - empty_obj.name = name - empty_obj.empty_display_size = 1 - bpy.data.collections[coll_name].objects.link(empty_obj) - empty_obj.location = location - return empty_obj - - -def make_empty2(name, location, collection): - object_data = None #bpy.data.meshes.new("NewMesh") #None - empty_obj = bpy.data.objects.new( name, object_data ) - empty_obj.name = name - empty_obj.location = location - - - empty_obj.empty_display_size = 2 - empty_obj.empty_display_type = 'PLAIN_AXES' - collection.objects.link( empty_obj ) - return empty_obj - -def make_empty3(name, location, collection): - original_active_object = bpy.context.active_object - bpy.ops.object.empty_add(type='PLAIN_AXES', location=location) - empty_obj = bpy.context.active_object - empty_obj.name = name - collection.objects.link( empty_obj ) - bpy.context.view_layer.objects.active = original_active_object - return empty_obj - -# generate a copy of a scene that replaces collection instances with empties -# FIXME: will not preserve original names -# alternative: copy original names before creating a new scene, & reset them -# or create empties, hide original ones, and do the same renaming trick -def generate_hollow_scene(scene): - root_collection = scene.collection - temp_scene = bpy.data.scenes.new(name="temp_scene") - copy_root_collection = temp_scene.collection - scene_objects = [o for o in root_collection.objects] - - - found = find_layer_collection_recursive(copy_root_collection, bpy.context.view_layer.layer_collection) - if found: - print("FOUND COLLECTION") - # once it's found, set the active layer collection to the one we found - bpy.context.view_layer.active_layer_collection = found - - #original_names = {} - original_names = [] - for object in scene_objects: - if object.instance_type == 'COLLECTION': - collection_name = object.instance_collection.name - - #original_names[object.name] = object.name# + "____bak" - #print("custom properties", object, object.keys(), object.items()) - #for k, e in object.items(): - # print("custom properties ", k, e) - print("object location", object.location) - original_name = object.name - original_names.append(original_name) - - object.name = original_name + "____bak" - empty_obj = make_empty3(original_name, object.location, copy_root_collection) - """we inject the collection/blueprint name, as a component called 'BlueprintName', but we only do this in the empty, not the original object""" - empty_obj['BlueprintName'] = '"'+collection_name+'"' - empty_obj['SpawnHere'] = '' - - for k, v in object.items(): - empty_obj[k] = v - else: - copy_root_collection.objects.link(object) - - # bpy.data.scenes.remove(temp_scene) - # objs = bpy.data.objects - #objs.remove(objs["Cube"], do_unlink=True) - return (temp_scene, original_names) - -def clear_hollow_scene(temp_scene, original_scene, original_names): - # reset original names - root_collection = original_scene.collection - scene_objects = [o for o in root_collection.objects] - - for object in scene_objects: - if object.instance_type == 'COLLECTION': - print("object name to reset", object.name) - if object.name.endswith("____bak"): - print("reseting") - object.name = object.name.replace("____bak", "") - - # remove empties (only needed when we go via ops ????) - root_collection = temp_scene.collection - scene_objects = [o for o in root_collection.objects] - for object in scene_objects: - if object.type == 'EMPTY': - bpy.data.objects.remove(object, do_unlink=True) - - bpy.data.scenes.remove(temp_scene) - -#Recursivly transverse layer_collection for a particular name -def recurLayerCollection(layerColl, collName): - found = None - if (layerColl.name == collName): - return layerColl - for layer in layerColl.children: - found = recurLayerCollection(layer, collName) - if found: - return found - -def get_used_collections(scene): - root_collection = scene.collection - - scene_objects = [o for o in root_collection.objects] - collection_names = set() - used_collections = [] - for object in scene_objects: - print("object ", object) - if object.instance_type == 'COLLECTION': - print("THIS OBJECT IS A COLLECTION") - # print("instance_type" ,object.instance_type) - collection_name = object.instance_collection.name - print("instance collection", object.instance_collection.name) - #object.instance_collection.users_scene - # del object['blueprint'] - # object['BlueprintName'] = '"'+collection_name+'"' - if not collection_name in collection_names: - collection_names.add(collection_name) - used_collections.append(object.instance_collection) - - print("scene objects", scene_objects) - return (collection_names, used_collections) - -def export_used_collections(scene, folder_path, gltf_export_preferences): - (collection_names, used_collections) = get_used_collections(scene) - print("used collection names", collection_names, used_collections) - - # set active scene to be the library scene (hack for now) - bpy.context.window.scene = bpy.data.scenes["library"] - # save current active collection - active_collection = bpy.context.view_layer.active_layer_collection - - for collection_name in list(collection_names): - print("exporting collection", collection_name) - - layer_collection = bpy.context.view_layer.layer_collection - layerColl = recurLayerCollection(layer_collection, collection_name) - # set active collection to the collection - bpy.context.view_layer.active_layer_collection = layerColl - - print("layercoll", layerColl) - gltf_output_path = os.path.join(folder_path, collection_name) - - export_settings = { **gltf_export_preferences, 'use_active_scene': True, 'use_active_collection': True} #'use_visible': False, - export_gltf(gltf_output_path, export_settings) - - # reset active collection to the one we save before - bpy.context.view_layer.active_layer_collection = active_collection - -def export_main(scene, folder_path, gltf_export_preferences, output_name, addon_prefs): - print("exporting to", folder_path, output_name) - export_blueprints = getattr(addon_prefs,"export_blueprints") - export_blueprints_path = os.path.join(folder_path, getattr(addon_prefs,"export_blueprints_path")) if getattr(addon_prefs,"export_blueprints_path") != '' else folder_path - - # backup current active scene - old_current_scene = bpy.context.scene - - if export_blueprints : - print("-----EXPORTING BLUEPRINTS----") - print("LIBRARY EXPORT", export_blueprints_path ) - - try: - #gltf_output_path = os.path.join(folder_path, "library") - #export_gltf(gltf_output_path, export_settings) - export_used_collections(scene, export_blueprints_path, gltf_export_preferences) - except Exception: - print("failed to export collections to gltf") - - (hollow_scene, object_names) = generate_hollow_scene(scene) - #except Exception: - # print("failed to create hollow scene") - - # set active scene to be the given scene - bpy.context.window.scene = hollow_scene - - gltf_output_path = os.path.join(folder_path, output_name) - - export_settings = { **gltf_export_preferences, - 'use_active_scene': True, - 'use_active_collection':True, - 'use_active_collection_with_nested':True, - 'use_visible': False, - 'use_renderable': False, - 'export_apply':True - } - export_gltf(gltf_output_path, export_settings) - - if export_blueprints : - clear_hollow_scene(hollow_scene, scene, object_names) - - # reset current scene from backup - bpy.context.window.scene = old_current_scene - -def auto_export(): - file_path = bpy.data.filepath - # Get the folder - folder_path = os.path.dirname(file_path) - addon_prefs = bpy.context.preferences.addons[__name__].preferences - - """ - print("folder", folder_path) - scn_col = bpy.context.scene.collection - - print("scene", scn_col) - print("scenes", library_scene, game_scene) - - library_root_collection = library_scene.collection - library_base_collections_lookup = get_collection_hierarchy(library_root_collection, 3) - print("lib root collection", library_root_collection) - print("all collections", library_base_collections_lookup) - """ - - """ - lkp_col = get_collection_hierarchy(scn_col, levels=3) - prt_col = {i : k for k, v in lkp_col.items() for i in v} - scn_obj = [o for o in scn_col.objects] - candidates = [x for v in lkp_col.values() for x in v] - print("scn_col", scn_col) - - print("lkp_col", lkp_col) - print("prt_col", prt_col) - print("scene objects", scn_obj) - print("candidate", candidates) - """ - - print("-------------") - - gltf_export_preferences = dict( - export_format= 'GLB', #'GLB', 'GLTF_SEPARATE', 'GLTF_EMBEDDED' - check_existing=False, - - use_selection=False, - use_visible=True, # Export visible and hidden objects. See Object/Batch Export to skip. - use_renderable=False, - use_active_collection= False, - use_active_collection_with_nested=False, - use_active_scene = False, - - export_texcoords=True, - export_normals=True, - # here add draco settings - export_draco_mesh_compression_enable = False, - - export_tangents=False, - #export_materials - export_colors=True, - export_attributes=True, - #use_mesh_edges - #use_mesh_vertices - export_cameras=True, - export_extras=True, # For custom exported properties. - export_lights=True, - export_yup=True, - export_skins=True, - export_morph=False, - export_apply=False, - export_animations=False - ) - - for key in addon_prefs.__annotations__.keys(): - if key != "export_on_library_changes" and key != "export_main_scene_name" and key != "export_main_output_name" and key != "export_library_scene_name" and key != "export_blueprints" and key != "export_blueprints_path": #FIXME: ugh, cleanup - gltf_export_preferences[key] = getattr(addon_prefs,key) - print("overriding setting", key, "value", getattr(addon_prefs,key)) - - # (we want an in-memory scene, not one that is visible in the ui) - #invisible_scene = bpy.types.Scene("foo") - - - # export the library - #export_library_split(library_scene, folder_path) - #export_library_merged(library_scene, folder_path, gltf_export_preferences) - - # export the main game world - # export_main(game_scene, folder_path, gltf_export_preferences) - - export_main_scene_name = getattr(addon_prefs,"export_main_scene_name") - export_main_output_name = getattr(addon_prefs,"export_main_output_name") - export_on_library_changes = getattr(addon_prefs,"export_on_library_changes") - export_library_scene_name = getattr(addon_prefs,"export_library_scene_name") - - # print("ADD ON PARAMS FOR EXPORT ??", export_on_library_changes, export_main_scene_name, export_main_output_name, export_blueprints) - print("last changed", bpy.context.scene.changedScene) - # optimised variation - last_changed = bpy.context.scene.changedScene #get_changedScene() - if last_changed == export_main_scene_name: - # export the main game world - game_scene = bpy.data.scenes[export_main_scene_name] - - print("game world changed, exporting game gltf only") - export_main(game_scene, folder_path, gltf_export_preferences, export_main_output_name, addon_prefs) - if last_changed == export_library_scene_name and export_library_scene_name != "" : # if the library has changed, so will likely the game world that uses the library assets - print("library changed, exporting both game & library gltf") - library_scene = bpy.data.scenes[export_library_scene_name] - - # export the library - # export_library_merged(library_scene, folder_path, gltf_export_preferences) - # export the main game world - if export_on_library_changes: - game_scene = bpy.data.scenes[export_main_scene_name] - export_main(game_scene, folder_path, gltf_export_preferences, export_main_output_name, addon_prefs) - - return {'FINISHED'} - - - -@persistent -def save_handler(dummy): - print("-------------") - print("saved", bpy.data.filepath) - auto_export() - - -def get_changedScene(self): - return self["changedScene"] - - -def set_ChangedScene(self, value): - self["changedScene"] = value - -class AutoExportGltfAddonPreferences(AddonPreferences): - # this must match the add-on name, use '__package__' - # when defining this in a submodule of a python package. - bl_idname = __name__ - ui_tab: EnumProperty( - items=(('GENERAL', "General", "General settings"), - ('MESHES', "Meshes", "Mesh settings"), - ('OBJECTS', "Objects", "Object settings"), - ('ANIMATION', "Animation", "Animation settings")), - name="ui_tab", - description="Export setting categories", - ) - export_format: EnumProperty( - name='Format', - items=(('GLB', 'glTF Binary (.glb)', - 'Exports a single file, with all data packed in binary form. ' - 'Most efficient and portable, but more difficult to edit later'), - ('GLTF_EMBEDDED', 'glTF Embedded (.gltf)', - 'Exports a single file, with all data packed in JSON. ' - 'Less efficient than binary, but easier to edit later'), - ('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)', - 'Exports multiple files, with separate JSON, binary and texture data. ' - 'Easiest to edit later')), - description=( - 'Output format and embedding options. Binary is most efficient, ' - 'but JSON (embedded or separate) may be easier to edit later' - ), - default='GLB' - ) - - - export_main_scene_name: StringProperty( - name='Main scene', - description='The name of the main scene/level/world to auto export', - default='Scene' - ) - export_main_output_name: StringProperty( - name='Glb output name', - description='The glb output name for the main scene to auto export', - default='world' - ) - export_on_library_changes: BoolProperty( - name='Export on library changes', - description='Export main scene on library changes', - default=False - ) - export_library_scene_name: StringProperty( - name='Library scene', - description='The name of the library scene to auto export', - default='' - ) - - # blueprint settings - export_blueprints: BoolProperty( - name='Export Blueprints', - description='Replaces collection instances with an Empty with a BlueprintName custom property', - default=False - ) - - export_blueprints_path: StringProperty( - name='Export Blueprints path', - description='path to export the blueprints to', - default='' - ) - - - ##### - export_copyright: StringProperty( - name='Copyright', - description='Legal rights and conditions for the model', - default='' - ) - - export_image_format: EnumProperty( - name='Images', - items=(('AUTO', 'Automatic', - 'Save PNGs as PNGs and JPEGs as JPEGs. ' - 'If neither one, use PNG'), - ('JPEG', 'JPEG Format (.jpg)', - 'Save images as JPEGs. (Images that need alpha are saved as PNGs though.) ' - 'Be aware of a possible loss in quality'), - ('NONE', 'None', - 'Don\'t export images'), - ), - description=( - 'Output format for images. PNG is lossless and generally preferred, but JPEG might be preferable for web ' - 'applications due to the smaller file size. Alternatively they can be omitted if they are not needed' - ), - default='AUTO' - ) - - export_texture_dir: StringProperty( - name='Textures', - description='Folder to place texture files in. Relative to the .gltf file', - default='', - ) - - """ - export_jpeg_quality: IntProperty( - name='JPEG quality', - description='Quality of JPEG export', - default=75, - min=0, - max=100 - ) - """ - - export_keep_originals: BoolProperty( - name='Keep original', - description=('Keep original textures files if possible. ' - 'WARNING: if you use more than one texture, ' - 'where pbr standard requires only one, only one texture will be used. ' - 'This can lead to unexpected results' - ), - default=False, - ) - - export_texcoords: BoolProperty( - name='UVs', - description='Export UVs (texture coordinates) with meshes', - default=True - ) - - export_normals: BoolProperty( - name='Normals', - description='Export vertex normals with meshes', - default=True - ) - - export_draco_mesh_compression_enable: BoolProperty( - name='Draco mesh compression', - description='Compress mesh using Draco', - default=False - ) - - export_draco_mesh_compression_level: IntProperty( - name='Compression level', - description='Compression level (0 = most speed, 6 = most compression, higher values currently not supported)', - default=6, - min=0, - max=10 - ) - - export_draco_position_quantization: IntProperty( - name='Position quantization bits', - description='Quantization bits for position values (0 = no quantization)', - default=14, - min=0, - max=30 - ) - - export_draco_normal_quantization: IntProperty( - name='Normal quantization bits', - description='Quantization bits for normal values (0 = no quantization)', - default=10, - min=0, - max=30 - ) - - export_draco_texcoord_quantization: IntProperty( - name='Texcoord quantization bits', - description='Quantization bits for texture coordinate values (0 = no quantization)', - default=12, - min=0, - max=30 - ) - - export_draco_color_quantization: IntProperty( - name='Color quantization bits', - description='Quantization bits for color values (0 = no quantization)', - default=10, - min=0, - max=30 - ) - - export_draco_generic_quantization: IntProperty( - name='Generic quantization bits', - description='Quantization bits for generic coordinate values like weights or joints (0 = no quantization)', - default=12, - min=0, - max=30 - ) - - export_tangents: BoolProperty( - name='Tangents', - description='Export vertex tangents with meshes', - default=False - ) - - export_materials: EnumProperty( - name='Materials', - items=(('EXPORT', 'Export', - 'Export all materials used by included objects'), - ('PLACEHOLDER', 'Placeholder', - 'Do not export materials, but write multiple primitive groups per mesh, keeping material slot information'), - ('NONE', 'No export', - 'Do not export materials, and combine mesh primitive groups, losing material slot information')), - description='Export materials', - default='EXPORT' - ) - - export_original_specular: BoolProperty( - name='Export original PBR Specular', - description=( - 'Export original glTF PBR Specular, instead of Blender Principled Shader Specular' - ), - default=False, - ) - - export_colors: BoolProperty( - name='Vertex Colors', - description='Export vertex colors with meshes', - default=True - ) - - export_attributes: BoolProperty( - name='Attributes', - description='Export Attributes (when starting with underscore)', - default=False - ) - - use_mesh_edges: BoolProperty( - name='Loose Edges', - description=( - 'Export loose edges as lines, using the material from the first material slot' - ), - default=False, - ) - - use_mesh_vertices: BoolProperty( - name='Loose Points', - description=( - 'Export loose points as glTF points, using the material from the first material slot' - ), - default=False, - ) - - export_cameras: BoolProperty( - name='Cameras', - description='Export cameras', - default=True - ) - - use_selection: BoolProperty( - name='Selected Objects', - description='Export selected objects only', - default=False - ) - - use_visible: BoolProperty( - name='Visible Objects', - description='Export visible objects only', - default=True - ) - - use_renderable: BoolProperty( - name='Renderable Objects', - description='Export renderable objects only', - default=False - ) - - - export_apply: BoolProperty( - name='Export Apply Modifiers', - description='Apply modifiers (excluding Armatures) to mesh objects -' - 'WARNING: prevents exporting shape keys', - default=True - ) - - export_yup: BoolProperty( - name='+Y Up', - description='Export using glTF convention, +Y up', - default=True - ) - - use_visible: BoolProperty( - name='Visible Objects', - description='Export visible objects only', - default=False - ) - - use_renderable: BoolProperty( - name='Renderable Objects', - description='Export renderable objects only', - default=False - ) - - export_extras: BoolProperty( - name='Custom Properties', - description='Export custom properties as glTF extras', - default=True - ) - - export_animations: BoolProperty( - name='Animations', - description='Exports active actions and NLA tracks as glTF animations', - default=False - ) - - - - - -class AutoExportGLTF2(Operator, ExportHelper): - """test""" - bl_idname = "export_scenes.auto_gltf" - bl_label = "Apply settings" - bl_options = {'PRESET', 'UNDO'} - - # ExportHelper mixin class uses this - filename_ext = '' - - filter_glob: StringProperty( - default='*.glb;*.gltf', - options={'HIDDEN'} - ) - - # List of operator properties, the attributes will be assigned - # to the class instance from the operator setting before calling. - - def draw(self, context): - pass - layout = self.layout - preferences = context.preferences - addon_prefs = preferences.addons[__name__].preferences - - # print("KEYS", dir(addon_prefs)) - #print("BLAS", addon_prefs.__annotations__) - #print(addon_prefs.__dict__) - for key in addon_prefs.__annotations__.keys(): - layout.prop(addon_prefs, key) - #print("key", key) - - - #def __init__(self): - # print("initializing my magic foo") - - def execute(self, context): - preferences = context.preferences - - #print("preferences", preferences.addons, __name__) - addon_prefs = preferences.addons[__name__].preferences - - #print("addon prefs", addon_prefs) - #info = ("Path: %s, Number: %d, Boolean %r" % - # (addon_prefs.filepath, addon_prefs.number, addon_prefs.boolean)) - - #self.report({'INFO'}, info) - #print(info) - - print("CHGANGED color", self) - return {'FINISHED'} - - - -# Only needed if you want to add into a dynamic menu -def menu_func_import(self, context): - self.layout.operator(AutoExportGLTF2.bl_idname, text="glTF auto Export (.glb/gltf)") - - -class GLTF_PT_export_main(bpy.types.Panel): - bl_space_type = 'FILE_BROWSER' - bl_region_type = 'TOOL_PROPS' - bl_label = "" - bl_parent_id = "FILE_PT_operator" - bl_options = {'HIDE_HEADER'} - - @classmethod - def poll(cls, context): - sfile = context.space_data - operator = sfile.active_operator - - return operator.bl_idname == "EXPORT_SCENE_OT_gltf" - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False # No animation. - - sfile = context.space_data - operator = sfile.active_operator - - layout.prop(operator, 'export_format') - if operator.export_format == 'GLTF_SEPARATE': - layout.prop(operator, 'export_keep_originals') - if operator.export_keep_originals is False: - layout.prop(operator, 'export_texture_dir', icon='FILE_FOLDER') - - layout.prop(operator, 'export_copyright') - layout.prop(operator, 'will_save_settings') - - -class GLTF_PT_export_transform(bpy.types.Panel): - bl_space_type = 'FILE_BROWSER' - bl_region_type = 'TOOL_PROPS' - bl_label = "Transform" - bl_parent_id = "FILE_PT_operator" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - sfile = context.space_data - operator = sfile.active_operator - - return operator.bl_idname == "EXPORT_SCENE_OT_gltf" - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False # No animation. - - sfile = context.space_data - operator = sfile.active_operator - - layout.prop(operator, 'export_yup') - -class GLTF_PT_export_include(bpy.types.Panel): - bl_space_type = 'FILE_BROWSER' - bl_region_type = 'TOOL_PROPS' - bl_label = "Include" - bl_parent_id = "FILE_PT_operator" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - sfile = context.space_data - operator = sfile.active_operator - - return operator.bl_idname == "EXPORT_SCENE_OT_gltf" - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False # No animation. - - sfile = context.space_data - operator = sfile.active_operator - - col = layout.column(heading = "Limit to", align = True) - col.prop(operator, 'use_selection') - col.prop(operator, 'use_visible') - col.prop(operator, 'use_renderable') - col.prop(operator, 'use_active_collection') - if operator.use_active_collection: - col.prop(operator, 'use_active_collection_with_nested') - col.prop(operator, 'use_active_scene') - - col = layout.column(heading = "Data", align = True) - col.prop(operator, 'export_extras') - col.prop(operator, 'export_cameras') - col.prop(operator, 'export_lights') - -class GLTF_PT_export_animation(bpy.types.Panel): - bl_space_type = 'FILE_BROWSER' - bl_region_type = 'TOOL_PROPS' - bl_label = "Animation" - bl_parent_id = "FILE_PT_operator" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - sfile = context.space_data - operator = sfile.active_operator - - return operator.bl_idname == "EXPORT_SCENE_OT_gltf" - - def draw_header(self, context): - sfile = context.space_data - operator = sfile.active_operator - self.layout.prop(operator, "export_animations", text="") - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False # No animation. - - sfile = context.space_data - operator = sfile.active_operator - - layout.active = operator.export_animations - - layout.prop(operator, 'export_animation_mode') - if operator.export_animation_mode == "ACTIVE_ACTIONS": - layout.prop(operator, 'export_nla_strips_merged_animation_name') - - row = layout.row() - row.active = operator.export_force_sampling and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS'] - row.prop(operator, 'export_bake_animation') - if operator.export_animation_mode == "SCENE": - layout.prop(operator, 'export_anim_scene_split_object') - -class GLTF_PT_export_animation_notes(bpy.types.Panel): - bl_space_type = 'FILE_BROWSER' - bl_region_type = 'TOOL_PROPS' - bl_label = "Notes" - bl_parent_id = "GLTF_PT_export_animation" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - sfile = context.space_data - operator = sfile.active_operator - - return operator.bl_idname == "EXPORT_SCENE_OT_gltf" and \ - operator.export_animation_mode in ["NLA_TRACKS", "SCENE"] - - def draw(self, context): - operator = context.space_data.active_operator - layout = self.layout - if operator.export_animation_mode == "SCENE": - layout.label(text="Scene mode uses full bake mode:") - layout.label(text="- sampling is active") - layout.label(text="- baking all objects is active") - layout.label(text="- Using scene frame range") - elif operator.export_animation_mode == "NLA_TRACKS": - layout.label(text="Track mode uses full bake mode:") - layout.label(text="- sampling is active") - layout.label(text="- baking all objects is active") - -classes = [ - AutoExportGLTF2, - AutoExportGltfAddonPreferences, - - # - # GLTF_PT_export_main, - # GLTF_PT_export_include, - # GLTF_PT_export_transform, - - # GLTF_PT_export_animation, - # GLTF_PT_export_animation_notes, - - - #panel1 - -] - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - #bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=My_Settings) - #CollectionProperty - - bpy.types.TOPBAR_MT_file_export.append(menu_func_import) - - bpy.app.handlers.depsgraph_update_post.append(deps_update_handler) - bpy.app.handlers.save_post.append(save_handler) - - bpy.types.Scene.changedScene = bpy.props.StringProperty(get=get_changedScene, set=set_ChangedScene) - - -def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) - - bpy.types.TOPBAR_MT_file_export.remove(menu_func_import) - - bpy.app.handlers.depsgraph_update_post.remove(deps_update_handler) - bpy.app.handlers.save_post.remove(save_handler) - #bpy.types.TOPBAR_MT_file_export.remove(menu_func_import) - del bpy.types.Scene.changedScene - #del bpy.types.Scene.my_tool - -if __name__ == "__main__": - register() \ No newline at end of file