From a630494f88e067db3fa58a619899a17f2546899c Mon Sep 17 00:00:00 2001 From: "kaosat.dev" Date: Wed, 10 Apr 2024 00:18:34 +0200 Subject: [PATCH] feat(auto_export): continued fixes & tons of restructuring * cleaned up most of the change detection tracking * updated/ fleshed out & improved the tests * improved sidebar ui (wip) * moved listing of blueprints to export to sidebar * improved display of changed objects * currently restructuring rest of the code to be able to display BEFORE saving what will get exported & what not * a ton of other cleanups & tweaks --- tools/gltf_auto_export/__init__.py | 7 +- .../auto_export/auto_export.py | 81 +------------ .../auto_export/get_collections_to_export.py | 87 ++++++++++++++ .../gltf_auto_export/auto_export/operators.py | 59 ++-------- tools/gltf_auto_export/auto_export/tracker.py | 94 ++++++++++----- .../helpers/helpers_scenes.py | 4 +- .../tests/test_change_tracking.py | 110 ++++++++++++++++-- .../tests/test_changed_parameters.py | 30 ++++- ...est_basic.py => test_export_parameters.py} | 21 +++- tools/gltf_auto_export/ui/main.py | 86 ++++++-------- 10 files changed, 355 insertions(+), 224 deletions(-) create mode 100644 tools/gltf_auto_export/auto_export/get_collections_to_export.py rename tools/gltf_auto_export/tests/{test_basic.py => test_export_parameters.py} (92%) diff --git a/tools/gltf_auto_export/__init__.py b/tools/gltf_auto_export/__init__.py index c6d6f33..3f7be6a 100644 --- a/tools/gltf_auto_export/__init__.py +++ b/tools/gltf_auto_export/__init__.py @@ -16,6 +16,7 @@ import json import bpy from bpy.types import Context from bpy.props import (StringProperty, BoolProperty, IntProperty, PointerProperty) +import rna_prop_ui # from .extension import ExampleExtensionProperties, GLTF_PT_UserExtensionPanel, unregister_panel @@ -29,7 +30,7 @@ from .auto_export.internals import (SceneLink, CollectionsToExport, CUSTOM_PG_sceneName ) -from .ui.main import (GLTF_PT_auto_export_main, +from .ui.main import (GLTF_PT_auto_export_changes_list, GLTF_PT_auto_export_main, GLTF_PT_auto_export_root, GLTF_PT_auto_export_general, GLTF_PT_auto_export_scenes, @@ -112,8 +113,9 @@ classes = [ GLTF_PT_auto_export_general, GLTF_PT_auto_export_scenes, GLTF_PT_auto_export_blueprints, - GLTF_PT_auto_export_collections_list, GLTF_PT_auto_export_SidePanel, + GLTF_PT_auto_export_collections_list, + GLTF_PT_auto_export_changes_list, AutoExportTracker, ] @@ -192,6 +194,7 @@ def register(): """bpy.utils.register_class(AutoExportExtensionProperties) bpy.types.Scene.AutoExportExtensionProperties = bpy.props.PointerProperty(type=AutoExportExtensionProperties)""" + def unregister(): for cls in classes: diff --git a/tools/gltf_auto_export/auto_export/auto_export.py b/tools/gltf_auto_export/auto_export/auto_export.py index e39e320..c997cf5 100644 --- a/tools/gltf_auto_export/auto_export/auto_export.py +++ b/tools/gltf_auto_export/auto_export/auto_export.py @@ -3,12 +3,12 @@ import os import bpy import traceback +from .get_collections_to_export import get_collections_to_export + from .export_main_scenes import export_main_scene from .export_blueprints import check_if_blueprint_on_disk, check_if_blueprints_exist, export_blueprints_from_collections -from .get_standard_exporter_settings import get_standard_exporter_settings from ..helpers.helpers_scenes import (get_scenes, ) -from ..helpers.helpers_collections import (get_collections_in_library, get_exportable_collections, get_collections_per_scene, find_collection_ascendant_target_collection) from ..modules.export_materials import cleanup_materials, export_materials from ..modules.bevy_scene_components import upsert_scene_components @@ -19,6 +19,7 @@ def auto_export(changes_per_scene, changed_export_parameters, addon_prefs): print ("changed_export_parameters", changed_export_parameters) try: + # path to the current blend file file_path = bpy.data.filepath # Get the folder folder_path = os.path.dirname(file_path) @@ -34,10 +35,6 @@ def auto_export(changes_per_scene, changed_export_parameters, addon_prefs): [main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs) - # standard gltf export settings are stored differently - - standard_gltf_exporter_settings = get_standard_exporter_settings() - print("main scenes", main_scene_names, "library_scenes", library_scene_names) print("export_output_folder", export_output_folder) @@ -53,81 +50,13 @@ def auto_export(changes_per_scene, changed_export_parameters, addon_prefs): if export_blueprints: print("EXPORTING") # create parent relations for all collections - collection_parents = dict() - for collection in bpy.data.collections: - collection_parents[collection.name] = None - for collection in bpy.data.collections: - for ch in collection.children: - collection_parents[ch.name] = collection.name - - # get a list of all collections actually in use - (collections, blueprint_hierarchy) = get_exportable_collections(level_scenes, library_scenes, addon_prefs) - - # 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 - export_levels_path = os.path.join(folder_path, export_output_folder) - - - gltf_extension = standard_gltf_exporter_settings.get("export_format", 'GLB') - 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 = [] - - for scene, objects in changes_per_scene.items(): - print(" changed scene", scene) - for obj_name, obj in objects.items(): - object_collections = list(obj.users_collection) if hasattr(obj, 'users_collection') else [] - object_collection_names = list(map(lambda collection: collection.name, object_collections)) - - if len(object_collection_names) > 1: - print("ERRROR for",obj_name,"objects in multiple collections not supported") - else: - object_collection_name = object_collection_names[0] if len(object_collection_names) > 0 else None - #recurse updwards until we find one of our collections (or not) - matching_collection = find_collection_ascendant_target_collection(collection_parents, collections, object_collection_name) - if matching_collection is not None: - changed_collections.append(matching_collection) - - collections_to_export = list(set(changed_collections + collections_not_on_disk)) if export_change_detection else collections - - # 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) - - - # collections that do not come from a library should not be exported as seperate blueprints - # FIMXE: logic is erroneous, needs to be changed - library_collections = get_collections_in_library(library_scenes) - collections_to_export = list(set(collections_to_export).intersection(set(library_collections))) + (collections, collections_to_export, main_scenes_to_export, library_collections, collections_per_scene, blueprint_hierarchy, export_levels_path, gltf_extension) = get_collections_to_export(folder_path, export_output_folder, changes_per_scene, changed_export_parameters, addon_prefs) # since materials export adds components we need to call this before blueprints are exported # export materials & inject materials components into relevant objects if export_materials_library: export_materials(collections, library_scenes, folder_path, addon_prefs) - - main_scenes_to_export = [scene_name for scene_name in main_scene_names if not export_change_detection or changed_export_parameters or scene_name in changes_per_scene.keys() or not check_if_blueprint_on_disk(scene_name, export_levels_path, gltf_extension)] - - bpy.context.window_manager.auto_export_tracker.exports_count = len(collections_to_export) - bpy.context.window_manager.auto_export_tracker.exports_count += len(main_scenes_to_export) - if export_materials_library: - bpy.context.window_manager.auto_export_tracker.exports_count += 1 - - print("-------------------------------") - print("collections: all:", collections) - print("collections: changed:", changed_collections) - print("collections: not found on disk:", collections_not_on_disk) - print("collections: in library:", library_collections) - print("collections: to export:", collections_to_export) - print("collections: per_scene:", collections_per_scene) - print("-------------------------------") - print("BLUEPRINTS: to export:", collections_to_export) - print("-------------------------------") - print("MAIN SCENES: to export:", main_scenes_to_export) - print("-------------------------------") - - # backup current active scene old_current_scene = bpy.context.scene # backup current selections @@ -168,8 +97,6 @@ def auto_export(changes_per_scene, changed_export_parameters, addon_prefs): for scene_name in main_scene_names: export_main_scene(bpy.data.scenes[scene_name], folder_path, addon_prefs, []) - print("we are done with all export work",bpy.context.window_manager.auto_export_tracker.change_detection_enabled) - except Exception as error: print(traceback.format_exc()) diff --git a/tools/gltf_auto_export/auto_export/get_collections_to_export.py b/tools/gltf_auto_export/auto_export/get_collections_to_export.py new file mode 100644 index 0000000..667ad63 --- /dev/null +++ b/tools/gltf_auto_export/auto_export/get_collections_to_export.py @@ -0,0 +1,87 @@ +import os +import bpy + +from .get_standard_exporter_settings import get_standard_exporter_settings +from .export_blueprints import check_if_blueprint_on_disk, check_if_blueprints_exist, export_blueprints_from_collections +from ..helpers.helpers_collections import get_exportable_collections +from ..helpers.helpers_collections import (get_collections_in_library, get_exportable_collections, get_collections_per_scene, find_collection_ascendant_target_collection) +from ..helpers.helpers_scenes import (get_scenes, ) + +def get_collections_to_export(folder_path, export_output_folder, changes_per_scene, changed_export_parameters, addon_prefs): + export_change_detection = getattr(addon_prefs, "export_change_detection") + export_materials_library = getattr(addon_prefs,"export_materials_library") + + # standard gltf export settings are stored differently + standard_gltf_exporter_settings = get_standard_exporter_settings() + [main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs) + + collection_parents = dict() + for collection in bpy.data.collections: + collection_parents[collection.name] = None + for collection in bpy.data.collections: + for ch in collection.children: + collection_parents[ch.name] = collection.name + + # get a list of all collections actually in use + (collections, blueprint_hierarchy) = get_exportable_collections(level_scenes, library_scenes, addon_prefs) + + # 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 + export_levels_path = os.path.join(folder_path, export_output_folder) + + gltf_extension = standard_gltf_exporter_settings.get("export_format", 'GLB') + 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 = [] + + for scene, objects in changes_per_scene.items(): + print(" changed scene", scene) + for obj_name, obj in objects.items(): + object_collections = list(obj.users_collection) if hasattr(obj, 'users_collection') else [] + object_collection_names = list(map(lambda collection: collection.name, object_collections)) + + if len(object_collection_names) > 1: + print("ERRROR for",obj_name,"objects in multiple collections not supported") + else: + object_collection_name = object_collection_names[0] if len(object_collection_names) > 0 else None + #recurse updwards until we find one of our collections (or not) + matching_collection = find_collection_ascendant_target_collection(collection_parents, collections, object_collection_name) + if matching_collection is not None: + changed_collections.append(matching_collection) + + collections_to_export = list(set(changed_collections + collections_not_on_disk)) if export_change_detection else collections + + # we need to re_export everything if the export parameters have been changed # TODO: perhaps do this BEFORE the rest above for better perfs + collections_to_export = collections if changed_export_parameters else collections_to_export + collections_per_scene = get_collections_per_scene(collections_to_export, library_scenes) + + # collections that do not come from a library should not be exported as seperate blueprints + # FIMXE: logic is erroneous, needs to be changed + library_collections = get_collections_in_library(library_scenes) + collections_to_export = list(set(collections_to_export).intersection(set(library_collections))) + + + main_scenes_to_export = [scene_name for scene_name in main_scene_names if not export_change_detection or changed_export_parameters or scene_name in changes_per_scene.keys() or not check_if_blueprint_on_disk(scene_name, export_levels_path, gltf_extension)] + + # update the list of tracked exports + exports_total = len(collections_to_export) + len(main_scenes_to_export) + (1 if export_materials_library else 0) + bpy.context.window_manager.auto_export_tracker.exports_total = exports_total + bpy.context.window_manager.auto_export_tracker.exports_count = exports_total + + + + print("-------------------------------") + print("collections: all:", collections) + print("collections: changed:", changed_collections) + print("collections: not found on disk:", collections_not_on_disk) + print("collections: in library:", library_collections) + print("collections: to export:", collections_to_export) + print("collections: per_scene:", collections_per_scene) + print("-------------------------------") + print("BLUEPRINTS: to export:", collections_to_export) + print("-------------------------------") + print("MAIN SCENES: to export:", main_scenes_to_export) + print("-------------------------------") + + return (collections, collections_to_export, main_scenes_to_export, library_collections, collections_per_scene, blueprint_hierarchy, export_levels_path, gltf_extension) \ No newline at end of file diff --git a/tools/gltf_auto_export/auto_export/operators.py b/tools/gltf_auto_export/auto_export/operators.py index a86b219..d5d5836 100644 --- a/tools/gltf_auto_export/auto_export/operators.py +++ b/tools/gltf_auto_export/auto_export/operators.py @@ -2,14 +2,10 @@ import json import bpy from bpy.types import Operator from bpy_extras.io_utils import ExportHelper -from bpy.props import (IntProperty, StringProperty) +from bpy.props import (IntProperty) from .preferences import (AutoExportGltfAddonPreferences, AutoExportGltfPreferenceNames) -from ..helpers.helpers_scenes import (get_scenes) -from ..helpers.helpers_collections import (get_exportable_collections) from .auto_export import auto_export -from io_scene_gltf2 import (ExportGLTF2, GLTF_PT_export_main,ExportGLTF2_Base, GLTF_PT_export_include) - class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): """auto export gltf""" #bl_idname = "object.xxx" @@ -158,7 +154,6 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): # if there were no setting before, it is new, we need export changed = False - print("previous_auto_settings", previous_auto_settings, "previous_gltf_settings", previous_gltf_settings) if previous_auto_settings == None: print("previous settings missing, exporting") changed = True @@ -167,7 +162,7 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): previous_gltf_settings = bpy.data.texts.new(".gltf_auto_export_gltf_settings_previous") previous_gltf_settings.write(json.dumps({})) if current_gltf_settings == None: - current_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] + current_gltf_settings = bpy.data.texts.new(".gltf_auto_export_gltf_settings") current_gltf_settings.write(json.dumps({})) changed = True @@ -199,65 +194,35 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): return changed def execute(self, context): - print("execute") + #print("execute") bpy.context.window_manager.auto_export_tracker.disable_change_detection() if self.direct_mode: self.load_settings(context) if self.will_save_settings: self.save_settings(context) - - changes_per_scene = context.window_manager.auto_export_tracker.changed_objects_per_scene + if self.auto_export: # only do the actual exporting if auto export is actually enabled + changes_per_scene = context.window_manager.auto_export_tracker.changed_objects_per_scene #& do the export - if self.direct_mode: #Do not auto export when applying settings in the menu, do it on save only - # disable change detection while the operator runs - + if self.direct_mode: #Do not auto export when applying settings in the menu, do it on save only #determine changed parameters params_changed = self.did_export_settings_change() auto_export(changes_per_scene, params_changed, self) - # cleanup - print("AUTO EXPORT DONE") - if bpy.context.window_manager.auto_export_tracker.exports_count == 0: # we need this in case there was nothing to export, to make sure change detection is enabled again - pass #print("YOLOOO") - #py.context.window_manager.auto_export_tracker.enable_change_detection() - #bpy.app.timers.register(bpy.context.window_manager.auto_export_tracker.enable_change_detection, first_interval=1) - #bpy.context.window_manager.auto_export_tracker.enable_change_detection() - # FIXME: wrong logic, this should be called only in an glTF2_post_export_callback - #bpy.app.timers.register(bpy.context.window_manager.auto_export_tracker.enable_change_detection, first_interval=1) + # cleanup + # reset the list of changes in the tracker + bpy.context.window_manager.auto_export_tracker.clear_changes() + print("AUTO EXPORT DONE") + bpy.app.timers.register(bpy.context.window_manager.auto_export_tracker.enable_change_detection, first_interval=0.1) else: print("auto export disabled, skipping") - - """if not self.direct_mode: - print("enabling") - bpy.context.window_manager.auto_export_tracker.enable_change_detection()""" - bpy.app.timers.register(bpy.context.window_manager.auto_export_tracker.enable_change_detection, first_interval=1) - return {'FINISHED'} def invoke(self, context, event): - print("invoke") + #print("invoke") bpy.context.window_manager.auto_export_tracker.disable_change_detection() self.load_settings(context) - - addon_prefs = self - """[main_scene_names, level_scenes, library_scene_names, library_scenes]=get_scenes(addon_prefs) - (collections, _) = get_exportable_collections(level_scenes, library_scenes, addon_prefs) - - 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'} def draw(self, context): diff --git a/tools/gltf_auto_export/auto_export/tracker.py b/tools/gltf_auto_export/auto_export/tracker.py index ebd54c1..4fd38d0 100644 --- a/tools/gltf_auto_export/auto_export/tracker.py +++ b/tools/gltf_auto_export/auto_export/tracker.py @@ -1,10 +1,13 @@ import json +from types import SimpleNamespace import bpy from bpy.types import (PropertyGroup) from bpy.props import (PointerProperty, IntProperty, StringProperty) from ..constants import TEMPSCENE_PREFIX from .internals import CollectionsToExport +from ..helpers.helpers_scenes import (get_scenes) +from ..helpers.helpers_collections import (get_exportable_collections) class AutoExportTracker(PropertyGroup): @@ -16,14 +19,18 @@ class AutoExportTracker(PropertyGroup): last_operator = None dummy_file_path = "" + exports_total : IntProperty( + name='exports_total', + description='Number of total exports', + default=0 + ) # type: ignore + exports_count : IntProperty( name='exports_count', description='Number of exports in progress', default=0 ) # type: ignore - - @classmethod def register(cls): bpy.types.WindowManager.auto_export_tracker = PointerProperty(type=AutoExportTracker) @@ -62,15 +69,15 @@ class AutoExportTracker(PropertyGroup): @classmethod def deps_update_handler(cls, scene, depsgraph): - print("change detection enabled", cls.change_detection_enabled) + # print("change detection enabled", cls.change_detection_enabled) - ops = bpy.context.window_manager.operators + """ops = bpy.context.window_manager.operators print("last operators", ops) for op in ops: - print("operator", op) + print("operator", op)""" active_operator = bpy.context.active_operator if active_operator: - print("Operator", active_operator.bl_label, active_operator.bl_idname) + #print("Operator", active_operator.bl_label, active_operator.bl_idname) if active_operator.bl_idname == "EXPORT_SCENE_OT_gltf" and active_operator.gltf_export_id == "gltf_auto_export": # we backup any existing gltf export settings, if there were any scene = bpy.context.scene @@ -87,30 +94,28 @@ class AutoExportTracker(PropertyGroup): # we force saving params active_operator.will_save_settings = True active_operator.auto_export = True + print("setting stuff for auto_export") # only deal with changes if we are NOT in the mids of saving/exporting if cls.change_detection_enabled: # ignore anything going on with temporary scenes if not scene.name.startswith(TEMPSCENE_PREFIX): - print("depsgraph_update_post", scene.name) + # print("depsgraph_update_post", scene.name) changed_scene = scene.name or "" - - #print("-------------") if not changed_scene in cls.changed_objects_per_scene: cls.changed_objects_per_scene[changed_scene] = {} - print("cls.changed_objects_per_scene", cls.changed_objects_per_scene) - depsgraph = bpy.context.evaluated_depsgraph_get() + # print("cls.changed_objects_per_scene", cls.changed_objects_per_scene) + # depsgraph = bpy.context.evaluated_depsgraph_get() for obj in depsgraph.updates: - print("depsgraph update", obj) + #print("depsgraph update", obj) if isinstance(obj.id, bpy.types.Object): # get the actual object object = bpy.data.objects[obj.id.name] - print("changed object", obj.id.name) - print("FOO","transforms", obj.is_updated_transform, "geometry", obj.is_updated_geometry) + # print(" changed object", obj.id.name,"transforms", obj.is_updated_transform, "geometry", obj.is_updated_geometry) cls.changed_objects_per_scene[scene.name][obj.id.name] = object elif isinstance(obj.id, bpy.types.Material): # or isinstance(obj.id, bpy.types.ShaderNodeTree): - print("changed material", obj.id, "scene", scene.name,) + # print(" changed material", obj.id, "scene", scene.name,) material = bpy.data.materials[obj.id.name] #now find which objects are using the material for obj in bpy.data.objects: @@ -123,37 +128,68 @@ class AutoExportTracker(PropertyGroup): items += len(cls.changed_objects_per_scene[scene_name].keys()) if items == 0: cls.changed_objects_per_scene.clear() - print("changed_objects_per_scene", cls.changed_objects_per_scene) + # print("changed_objects_per_scene", cls.changed_objects_per_scene) else: cls.changed_objects_per_scene.clear() + + + # get a list of exportable collections for display + # keep it simple, just use Simplenamespace for compatibility with the rest of our code + addon_prefs = SimpleNamespace(**get_auto_exporter_settings()) + print("addon prefs", addon_prefs) + addon_prefs.export_marked_assets = True + [_, level_scenes, _, library_scenes] = get_scenes(addon_prefs) + (collections, _) = get_exportable_collections(level_scenes, library_scenes, addon_prefs) + 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: + pass + #self.report({"ERROR"}, "Failed to populate list of exported collections/blueprints") """depsgraph = bpy.context.evaluated_depsgraph_get() for update in depsgraph.updates: print("update", update)""" def disable_change_detection(self): - print("disable change detection") + #print("disable change detection") self.change_detection_enabled = False self.__class__.change_detection_enabled = False return None def enable_change_detection(self): - print("enable change detection") + #print("enable change detection") self.change_detection_enabled = True self.__class__.change_detection_enabled = True - #FIXME: not sure about these + #print("bpy.context.window_manager.auto_export_tracker.change_detection_enabled", bpy.context.window_manager.auto_export_tracker.change_detection_enabled) + return None + + def clear_changes(self): self.changed_objects_per_scene.clear() self.__class__.changed_objects_per_scene.clear() - # bpy.context.window_manager.auto_export_tracker.change_detection_enabled = True - print("bpy.context.window_manager.auto_export_tracker.change_detection_enabled", bpy.context.window_manager.auto_export_tracker.change_detection_enabled) - return None def export_finished(self): - print("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAHHHHHHHH export_finished") - bpy.context.window_manager.auto_export_tracker.exports_count -= 1 - if bpy.context.window_manager.auto_export_tracker.exports_count == 0: - #print("preparing to reset change detection") - # bpy.app.timers.register(bpy.context.window_manager.auto_export_tracker.enable_change_detection, first_interval=1) - - self.enable_change_detection() + #print("export_finished") + self.exports_count -= 1 + if self.exports_count == 0: + print("preparing to reset change detection") + bpy.app.timers.register(self.enable_change_detection, first_interval=0.1) + #self.enable_change_detection() return None + + +def get_auto_exporter_settings(): + auto_exporter_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else None + if auto_exporter_settings != None: + try: + auto_exporter_settings = json.loads(auto_exporter_settings.as_string()) + except: + auto_exporter_settings = {} + else: + auto_exporter_settings = {} + + return auto_exporter_settings \ No newline at end of file diff --git a/tools/gltf_auto_export/helpers/helpers_scenes.py b/tools/gltf_auto_export/helpers/helpers_scenes.py index 59d0015..3e6def3 100644 --- a/tools/gltf_auto_export/helpers/helpers_scenes.py +++ b/tools/gltf_auto_export/helpers/helpers_scenes.py @@ -212,8 +212,8 @@ def clear_hollow_scene(temp_scene, original_root_collection): # convenience utility to get lists of scenes def get_scenes(addon_prefs): - level_scene_names= list(map(lambda scene: scene.name, getattr(addon_prefs,"main_scenes"))) # getattr(addon_prefs, "main_scene_names_compact").split(',')# - library_scene_names = list(map(lambda scene: scene.name, getattr(addon_prefs,"library_scenes"))) #getattr(addon_prefs, "main_scene_names_compact").split(',')# + level_scene_names= getattr(addon_prefs,"main_scene_names") #list(map(lambda scene: scene.name, getattr(addon_prefs,"main_scenes"))) + library_scene_names = getattr(addon_prefs,"library_scene_names") #list(map(lambda scene: scene.name, getattr(addon_prefs,"library_scenes"))) level_scene_names = list(filter(lambda name: name in bpy.data.scenes, level_scene_names)) library_scene_names = list(filter(lambda name: name in bpy.data.scenes, library_scene_names)) diff --git a/tools/gltf_auto_export/tests/test_change_tracking.py b/tools/gltf_auto_export/tests/test_change_tracking.py index d3a4340..8953652 100644 --- a/tools/gltf_auto_export/tests/test_change_tracking.py +++ b/tools/gltf_auto_export/tests/test_change_tracking.py @@ -5,6 +5,7 @@ import mathutils import pytest import shutil import pathlib +import rna_prop_ui @pytest.fixture def setup_data(request): @@ -80,6 +81,89 @@ def setup_data(request): return None +def test_export_change_tracking_custom_properties(setup_data): + root_path = "../../testing/bevy_example" + assets_root_path = os.path.join(root_path, "assets") + models_path = os.path.join(assets_root_path, "models") + auto_export_operator = bpy.ops.export_scenes.auto_gltf + + # with change detection + # first, configure things + # we use the global settings for that + export_props = { + "main_scene_names" : ['World'], + "library_scene_names": ['Library'], + } + + # store settings for the auto_export part + stored_auto_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings") + stored_auto_settings.clear() + stored_auto_settings.write(json.dumps(export_props)) + + gltf_settings = { + "export_animations": False, + "export_optimize_animation_size": False + } + # and store settings for the gltf part + stored_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] if ".gltf_auto_export_gltf_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_gltf_settings") + stored_gltf_settings.clear() + stored_gltf_settings.write(json.dumps(gltf_settings)) + + auto_export_operator( + auto_export=True, + direct_mode=True, + export_output_folder="./models", + export_scene_settings=True, + export_blueprints=True, + export_legacy_mode=False, + export_materials_library=False + ) + + world_file_path = os.path.join(models_path, "World.glb") + assert os.path.exists(world_file_path) == True + + models_library_path = os.path.join(models_path, "library") + model_library_file_paths = list(map(lambda file_name: os.path.join(models_library_path, file_name), sorted(os.listdir(models_library_path)))) + modification_times_first = list(map(lambda file_path: os.path.getmtime(file_path), model_library_file_paths + [world_file_path])) + + mapped_files_to_timestamps_and_index = {} + for (index, file_path) in enumerate(model_library_file_paths+ [world_file_path]): + file_path = pathlib.Path(file_path).stem + mapped_files_to_timestamps_and_index[file_path] = (modification_times_first[index], index) + + # now add a custom property to the cube in the main scene & export again + print("----------------") + print("main scene change (custom property)") + print("----------------") + + bpy.context.window_manager.auto_export_tracker.enable_change_detection() # FIXME: should not be needed, but .. + bpy.data.objects["Cube"]["test_property"] = 42 + + #force an update + rna_prop_ui.rna_idprop_ui_create(bpy.data.objects["Cube"], "________temp", default=0) + rna_prop_ui.rna_idprop_ui_prop_clear(bpy.data.objects["Cube"], "________temp") + + auto_export_operator( + auto_export=True, + direct_mode=True, + export_output_folder="./models", + export_scene_settings=True, + export_blueprints=True, + export_legacy_mode=False, + export_materials_library=False + ) + + modification_times = list(map(lambda file_path: os.path.getmtime(file_path), model_library_file_paths + [world_file_path])) + assert modification_times != modification_times_first + # only the "world" file should have changed + world_file_index = mapped_files_to_timestamps_and_index["World"][1] + other_files_modification_times = [value for index, value in enumerate(modification_times) if index not in [world_file_index]] + other_files_modification_times_first = [value for index, value in enumerate(modification_times_first) if index not in [world_file_index]] + + assert modification_times[world_file_index] != modification_times_first[world_file_index] + assert other_files_modification_times == other_files_modification_times_first + # reset the comparing + modification_times_first = modification_times """ - setup gltf parameters & auto_export parameters @@ -233,7 +317,7 @@ def test_export_changed_parameters(setup_data): print("----------------") bpy.context.window_manager.auto_export_tracker.enable_change_detection() # FIXME: should not be needed, but .. - bpy.data.objects["Blueprint3_mesh"]["test_component"] = 42 + bpy.data.objects["Blueprint3_mesh"].location= [0, 0.1 ,2] auto_export_operator( auto_export=True, @@ -249,13 +333,17 @@ def test_export_changed_parameters(setup_data): assert modification_times != modification_times_first # the "world" file should have changed (TODO: double check: this is since changing an instances collection changes the instance too ?) world_file_index = mapped_files_to_timestamps_and_index["World"][1] - # and the blueprint1 file too, since that is the collection we changed - blueprint1_file_index = mapped_files_to_timestamps_and_index["Blueprint1"][1] - other_files_modification_times = [value for index, value in enumerate(modification_times) if index not in [world_file_index, blueprint1_file_index]] - other_files_modification_times_first = [value for index, value in enumerate(modification_times_first) if index not in [world_file_index, blueprint1_file_index]] + # and the blueprint3 file too, since that is the collection we changed + blueprint3_file_index = mapped_files_to_timestamps_and_index["Blueprint3"][1] + # and the blueprint4 file too, since it contains the collection we changed + blueprint4_file_index = mapped_files_to_timestamps_and_index["Blueprint4_nested"][1] + + other_files_modification_times = [value for index, value in enumerate(modification_times) if index not in [world_file_index, blueprint3_file_index, blueprint4_file_index]] + other_files_modification_times_first = [value for index, value in enumerate(modification_times_first) if index not in [world_file_index, blueprint3_file_index, blueprint4_file_index]] assert modification_times[world_file_index] != modification_times_first[world_file_index] - assert modification_times[blueprint1_file_index] != modification_times_first[blueprint1_file_index] + assert modification_times[blueprint3_file_index] != modification_times_first[blueprint3_file_index] + assert modification_times[blueprint4_file_index] != modification_times_first[blueprint4_file_index] assert other_files_modification_times == other_files_modification_times_first # reset the comparing modification_times_first = modification_times @@ -271,11 +359,15 @@ def test_export_changed_parameters(setup_data): with bpy.context.temp_override(active_object=bpy.data.objects["Cube"]): print("translate using operator") - bpy.ops.transform.translate(value=mathutils.Vector((2.0, 1.0, -5.0))) - bpy.ops.transform.rotate(value=0.378874, constraint_axis=(False, False, True), mirror=False, proportional_edit_falloff='SMOOTH', proportional_size=1) - bpy.ops.object.transform_apply() + bpy.ops.transform.translate(value=mathutils.Vector((2.0, 1.0, -5.0))) + bpy.ops.transform.rotate(value=0.378874, constraint_axis=(False, False, True), mirror=False, proportional_edit_falloff='SMOOTH', proportional_size=1) + bpy.ops.object.transform_apply() bpy.ops.transform.translate(value=(0.5, 0, 0), constraint_axis=(True, False, False)) + #force an update, as apparently all the operators above do not trigger changes ??? + rna_prop_ui.rna_idprop_ui_create(bpy.data.objects["Cube"], "________temp", default=0) + rna_prop_ui.rna_idprop_ui_prop_clear(bpy.data.objects["Cube"], "________temp") + auto_export_operator( auto_export=True, diff --git a/tools/gltf_auto_export/tests/test_changed_parameters.py b/tools/gltf_auto_export/tests/test_changed_parameters.py index 8c5096d..9060fef 100644 --- a/tools/gltf_auto_export/tests/test_changed_parameters.py +++ b/tools/gltf_auto_export/tests/test_changed_parameters.py @@ -57,7 +57,7 @@ def test_export_no_parameters(setup_data): auto_export_operator = bpy.ops.export_scenes.auto_gltf - # first test exporting withouth any parameters set, this should export with default parameters gracefully + # first test exporting withouth any parameters set, this should not export anything auto_export_operator( auto_export=True, @@ -67,7 +67,35 @@ def test_export_no_parameters(setup_data): export_materials_library=True ) + world_file_path = os.path.join(models_path, "World.glb") + assert os.path.exists(world_file_path) != True +def test_export_auto_export_parameters_only(setup_data): + root_path = "../../testing/bevy_example" + assets_root_path = os.path.join(root_path, "assets") + models_path = os.path.join(assets_root_path, "models") + auto_export_operator = bpy.ops.export_scenes.auto_gltf + + export_props = { + "main_scene_names" : ['World'], + "library_scene_names": ['Library'], + } + + # store settings for the auto_export part + stored_auto_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings") + stored_auto_settings.clear() + stored_auto_settings.write(json.dumps(export_props)) + + auto_export_operator( + auto_export=True, + direct_mode=True, + export_output_folder="./models", + export_legacy_mode=False, + export_materials_library=True + ) + + world_file_path = os.path.join(models_path, "World.glb") + assert os.path.exists(world_file_path) == True def test_export_changed_parameters(setup_data): root_path = "../../testing/bevy_example" diff --git a/tools/gltf_auto_export/tests/test_basic.py b/tools/gltf_auto_export/tests/test_export_parameters.py similarity index 92% rename from tools/gltf_auto_export/tests/test_basic.py rename to tools/gltf_auto_export/tests/test_export_parameters.py index 8d3c1e5..793c075 100644 --- a/tools/gltf_auto_export/tests/test_basic.py +++ b/tools/gltf_auto_export/tests/test_export_parameters.py @@ -51,11 +51,12 @@ def test_export_do_not_export_blueprints(setup_data): "main_scene_names" : ['World'], "library_scene_names": ['Library'] } - stored_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings") - stored_settings.clear() - stored_settings.write(json.dumps(export_props)) + stored_auto_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings") + stored_auto_settings.clear() + stored_auto_settings.write(json.dumps(export_props)) auto_export_operator( + auto_export=True, direct_mode=True, export_output_folder="./models", export_scene_settings=True, @@ -75,11 +76,13 @@ def test_export_custom_blueprints_path(setup_data): "main_scene_names" : ['World'], "library_scene_names": ['Library'] } - stored_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings") - stored_settings.clear() - stored_settings.write(json.dumps(export_props)) + + stored_auto_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings") + stored_auto_settings.clear() + stored_auto_settings.write(json.dumps(export_props)) auto_export_operator( + auto_export=True, direct_mode=True, export_output_folder="./models", export_scene_settings=True, @@ -104,6 +107,7 @@ def test_export_materials_library(setup_data): stored_settings.write(json.dumps(export_props)) auto_export_operator( + auto_export=True, direct_mode=True, export_output_folder="./models", export_scene_settings=True, @@ -129,6 +133,7 @@ def test_export_materials_library_custom_path(setup_data): stored_settings.write(json.dumps(export_props)) auto_export_operator( + auto_export=True, direct_mode=True, export_output_folder="./models", export_scene_settings=True, @@ -159,6 +164,7 @@ def test_export_collection_instances_combine_mode(setup_data): # TODO: change & bpy.data.objects["Cube"]["dynamic"] = True auto_export_operator( + auto_export=True, direct_mode=True, export_output_folder="./models", export_blueprints=True, @@ -184,6 +190,7 @@ def test_export_do_not_export_marked_assets(setup_data): stored_settings.write(json.dumps(export_props)) auto_export_operator( + auto_export=True, direct_mode=True, export_output_folder="./models", export_scene_settings=True, @@ -216,6 +223,7 @@ def test_export_separate_dynamic_and_static_objects(setup_data): bpy.data.objects["Cube"]["dynamic"] = True auto_export_operator( + auto_export=True, direct_mode=True, export_output_folder="./models", export_scene_settings=True, @@ -242,6 +250,7 @@ def test_export_should_not_generate_orphan_data(setup_data): stored_settings.write(json.dumps(export_props)) auto_export_operator( + auto_export=True, direct_mode=True, export_output_folder="./models", export_scene_settings=True, diff --git a/tools/gltf_auto_export/ui/main.py b/tools/gltf_auto_export/ui/main.py index da76583..f27c4fd 100644 --- a/tools/gltf_auto_export/ui/main.py +++ b/tools/gltf_auto_export/ui/main.py @@ -1,19 +1,5 @@ from typing import Set import bpy -from bpy.types import Context, Event, Operator -from bpy_extras.io_utils import ExportHelper -from bpy.props import (BoolProperty, - IntProperty, - StringProperty, - EnumProperty, - CollectionProperty - ) - -from ..auto_export import auto_export - -from ..auto_export.preferences import (AutoExportGltfAddonPreferences, AutoExportGltfPreferenceNames) -from ..helpers.helpers_scenes import (get_scenes) -from ..helpers.helpers_collections import (get_exportable_collections) ###################################################### ## ui logic & co @@ -49,15 +35,41 @@ class GLTF_PT_auto_export_SidePanel(bpy.types.Panel): op = layout.operator("EXPORT_SCENES_OT_auto_gltf", text="Auto Export Settings") op.auto_export = True - layout.label(text="changes since last save:") - changed_objects_per_scene = {} - for scene in context.window_manager.auto_export_tracker.changed_objects_per_scene: - if not scene in changed_objects_per_scene.keys(): - changed_objects_per_scene[scene] = [] - changed_objects_per_scene[scene]+= context.window_manager.auto_export_tracker.changed_objects_per_scene[scene].keys() - - layout.label(text=str(changed_objects_per_scene)) - #print("GLTF_PT_export_main", GLTF_PT_export_main.bl_parent_id) +class GLTF_PT_auto_export_changes_list(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Changes per scene since last save " + bl_parent_id = "GLTF_PT_auto_export_SidePanel" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + #if "auto_export_tracker" in context.window_manager: + changed_objects_per_scene = context.window_manager.auto_export_tracker.changed_objects_per_scene + for scene_name in changed_objects_per_scene: + layout.label(text=f'{scene_name}') + for object_name in list(changed_objects_per_scene[scene_name].keys()): + row = layout.row() + row.label(text=f' {object_name}') + +class GLTF_PT_auto_export_collections_list(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Blueprints to export" + bl_parent_id = "GLTF_PT_auto_export_SidePanel" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + for collection in bpy.context.window_manager.exportedCollections: + row = layout.row() + row.label(text=collection.name) # main ui in the file => export class GLTF_PT_auto_export_main(bpy.types.Panel): @@ -260,34 +272,6 @@ class GLTF_PT_auto_export_blueprints(bpy.types.Panel): # materials layout.prop(operator, "export_materials_library") layout.prop(operator, "export_materials_path") - - -class GLTF_PT_auto_export_collections_list(bpy.types.Panel): - bl_space_type = 'FILE_BROWSER' - bl_region_type = 'TOOL_PROPS' - bl_label = "Blueprints: Exported Collections" - bl_parent_id = "GLTF_PT_auto_export_blueprints" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - sfile = context.space_data - operator = sfile.active_operator - - return operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf" #"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.active = operator.auto_export and operator.export_blueprints - - for collection in bpy.context.window_manager.exportedCollections: - row = layout.row() - row.label(text=collection.name) class SCENE_UL_GLTF_auto_export(bpy.types.UIList): # The draw_item function is called for each item of the collection that is visible in the list.