Compare commits

...

2 Commits

Author SHA1 Message Date
kaosat.dev
8683a6482f refactor(animation): renamed instancexxx to scenexxx (ie InstanceAnimations & InstancePlayerLinks) 2024-04-08 23:48:07 +02:00
kaosat.dev
cf4673c1e3 feat(auto_export): MORE experimenting, cleanup & analysis attempts
* moved temp scene prefix to new 'constants' file, updated code accordingly
 * fixed, updated & refactored handling of auto export & gltf parameters
 * added additional tests to changed_parameters
 * added display of changes since last save in new side tab
 * more investigation & fix attempts for change detection...
 * various related tweaks & cleanups
2024-04-08 23:41:17 +02:00
16 changed files with 179 additions and 63 deletions

View File

@ -17,8 +17,8 @@ pub struct BlueprintAnimationPlayerLink(pub Entity);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// storage for animations for a given entity (hierarchy), essentially a clone of gltf's `named_animations`
pub struct InstanceAnimations {
/// storage for scene level animations for a given entity (hierarchy), essentially a clone of gltf's `named_animations`
pub struct SceneAnimations {
pub named_animations: HashMap<String, Handle<AnimationClip>>,
}
@ -27,7 +27,7 @@ pub struct InstanceAnimations {
/// so that the root entity knows which of its children contains an actualy `AnimationPlayer` component
/// this is for convenience, because currently , Bevy's gltf parsing inserts `AnimationPlayers` "one level down"
/// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid
pub struct InstanceAnimationPlayerLink(pub Entity);
pub struct SceneAnimationPlayerLink(pub Entity);
/// Stores Animation information: name, frame informations etc
#[derive(Reflect, Default, Debug)]
@ -78,8 +78,8 @@ pub fn trigger_instance_animation_markers_events(
animation_infos: Query<(
Entity,
&AnimationMarkers,
&InstanceAnimationPlayerLink,
&InstanceAnimations,
&SceneAnimationPlayerLink,
&SceneAnimations,
&AnimationInfos,
)>,
animation_players: Query<&AnimationPlayer>,

View File

@ -120,7 +120,7 @@ impl Plugin for BlueprintsPlugin {
.register_type::<MaterialInfo>()
.register_type::<SpawnHere>()
.register_type::<BlueprintAnimations>()
.register_type::<InstanceAnimations>()
.register_type::<SceneAnimations>()
.register_type::<AnimationInfo>()
.register_type::<AnimationInfos>()
.register_type::<Vec<AnimationInfo>>()

View File

@ -3676,7 +3676,7 @@
"type": "object",
"typeInfo": "Struct"
},
"bevy_gltf_blueprints::animation::InstanceAnimations": {
"bevy_gltf_blueprints::animation::SceneAnimations": {
"additionalProperties": false,
"isComponent": true,
"isResource": false,
@ -3690,8 +3690,8 @@
"required": [
"named_animations"
],
"short_name": "InstanceAnimations",
"title": "bevy_gltf_blueprints::animation::InstanceAnimations",
"short_name": "SceneAnimations",
"title": "bevy_gltf_blueprints::animation::SceneAnimations",
"type": "object",
"typeInfo": "Struct"
},

View File

@ -2,7 +2,7 @@ use std::time::Duration;
use bevy_gltf_blueprints::{
AnimationInfos, AnimationMarkerReached, BlueprintAnimationPlayerLink, BlueprintAnimations,
InstanceAnimationPlayerLink, InstanceAnimations,
SceneAnimationPlayerLink, SceneAnimations,
};
use bevy::{gltf::Gltf, prelude::*};
@ -58,7 +58,7 @@ pub fn animations(
name, entity
);
println!("Found match {:?}", gltf.named_animations);
commands.entity(entity).insert(InstanceAnimations {
commands.entity(entity).insert(SceneAnimations {
named_animations: gltf.named_animations.clone(),
});
for ancestor in parents.iter_ancestors(entity) {
@ -66,7 +66,7 @@ pub fn animations(
// println!("found match with animationPlayer !! {:?}",names.get(ancestor));
commands
.entity(entity)
.insert(InstanceAnimationPlayerLink(ancestor));
.insert(SceneAnimationPlayerLink(ancestor));
}
// info!("{:?} is an ancestor of {:?}", ancestor, player);
}
@ -77,17 +77,17 @@ pub fn animations(
pub fn play_animations(
animated_marker1: Query<
(&InstanceAnimationPlayerLink, &InstanceAnimations),
(&SceneAnimationPlayerLink, &SceneAnimations),
(With<AnimationInfos>, With<Marker1>),
>,
animated_marker2: Query<
(&InstanceAnimationPlayerLink, &InstanceAnimations),
(&SceneAnimationPlayerLink, &SceneAnimations),
(With<AnimationInfos>, With<Marker2>),
>,
animated_marker3: Query<
(
&InstanceAnimationPlayerLink,
&InstanceAnimations,
&SceneAnimationPlayerLink,
&SceneAnimations,
&BlueprintAnimationPlayerLink,
&BlueprintAnimations,
),

View File

@ -5,6 +5,7 @@ import traceback
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)
@ -34,9 +35,8 @@ 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 = 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")
print("standard_gltf_exporter_settings", standard_gltf_exporter_settings.as_string())
standard_gltf_exporter_settings = json.loads(standard_gltf_exporter_settings.as_string())
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)

View File

@ -1,6 +1,7 @@
import os
import bpy
from ..constants import TEMPSCENE_PREFIX
from ..helpers.generate_and_export import generate_and_export
from .export_gltf import (generate_gltf_export_preferences)
from ..helpers.helpers_scenes import clear_hollow_scene, copy_hollowed_collection_into
@ -24,7 +25,7 @@ def export_collections(collections, folder_path, library_scene, addon_prefs, glt
collection = bpy.data.collections[collection_name]
generate_and_export(
addon_prefs,
temp_scene_name="__temp_scene_"+collection.name,
temp_scene_name=TEMPSCENE_PREFIX+collection.name,
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: copy_hollowed_collection_into(collection, temp_collection, library_collections=library_collections, addon_prefs=addon_prefs),

View File

@ -2,6 +2,7 @@ import json
import os
import bpy
from .get_standard_exporter_settings import get_standard_exporter_settings
from .preferences import (AutoExportGltfPreferenceNames)
def generate_gltf_export_preferences(addon_prefs):
@ -48,9 +49,7 @@ def generate_gltf_export_preferences(addon_prefs):
gltf_export_preferences[key] = getattr(addon_prefs, key)
standard_gltf_exporter_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")
standard_gltf_exporter_settings = json.loads(standard_gltf_exporter_settings.as_string())
"""standard_gltf_exporter_settings = get_standard_exporter_settings()"""
standard_gltf_exporter_settings = get_standard_exporter_settings()
#print("standard settings", standard_gltf_exporter_settings)
constant_keys = [

View File

@ -1,6 +1,7 @@
import os
import bpy
from ..constants import TEMPSCENE_PREFIX
from ..helpers.generate_and_export import generate_and_export
from .export_gltf import (generate_gltf_export_preferences, export_gltf)
from ..modules.bevy_dynamic import is_object_dynamic, is_object_static
@ -38,7 +39,7 @@ def export_main_scene(scene, folder_path, addon_prefs, library_collections):
# first export static objects
generate_and_export(
addon_prefs,
temp_scene_name="__temp_scene",
temp_scene_name=TEMPSCENE_PREFIX,
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: copy_hollowed_collection_into(scene.collection, temp_collection, library_collections=library_collections, filter=is_object_static, addon_prefs=addon_prefs),
@ -49,7 +50,7 @@ def export_main_scene(scene, folder_path, addon_prefs, library_collections):
gltf_output_path = os.path.join(folder_path, export_output_folder, scene.name+ "_dynamic")
generate_and_export(
addon_prefs,
temp_scene_name="__temp_scene",
temp_scene_name=TEMPSCENE_PREFIX,
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: copy_hollowed_collection_into(scene.collection, temp_collection, library_collections=library_collections, filter=is_object_dynamic, addon_prefs=addon_prefs),
@ -60,7 +61,7 @@ def export_main_scene(scene, folder_path, addon_prefs, library_collections):
#print("NO SPLIT")
generate_and_export(
addon_prefs,
temp_scene_name="__temp_scene",
temp_scene_name=TEMPSCENE_PREFIX,
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: copy_hollowed_collection_into(scene.collection, temp_collection, library_collections=library_collections, addon_prefs=addon_prefs),

View File

@ -0,0 +1,14 @@
import bpy
import json
def get_standard_exporter_settings():
standard_gltf_exporter_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] if ".gltf_auto_export_gltf_settings" in bpy.data.texts else None
if standard_gltf_exporter_settings != None:
try:
standard_gltf_exporter_settings = json.loads(standard_gltf_exporter_settings.as_string())
except:
standard_gltf_exporter_settings = {}
else:
standard_gltf_exporter_settings = {}
return standard_gltf_exporter_settings

View File

@ -46,7 +46,6 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
bpy.types.WindowManager.main_scenes_list_index = IntProperty(name = "Index for main scenes list", default = 0)
bpy.types.WindowManager.library_scenes_list_index = IntProperty(name = "Index for library scenes list", default = 0)
bpy.types.WindowManager.previous_export_settings = StringProperty(default="")
cls.main_scenes_index = 0
cls.library_scenes_index = 0
@ -59,8 +58,6 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
del bpy.types.WindowManager.main_scenes_list_index
del bpy.types.WindowManager.library_scenes_list_index
del bpy.types.WindowManager.previous_export_settings
def is_scene_ok(self, scene):
try:
operator = bpy.context.space_data.active_operator
@ -161,9 +158,20 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
# if there were no setting before, it is new, we need export
changed = False
if previous_auto_settings == None or previous_gltf_settings == None:
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
elif previous_gltf_settings == None:
print("previous gltf settings missing, exporting")
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.write(json.dumps({}))
changed = True
else:
auto_settings_changed = sorted(json.loads(previous_auto_settings.as_string()).items()) != sorted(json.loads(current_auto_settings.as_string()).items()) if current_auto_settings != None else False
gltf_settings_changed = sorted(json.loads(previous_gltf_settings.as_string()).items()) != sorted(json.loads(current_gltf_settings.as_string()).items()) if current_gltf_settings != None else False
@ -192,7 +200,6 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
def execute(self, context):
print("execute")
# disable change detection while the operator runs
bpy.context.window_manager.auto_export_tracker.disable_change_detection()
if self.direct_mode:
self.load_settings(context)
@ -203,6 +210,8 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
if self.auto_export: # only do the actual exporting if auto export is actually enabled
#& 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
#determine changed parameters
params_changed = self.did_export_settings_change()
auto_export(changes_per_scene, params_changed, self)
@ -217,14 +226,21 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
#bpy.app.timers.register(bpy.context.window_manager.auto_export_tracker.enable_change_detection, first_interval=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")
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)
"""[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:
@ -235,15 +251,20 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper):
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")
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):
pass
def cancel(self, context):
print("cancel")
#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)

View File

@ -1,8 +1,9 @@
import json
import bpy
from bpy.types import (PropertyGroup)
from bpy.props import (PointerProperty, IntProperty)
from bpy.props import (PointerProperty, IntProperty, StringProperty)
from ..constants import TEMPSCENE_PREFIX
from .internals import CollectionsToExport
class AutoExportTracker(PropertyGroup):
@ -21,6 +22,8 @@ class AutoExportTracker(PropertyGroup):
default=0
) # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.auto_export_tracker = PointerProperty(type=AutoExportTracker)
@ -59,10 +62,15 @@ class AutoExportTracker(PropertyGroup):
@classmethod
def deps_update_handler(cls, scene, depsgraph):
print("change detection enabled", cls.change_detection_enabled, bpy.context.window_manager.auto_export_tracker.change_detection_enabled)
print("change detection enabled", cls.change_detection_enabled)
ops = bpy.context.window_manager.operators
print("last operators", ops)
for op in ops:
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
@ -80,19 +88,21 @@ class AutoExportTracker(PropertyGroup):
active_operator.will_save_settings = True
active_operator.auto_export = True
if not scene.name.startswith("__temp_scene"):
# 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)
changed_scene = scene.name or ""
# only deal with changes if we are no in the mids of saving/exporting
if cls.change_detection_enabled:
#print("-------------")
if not changed_scene in cls.changed_objects_per_scene:
cls.changed_objects_per_scene[changed_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]
@ -100,7 +110,7 @@ class AutoExportTracker(PropertyGroup):
print("FOO","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:
@ -113,7 +123,7 @@ 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()
@ -125,13 +135,15 @@ class AutoExportTracker(PropertyGroup):
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")
self.change_detection_enabled = True
self.__class__.change_detection_enabled = True
#FIXME: not sure about these
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

View File

@ -0,0 +1 @@
TEMPSCENE_PREFIX = "__temp_scene"

View File

@ -195,11 +195,12 @@ def test_export_changed_parameters(setup_data):
# now same, but move the cube in the library
print("----------------")
print("library change")
print("library change (blueprint) ")
print("----------------")
bpy.context.window_manager.auto_export_tracker.enable_change_detection() # FIXME: should not be needed, but ..
bpy.data.objects["Blueprint1_mesh"].location = [1, 2, 1]
auto_export_operator(
auto_export=True,
direct_mode=True,
@ -212,17 +213,55 @@ 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
# only the "world" file should have changed
# 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 [blueprint1_file_index]]
other_files_modification_times_first = [value for index, value in enumerate(modification_times_first) if index not in [blueprint1_file_index]]
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[blueprint1_file_index] != modification_times_first[blueprint1_file_index]
assert other_files_modification_times == other_files_modification_times_first
# reset the comparing
modification_times_first = modification_times
# now change something in a nested blueprint
print("----------------")
print("library change (nested blueprint) ")
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
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
# 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]]
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
modification_times_first = modification_times
# now same, but using an operator
print("----------------")

View File

@ -47,8 +47,28 @@ def setup_data(request):
- checks if timestamps have changed
- if all worked => test is a-ok
- removes generated files
"""
def test_export_no_parameters(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
# first test exporting withouth any parameters set, this should export with default parameters gracefully
auto_export_operator(
auto_export=True,
direct_mode=True,
export_output_folder="./models",
export_legacy_mode=False,
export_materials_library=True
)
def test_export_changed_parameters(setup_data):
root_path = "../../testing/bevy_example"
assets_root_path = os.path.join(root_path, "assets")
@ -77,11 +97,6 @@ def test_export_changed_parameters(setup_data):
stored_gltf_settings.clear()
stored_gltf_settings.write(json.dumps(gltf_settings))
# move the main cube
bpy.data.objects["Cube"].location = [1, 0, 0]
# move the cube in the library
bpy.data.objects["Blueprint1_mesh"].location = [1, 2, 1]
auto_export_operator(
auto_export=True,
direct_mode=True,

View File

@ -0,0 +1,4 @@
- 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

View File

@ -48,6 +48,15 @@ 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)
# main ui in the file => export