feat(auto_export): fixes, improvements etc etc

* fixed issues with renaming after export (should have always been broken ???)
 * fleshed out scene serialisation some more (parents & collections handling) + bubbling
 * disabled bulk of tracker
 * added notes for  get collections & scenes about combine mode
 * fixed some issues with main operator
 * updated tests for more correctness
 * etc
This commit is contained in:
kaosat.dev 2024-04-15 15:07:25 +02:00
parent 2f54bea7c9
commit db1a15ec63
10 changed files with 99 additions and 52 deletions

View File

@ -213,7 +213,6 @@ def register():
"""bpy.utils.register_class(AutoExportExtensionProperties)
bpy.types.Scene.AutoExportExtensionProperties = bpy.props.PointerProperty(type=AutoExportExtensionProperties)"""
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)

View File

@ -83,7 +83,7 @@ def auto_export(changes_per_scene, changed_export_parameters, addon_prefs):
print("EXPORTING")
# get blueprints/collections infos
(collections, collections_to_export, library_collections, collections_per_scene) = get_collections_to_export(changes_per_scene, changed_export_parameters, addon_prefs)
# get level/main scenes infos
(main_scenes_to_export) = get_levels_to_export(changes_per_scene, changed_export_parameters, addon_prefs)

View File

@ -6,6 +6,7 @@ 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, )
# TODO: this should also take the split/embed mode into account: if a nested collection changes AND embed is active, its container collection should also be exported
def get_collections_to_export(changes_per_scene, changed_export_parameters, addon_prefs):
export_change_detection = getattr(addon_prefs, "export_change_detection")
export_gltf_extension = getattr(addon_prefs, "export_gltf_extension", ".glb")
@ -16,7 +17,7 @@ def get_collections_to_export(changes_per_scene, changed_export_parameters, addo
(collections, blueprint_hierarchy) = get_exportable_collections(level_scenes, library_scenes, addon_prefs)
collections_to_export = collections # just for clarity
print("export_change_detection", export_change_detection, "changed_export_parameters", changed_export_parameters, "changes_per_scene", changes_per_scene)
# print("export_change_detection", export_change_detection, "changed_export_parameters", changed_export_parameters, "changes_per_scene", changes_per_scene)
# if the export parameters have changed, bail out early
# we need to re_export everything if the export parameters have been changed

View File

@ -2,6 +2,7 @@ import bpy
from .export_blueprints import check_if_blueprint_on_disk
from ..helpers.helpers_scenes import (get_scenes, )
# TODO: this should also take the split/embed mode into account: if a collection instance changes AND embed is active, its container level/world should also be exported
def get_levels_to_export(changes_per_scene, changed_export_parameters, addon_prefs):
export_change_detection = getattr(addon_prefs, "export_change_detection")
export_gltf_extension = getattr(addon_prefs, "export_gltf_extension")
@ -9,6 +10,7 @@ def get_levels_to_export(changes_per_scene, changed_export_parameters, addon_pre
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
# print("levels export", "export_change_detection", export_change_detection, "changed_export_parameters",changed_export_parameters, "export_models_path", export_models_path, "export_gltf_extension", export_gltf_extension, "changes_per_scene", changes_per_scene)
# determine list of main scenes to export
# we have more relaxed rules to determine if the main scenes have changed : any change is ok, (allows easier handling of changes, render settings etc)
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_models_path, export_gltf_extension)]

View File

@ -10,12 +10,18 @@ from .auto_export import auto_export
from ..helpers.generate_complete_preferences_dict import generate_complete_preferences_dict_auto
from ..helpers.serialize_scene import serialize_scene
def bubble_up_changes(object, changes_per_scene):
if object.parent:
changes_per_scene[object.parent.name] = bpy.data.objects[object.parent.name]
bubble_up_changes(object.parent, changes_per_scene)
class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
"""auto export gltf"""
#bl_idname = "object.xxx"
bl_idname = "export_scenes.auto_gltf"
bl_label = "Apply settings"
bl_options = {'PRESET', 'UNDO'}
bl_options = {'PRESET'} # we do not add UNDO otherwise it leads to an invisible operation that resets the state of the saved serialized scene, breaking compares for normal undo/redo operations
# ExportHelper mixin class uses this
filename_ext = ''
@ -203,15 +209,28 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
return changed
def did_objects_change(self):
previous_stored = bpy.data.texts[".TESTING"] if ".TESTING" in bpy.data.texts else None # bpy.data.texts.new(".TESTING")
# sigh... you need to save & reset the frame otherwise it saves the values AT THE CURRENT FRAME WHICH CAN DIFFER ACROSS SCENES
current_frames = [scene.frame_current for scene in bpy.data.scenes]
for scene in bpy.data.scenes:
scene.frame_set(0)
current_scene = bpy.context.window.scene
bpy.context.window.scene = bpy.data.scenes[0]
#serialize scene at frame 0
"""with bpy.context.temp_override(scene=bpy.data.scenes[1]):
bpy.context.scene.frame_set(0)"""
current = serialize_scene()
bpy.context.window.scene = current_scene
# reset previous frames
for (index, scene) in enumerate(bpy.data.scenes):
scene.frame_set(int(current_frames[index]))
previous_stored = bpy.data.texts[".TESTING"] if ".TESTING" in bpy.data.texts else None # bpy.data.texts.new(".TESTING")
if previous_stored == None:
print("setting bla")
previous_stored = bpy.data.texts.new(".TESTING")
previous_stored.write(current)
return {}
previous = json.loads(previous_stored.as_string())
current = json.loads(current)
@ -219,7 +238,6 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
# TODO : how do we deal with changed scene names ???
for scene in current:
print('scene', scene)
changes_per_scene[scene] = {}
previous_object_names = list(previous[scene].keys())
current_object_names =list(current[scene].keys())
#print("previous_object_names", len(previous_object_names), previous_object_names)
@ -231,12 +249,16 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
print("added")"""
added = list(set(current_object_names) - set(previous_object_names))
removed = list(set(previous_object_names) - set(current_object_names))
print("removed", removed)
print("added",added)
"""print("removed", removed)
print("added",added)"""
for obj in added:
if not scene in changes_per_scene:
changes_per_scene[scene] = {}
changes_per_scene[scene][obj] = bpy.data.objects[obj]
# TODO: how do we deal with this, as we obviously do not have data for removed objects ?
for obj in removed:
if not scene in changes_per_scene:
changes_per_scene[scene] = {}
changes_per_scene[scene][obj] = None # bpy.data.objects[obj]
for object_name in list(current[scene].keys()): # todo : exclude directly added/removed objects
@ -249,19 +271,31 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
if "Camera" in object_name:
pass#print(" current", current_obj, prev_obj)
if "Fox" in object_name:
"""if "Fox" in object_name:
print(" current", current_obj)
print(" previou", prev_obj)
print(" same?", same)
print(" same?", same)"""
#print("foo", same)
if not same:
""" print(" current", current_obj)
print(" previou", prev_obj)"""
if not scene in changes_per_scene:
changes_per_scene[scene] = {}
changes_per_scene[scene][object_name] = bpy.data.objects[object_name]
bubble_up_changes(bpy.data.objects[object_name], changes_per_scene[scene])
# now bubble up for instances & parents
"""if len(current[scene]) != len(previous[scene]) :
print("toto")"""
previous_stored.clear()
previous_stored.write(json.dumps(current))
print("changes per scene alternative", changes_per_scene)
return changes_per_scene
def execute(self, context):
@ -273,12 +307,12 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
self.save_settings(context)
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
changes_per_scene_2 = self.did_objects_change()
#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
# determine changed objects
changes_per_scene = self.did_objects_change()
# determine changed parameters
params_changed = self.did_export_settings_change()
auto_export(changes_per_scene, params_changed, self)

View File

@ -150,6 +150,7 @@ class AutoExportTracker(PropertyGroup):
#print(" changed object", obj.id.name, "changes", obj, "evalutated", obj.id.is_evaluated, "transforms", obj.is_updated_transform, "geometry", obj.is_updated_geometry)
if obj.is_updated_transform or 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,)
material = bpy.data.materials[obj.id.name]
@ -158,19 +159,21 @@ class AutoExportTracker(PropertyGroup):
for slot in obj.material_slots:
if slot.material == material:
cls.changed_objects_per_scene[scene.name][obj.name] = obj
#print("changed_objects_per_scene", cls.changed_objects_per_scene)
"""for obj_name_original in cls.changed_objects_per_scene[scene_name]:
if obj_name_original != ls.changed_objects_per_scene[scene_name][obj_name_original]"""
items = 0
for scene_name in cls.changed_objects_per_scene:
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)
# filter out invalid objects
for scene_name in cls.changed_objects_per_scene.keys():
"""for scene_name in cls.changed_objects_per_scene.keys():
bla = {}
for object_name in cls.changed_objects_per_scene[scene.name]:
object = cls.changed_objects_per_scene[scene.name][object_name]
object = cls.changed_objects_per_scene[scene.name][object_name]"""
#print("sdfsd", object, object.valid)
#if not cls.changed_objects_per_scene[scene.name][object_name].invalid:
# bla[object_name] = cls.changed_objects_per_scene[scene.name][object_name]
@ -181,7 +184,7 @@ class AutoExportTracker(PropertyGroup):
# keep it simple, just use Simplenamespace for compatibility with the rest of our code
# TODO: debounce
export_settings_changed = did_export_settings_change()
"""export_settings_changed = did_export_settings_change()
tmp = {}
for k in AutoExportGltfAddonPreferences.__annotations__:
item = AutoExportGltfAddonPreferences.__annotations__[k]
@ -217,7 +220,7 @@ class AutoExportTracker(PropertyGroup):
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)"""

View File

@ -214,8 +214,6 @@ def clear_hollow_scene(temp_scene, original_root_collection):
for child_collection in collection.children:
restore_original_names(child_collection)
# reset original names
restore_original_names(original_root_collection)
# remove any data we created
temp_root_collection = temp_scene.collection
@ -223,8 +221,12 @@ def clear_hollow_scene(temp_scene, original_root_collection):
for object in temp_scene_objects:
#print("removing", object.name)
bpy.data.objects.remove(object, do_unlink=True)
# remove the temporary scene
bpy.data.scenes.remove(temp_scene, do_unlink=True)
# reset original names
restore_original_names(original_root_collection)
# convenience utility to get lists of scenes
def get_scenes(addon_prefs):

View File

@ -1,8 +1,7 @@
import json
import numpy as np
import bpy
from ..constants import TEMPSCENE_PREFIX
fields_to_ignore_generic = ["tag", "type", "update_tag", "use_extra_user", "use_fake_user", "user_clear", "user_of_id", "user_remap", "users",
'animation_data_clear', 'animation_data_create', 'asset_clear', 'asset_data', 'asset_generate_preview', 'asset_mark', 'bl_rna', 'evaluated_get',
@ -19,18 +18,18 @@ def mesh_hash(obj):
h = str(hash(vertices_np.tobytes()))
return h
# TODO: redo this one, this is essentially modifiec copy & pasted data, not fitting
def animation_hash(obj):
animation_data = obj.animation_data
if not animation_data:
return None
return None
blender_actions = []
blender_tracks = {}
# TODO: this might need to be modified/ adapted to match the standard gltf exporter settings
for track in animation_data.nla_tracks:
non_muted_strips = [strip for strip in track.strips if strip.action is not None and strip.mute is False]
for strip in non_muted_strips: #t.strips:
strips = [strip for strip in track.strips if strip.action is not None]
for strip in strips:
# print(" ", source.name,'uses',strip.action.name, "active", strip.active, "action", strip.action)
blender_actions.append(strip.action)
blender_tracks[strip.action.name] = track.name
@ -55,6 +54,9 @@ def animation_hash(obj):
markers_per_animation[animation_name][marker.frame] = []
markers_per_animation[animation_name][marker.frame].append(marker.name)
compact_result = hash(str((blender_actions, blender_tracks, markers_per_animation, animations_infos)))
return compact_result
def camera_hash(obj):
camera_fields = ["angle", "angle_x", "angle_y", "animation_data", "background_images", "clip_end", "clip_start", "display_size", "dof", "fisheye_fov"]
@ -83,7 +85,7 @@ def bones_hash(bones):
all_field_names = dir(bone)
fields = [getattr(bone, prop, None) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
bones_result.append(fields)
print("fields of bone", bones_result)
#print("fields of bone", bones_result)
return str(hash(str(bones_result)))
# fixme: not good enough ?
@ -104,13 +106,15 @@ def serialize_scene():
print("serializing scene")
data = {}
for scene in bpy.data.scenes:
if scene.name.startswith(TEMPSCENE_PREFIX):
continue
data[scene.name] = {}
for object in scene.objects:
object = bpy.data.objects[object.name]
#print("object", object.name, object.location)
transform = str((object.location, object.rotation_euler, object.scale))
visibility = object.visible_get()
print("object type", object.type)
transform = str((object.location, object.rotation_euler, object.scale)) #str((object.matrix_world.to_translation(), object.matrix_world.to_euler('XYZ'), object.matrix_world.to_quaternion()))#
visibility = object.visible_get()
#print("object type", object.type)
custom_properties = {}
for K in object.keys():
if K not in '_RNA_UI' and K != 'components_meta':
@ -122,6 +126,8 @@ def serialize_scene():
camera = camera_hash(object) if object.type == 'CAMERA' else None
light = light_hash(object) if object.type == 'LIGHT' else None
armature = armature_hash(object) if object.type == 'ARMATURE' else None
parent = object.parent.name if object.parent else None
collections = [collection.name for collection in object.users_collection]
data[scene.name][object.name] = {
"name": object.name,
@ -132,7 +138,9 @@ def serialize_scene():
"mesh": mesh,
"camera": camera,
"light": light,
"armature": armature
"armature": armature,
"parent": parent,
"collections": collections
}
"""print("data", data)

View File

@ -304,7 +304,7 @@ def test_export_changed_parameters(setup_data):
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]]
assert modification_times[world_file_index] != modification_times_first[world_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 other_files_modification_times == other_files_modification_times_first
# reset the comparing
@ -331,26 +331,23 @@ def test_export_changed_parameters(setup_data):
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
# the "world" file should have changed (TODO: double check: this is since changing an instances collection changes the instance too ?)
# the "world" file should not have changed
world_file_index = mapped_files_to_timestamps_and_index["World"][1]
# and the blueprint3 file too, since that is the collection we changed
# the blueprint3 file should have changed, 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
# the blueprint4 file NOT, since, while it contains an instance of the collection we changed, the default export mode is "split"
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[world_file_index] == modification_times_first[world_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 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
# now same, but using an operator
print("----------------")
print("change using operator")
@ -362,13 +359,9 @@ def test_export_changed_parameters(setup_data):
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")
bpy.ops.transform.translate(value=(3.5, 0, 0), constraint_axis=(True, False, False))
auto_export_operator(
auto_export=True,
direct_mode=True,

View File

@ -1,4 +1,9 @@
- investigate remove_blueprints_list_from_main_scene (could be a case of changes to bpy.data not being applied immediatly)
- investigate clearing of changed_objects_per_scene
- it seems bevy_components does not trigger updates
- undo redo is ignored: ie save, do something, undo it, you still get changes
- undo redo is ignored: ie save, do something, undo it, you still get changes
- for collection instances:
* [ ] blueprints export should also take the split/embed mode into account: if a nested collection changes AND embed is active, its container collection should also be exported
* [ ] level exports should do the same