Compare commits

..

5 Commits

Author SHA1 Message Date
kaosat.dev
b8a7eba71d feat(blenvy):
* improvements to assets ui
 * assets ui now specific to per level data, not based on selection anymore
 * blueprints ui now presents assets per blueprint
 * a lot of tweaks & improvements in the areas above
2024-05-13 23:36:13 +02:00
kaosat.dev
f3dbf76ce6 feat(tools): updated/fixed some tests 2024-05-13 11:21:50 +02:00
kaosat.dev
0306bfb857 feat(blenvy): added most of the code from both add-ons
* adapted most of the UI to the new structure
2024-05-13 10:28:44 +02:00
kaosat.dev
53d432fc77 feat(blenvy):
* overhauled selection
 * made adding/removing assets specific to collections/scenes (blueprints/levels)
 * ui tweaks & improvements
 * etc
2024-05-12 15:10:35 +02:00
kaosat.dev
e42719daf7 feat(blenvy): experimenting with new combined tooling 2024-05-12 12:09:11 +02:00
119 changed files with 12069 additions and 115 deletions

186
tools/blenvy/__init__.py Normal file
View File

@ -0,0 +1,186 @@
bl_info = {
"name": "blenvy",
"author": "kaosigh",
"version": (0, 1, 0),
"blender": (3, 4, 0),
"location": "File > Import-Export",
"description": "tooling for the Bevy engine",
"warning": "",
"wiki_url": "https://github.com/kaosat-dev/Blender_bevy_components_workflow",
"tracker_url": "https://github.com/kaosat-dev/Blender_bevy_components_workflow/issues/new",
"category": "Import-Export"
}
import bpy
from bpy.app.handlers import persistent
from bpy.props import (StringProperty)
# components management
from .bevy_components.components.operators import CopyComponentOperator, Fix_Component_Operator, OT_rename_component, RemoveComponentFromAllObjectsOperator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, AddComponentOperator, RenameHelper, Toggle_ComponentVisibility
from .bevy_components.registry.registry import ComponentsRegistry,MissingBevyType
from .bevy_components.registry.operators import (COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT, OT_select_component_name_to_replace, OT_select_object, ReloadRegistryOperator, OT_OpenFilebrowser)
from .bevy_components.registry.ui import (BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_AdvancedToolsPanel, BEVY_COMPONENTS_PT_MissingTypesPanel, MISSING_TYPES_UL_List)
from .bevy_components.components.metadata import (ComponentMetadata, ComponentsMeta)
from .bevy_components.components.lists import GENERIC_LIST_OT_actions, Generic_LIST_OT_AddItem, Generic_LIST_OT_RemoveItem, Generic_LIST_OT_SelectItem
from .bevy_components.components.maps import GENERIC_MAP_OT_actions
from .bevy_components.components.definitions_list import (ComponentDefinitionsList, ClearComponentDefinitionsList)
from .bevy_components.components.ui import (BEVY_COMPONENTS_PT_ComponentsPanel)
# auto export
from .gltf_auto_export.auto_export.operators import AutoExportGLTF
from .gltf_auto_export.auto_export.tracker import AutoExportTracker
from .gltf_auto_export.auto_export.preferences import (AutoExportGltfAddonPreferences)
from .gltf_auto_export.auto_export.internals import (SceneLink,
SceneLinks,
CollectionToExport,
BlueprintsToExport,
CUSTOM_PG_sceneName
)
from .gltf_auto_export.ui.main import (GLTF_PT_auto_export_change_detection, 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,
GLTF_PT_auto_export_blueprints,
SCENE_UL_GLTF_auto_export,
GLTF_PT_auto_export_SidePanel
)
from .gltf_auto_export.ui.operators import (OT_OpenFolderbrowser, SCENES_LIST_OT_actions)
# asset management
from .assets.ui import GLTF_PT_auto_export_assets
from .assets.assets_registry import AssetsRegistry
from .assets.operators import OT_Add_asset_filebrowser, OT_add_bevy_asset, OT_remove_bevy_asset
# blueprints management
from .blueprints.ui import GLTF_PT_auto_export_blueprints_list
from .blueprints.blueprints_registry import BlueprintsRegistry
from .blueprints.operators import OT_select_blueprint
# blenvy core
from .core.ui import BLENVY_PT_SidePanel
from .core.blenvy_manager import BlenvyManager
from .core.operators import OT_switch_bevy_tooling
classes = [
# blenvy
BLENVY_PT_SidePanel,
# bevy components
AddComponentOperator,
CopyComponentOperator,
PasteComponentOperator,
RemoveComponentOperator,
RemoveComponentFromAllObjectsOperator,
Fix_Component_Operator,
OT_rename_component,
RenameHelper,
GenerateComponent_From_custom_property_Operator,
Toggle_ComponentVisibility,
ComponentDefinitionsList,
ClearComponentDefinitionsList,
ComponentMetadata,
ComponentsMeta,
MissingBevyType,
ComponentsRegistry,
OT_OpenFilebrowser,
ReloadRegistryOperator,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT,
OT_select_object,
OT_select_component_name_to_replace,
BEVY_COMPONENTS_PT_ComponentsPanel,
BEVY_COMPONENTS_PT_AdvancedToolsPanel,
BEVY_COMPONENTS_PT_Configuration,
MISSING_TYPES_UL_List,
BEVY_COMPONENTS_PT_MissingTypesPanel,
Generic_LIST_OT_SelectItem,
Generic_LIST_OT_AddItem,
Generic_LIST_OT_RemoveItem,
GENERIC_LIST_OT_actions,
GENERIC_MAP_OT_actions,
# gltf auto export
SceneLink,
SceneLinks,
CUSTOM_PG_sceneName,
SCENE_UL_GLTF_auto_export,
SCENES_LIST_OT_actions,
OT_OpenFolderbrowser,
AutoExportGLTF,
CollectionToExport,
BlueprintsToExport,
GLTF_PT_auto_export_main,
GLTF_PT_auto_export_root,
GLTF_PT_auto_export_general,
GLTF_PT_auto_export_change_detection,
GLTF_PT_auto_export_scenes,
GLTF_PT_auto_export_blueprints,
GLTF_PT_auto_export_SidePanel,
AutoExportTracker,
# blenvy
BlenvyManager,
OT_switch_bevy_tooling,
AssetsRegistry,
OT_add_bevy_asset,
OT_remove_bevy_asset,
OT_Add_asset_filebrowser,
GLTF_PT_auto_export_assets,
BlueprintsRegistry,
OT_select_blueprint,
GLTF_PT_auto_export_blueprints_list,
]
@persistent
def post_update(scene, depsgraph):
bpy.context.window_manager.auto_export_tracker.deps_post_update_handler( scene, depsgraph)
@persistent
def post_save(scene, depsgraph):
bpy.context.window_manager.auto_export_tracker.save_handler( scene, depsgraph)
@persistent
def post_load(file_name):
registry = bpy.context.window_manager.components_registry
if registry != None:
registry.load_settings()
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.app.handlers.load_post.append(post_load)
# for some reason, adding these directly to the tracker class in register() do not work reliably
bpy.app.handlers.depsgraph_update_post.append(post_update)
bpy.app.handlers.save_post.append(post_save)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(post_load)
bpy.app.handlers.depsgraph_update_post.remove(post_update)
bpy.app.handlers.save_post.remove(post_save)
print("TOTO")

View File

@ -68,6 +68,7 @@ class AssetsRegistry(PropertyGroup):
('MODEL', "Model", ""), ('MODEL', "Model", ""),
('AUDIO', "Audio", ""), ('AUDIO', "Audio", ""),
('IMAGE', "Image", ""), ('IMAGE', "Image", ""),
('TEXT', "Text", ""),
) )
) # type: ignore ) # type: ignore

View File

@ -0,0 +1,152 @@
import os
import json
import bpy
from bpy_types import (Operator)
from bpy.props import (BoolProperty, StringProperty, EnumProperty)
from ..settings import load_settings
class OT_add_bevy_asset(Operator):
"""Add asset"""
bl_idname = "bevyassets.add"
bl_label = "Add bevy asset"
bl_options = {"UNDO"}
asset_name: StringProperty(
name="asset name",
description="name of asset to add",
) # type: ignore
asset_type: EnumProperty(
items=(
('MODEL', "Model", ""),
('AUDIO', "Audio", ""),
('IMAGE', "Image", ""),
('TEXT', "Text", ""),
)
) # type: ignore
asset_path: StringProperty(
name="asset path",
description="path of asset to add",
subtype='FILE_PATH'
) # type: ignore
# what are we targetting
target_type: EnumProperty(
name="target type",
description="type of the target: scene or blueprint to add an asset to",
items=(
('SCENE', "Scene", ""),
('BLUEPRINT', "Blueprint", ""),
),
) # type: ignore
target_name: StringProperty(
name="target name",
description="name of the target blueprint or scene to add asset to"
) # type: ignore
def execute(self, context):
assets = []
blueprint_assets = self.target_type == 'BLUEPRINT'
print("FOOO", self.target_name, self.target_type)
if blueprint_assets:
assets = json.loads(bpy.data.collections[self.target_name].get('assets')) if 'assets' in bpy.data.collections[self.target_name] else []
else:
assets = json.loads(bpy.data.scenes[self.target_name].get('assets')) if 'assets' in bpy.data.scenes[self.target_name] else []
in_list = [asset for asset in assets if (asset["path"] == self.asset_path)]
in_list = len(in_list) > 0
if not in_list:
assets.append({"name": self.asset_name, "type": self.asset_type, "path": self.asset_path, "internal": False})
if blueprint_assets:
bpy.data.collections[self.target_name]["assets"] = json.dumps(assets)
else:
bpy.data.scenes[self.target_name]["assets"] = json.dumps(assets)
#context.window_manager.assets_registry.add_asset(self.asset_name, self.asset_type, self.asset_path, False)
return {'FINISHED'}
class OT_remove_bevy_asset(Operator):
"""Remove asset"""
bl_idname = "bevyassets.remove"
bl_label = "remove bevy asset"
bl_options = {"UNDO"}
asset_path: StringProperty(
name="asset path",
description="path of asset to add",
subtype='FILE_PATH'
) # type: ignore
clear_all: BoolProperty (
name="clear all assets",
description="clear all assets",
default=False
) # type: ignore
# what are we targetting
target_type: EnumProperty(
name="target type",
description="type of the target: scene or blueprint to add an asset to",
items=(
('SCENE', "Scene", ""),
('BLUEPRINT', "Blueprint", ""),
),
) # type: ignore
target_name: StringProperty(
name="target name",
description="name of the target blueprint or scene to add asset to"
) # type: ignore
def execute(self, context):
assets = []
blueprint_assets = self.target_type == 'BLUEPRINT'
if blueprint_assets:
assets = json.loads(bpy.data.collections[self.target_name].get('assets')) if 'assets' in bpy.data.collections[self.target_name] else []
else:
assets = json.loads(bpy.data.scenes[self.target_name].get('assets')) if 'assets' in bpy.data.scenes[self.target_name] else []
assets = [asset for asset in assets if (asset["path"] != self.asset_path)]
if blueprint_assets:
bpy.data.collections[self.target_name]["assets"] = json.dumps(assets)
else:
bpy.data.scenes[self.target_name]["assets"] = json.dumps(assets)
#context.window_manager.assets_registry.remove_asset(self.asset_path)
return {'FINISHED'}
import os
from bpy_extras.io_utils import ImportHelper
class OT_Add_asset_filebrowser(Operator, ImportHelper):
"""Browse for asset files"""
bl_idname = "asset.open_filebrowser"
bl_label = "Select asset file"
# Define this to tell 'fileselect_add' that we want a directoy
filepath: bpy.props.StringProperty(
name="asset Path",
description="selected file",
subtype='FILE_PATH',
) # type: ignore
# Filters files
filter_glob: StringProperty(options={'HIDDEN'}, default='*.jpg;*.jpeg;*.png;*.bmp') # type: ignore
def execute(self, context):
current_auto_settings = load_settings(".gltf_auto_export_settings")
export_root_folder = current_auto_settings.get("export_root_folder")
asset_path = os.path.relpath(self.filepath, export_root_folder)
assets_registry = context.window_manager.assets_registry
assets_registry.asset_path_selector = asset_path
print("SELECTED ASSET PATH", asset_path)
return {'FINISHED'}

79
tools/blenvy/assets/ui.py Normal file
View File

@ -0,0 +1,79 @@
import bpy
import json
def draw_assets(layout, name, title, asset_registry, assets, target_type, target_name):
header, panel = layout.box().panel(f"assets{name}", default_closed=False)
header.label(text=title)
if panel:
row = panel.row()
row.prop(asset_registry, "asset_name_selector", text="")
row.prop(asset_registry, "asset_type_selector", text="")
asset_selector = row.operator(operator="asset.open_filebrowser", text="", icon="FILE_FOLDER")
if asset_registry.asset_type_selector == 'IMAGE':
asset_selector.filter_glob = '*.jpg;*.jpeg;*.png;*.bmp'
if asset_registry.asset_type_selector == 'MODEL':
asset_selector.filter_glob="*.glb;*.gltf"
if asset_registry.asset_type_selector == 'TEXT':
asset_selector.filter_glob="*.txt;*.md;*.ron;*.json"
if asset_registry.asset_type_selector == 'AUDIO':
asset_selector.filter_glob="*.mp3;*.wav;*.flac"
add_asset = row.operator(operator="bevyassets.add", text="", icon="ADD")
add_asset.target_type = target_type
add_asset.target_name = target_name
add_asset.asset_name = asset_registry.asset_name_selector
add_asset.asset_type = asset_registry.asset_type_selector
add_asset.asset_path = asset_registry.asset_path_selector
#assets = json.loads(blueprint.collection["assets"]) if "assets" in blueprint.collection else []
for asset in assets:
row = panel.row()
row.label(text=asset["name"])
row.label(text=asset["type"])
row.label(text=asset["path"])
if not asset["internal"]:
remove_asset = row.operator(operator="bevyassets.remove", text="", icon="TRASH")
remove_asset.target_type = target_type
remove_asset.target_name = target_name
remove_asset.asset_path = asset["path"]
else:
row.label(text="")
class GLTF_PT_auto_export_assets(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_label = ""
bl_parent_id = "BLENVY_PT_SidePanel"
bl_options = {'DEFAULT_CLOSED','HIDE_HEADER'}
@classmethod
def poll(cls, context):
return context.window_manager.blenvy.mode == 'ASSETS'
"""def draw_header(self, context):
layout = self.layout
name = ""
if context.collection is not None and context.collection.name == 'Scene Collection':
name = f"WORLD/LEVEL: {context.scene.name}"
else:
name = f"BLUEPRINT: {context.collection.name}"
layout.label(text=f"Assets For {name}")"""
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
asset_registry = context.window_manager.assets_registry
name = "world"
header, panel = layout.box().panel(f"assets{name}", default_closed=False)
header.label(text="World/Level Assets")
if panel:
for scene in bpy.data.scenes:
if scene.name != "Library": # FIXME: hack for testing
assets = json.loads(scene.get('assets')) if 'assets' in scene else []
row = panel.row()
draw_assets(layout=row, name=scene.name, title=f"{scene.name} Assets", asset_registry=asset_registry, assets=assets, target_type="SCENE", target_name=scene.name)

View File

@ -0,0 +1,57 @@
import bpy
from bpy.props import (StringProperty)
# this one is for UI only, and its inner list contains a useable list of shortnames of components
class ComponentDefinitionsList(bpy.types.PropertyGroup):
# FIXME: not sure, hard coded exclude list, feels wrong
exclude = ['Parent', 'Children']
def add_component_to_ui_list(self, context):
#print("add components to ui_list")
items = []
type_infos = context.window_manager.components_registry.type_infos
for long_name in type_infos.keys():
definition = type_infos[long_name]
short_name = definition["short_name"]
is_component = definition['isComponent'] if "isComponent" in definition else False
if self.filter in short_name and is_component:
if not 'Handle' in short_name and not "Cow" in short_name and not "AssetId" in short_name and short_name not in self.exclude: # FIXME: hard coded, seems wrong
items.append((long_name, short_name, long_name))
items.sort(key=lambda a: a[1])
return items
@classmethod
def register(cls):
bpy.types.WindowManager.components_list = bpy.props.PointerProperty(type=ComponentDefinitionsList)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_list
list : bpy.props.EnumProperty(
name="list",
description="list",
# items argument required to initialize, just filled with empty values
items = add_component_to_ui_list,
) # type: ignore
filter: StringProperty(
name="component filter",
description="filter for the components list",
options={'TEXTEDIT_UPDATE'}
) # type: ignore
class ClearComponentDefinitionsList(bpy.types.Operator):
''' clear list of bpy.context.collection.component_definitions '''
bl_label = "clear component definitions"
bl_idname = "components.clear_component_definitions"
def execute(self, context):
# create a new item, assign its properties
bpy.context.collection.component_definitions.clear()
return {'FINISHED'}

View File

@ -0,0 +1,6 @@
import rna_prop_ui
# fake way to make our operator's changes be visible to the change/depsgraph update handler in gltf_auto_export
def ping_depsgraph_update(object):
rna_prop_ui.rna_idprop_ui_create(object, "________temp", default=0)
rna_prop_ui.rna_idprop_ui_prop_clear(object, "________temp")

View File

@ -0,0 +1,170 @@
import json
from bpy_types import Operator, UIList
from bpy.props import (StringProperty, EnumProperty, PointerProperty, FloatVectorProperty, IntProperty)
class Generic_LIST_OT_AddItem(Operator):
"""Add a new item to the list."""
bl_idname = "generic_list.add_item"
bl_label = "Add a new item"
property_group_path: StringProperty(
name="property group path",
description="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
def execute(self, context):
print("")
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
print("list container", propertyGroup, dict(propertyGroup))
target_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
item = target_list.add()
propertyGroup.list_index = index + 1 # we use this to force the change detection
print("added item", item, item.field_names, getattr(item, "field_names"))
print("")
return{'FINISHED'}
class Generic_LIST_OT_RemoveItem(Operator):
"""Remove an item to the list."""
bl_idname = "generic_list.remove_item"
bl_label = "Remove selected item"
property_group_path: StringProperty(
name="property group path",
description="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
def execute(self, context):
print("remove from list", context.object)
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
target_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
target_list.remove(index)
propertyGroup.list_index = min(max(0, index - 1), len(target_list) - 1)
return{'FINISHED'}
class Generic_LIST_OT_SelectItem(Operator):
"""Remove an item to the list."""
bl_idname = "generic_list.select_item"
bl_label = "select an item"
property_group_path: StringProperty(
name="property group path",
description="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
selection_index: IntProperty() # type: ignore
def execute(self, context):
print("select in list", context.object)
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
target_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
propertyGroup.list_index = self.selection_index
return{'FINISHED'}
class GENERIC_LIST_OT_actions(Operator):
"""Move items up and down, add and remove"""
bl_idname = "generic_list.list_action"
bl_label = "List Actions"
bl_description = "Move items up and down, add and remove"
bl_options = {'REGISTER', 'UNDO'}
action: EnumProperty(
items=(
('UP', "Up", ""),
('DOWN', "Down", ""),
('REMOVE', "Remove", ""),
('ADD', "Add", ""))) # type: ignore
property_group_path: StringProperty(
name="property group path",
description="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
def invoke(self, context, event):
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
target_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
if self.action == 'DOWN' and index < len(target_list) - 1:
#item_next = scn.rule_list[index + 1].name
target_list.move(index, index + 1)
propertyGroup.list_index += 1
elif self.action == 'UP' and index >= 1:
#item_prev = scn.rule_list[index - 1].name
target_list.move(index, index - 1)
propertyGroup.list_index -= 1
elif self.action == 'REMOVE':
target_list.remove(index)
propertyGroup.list_index = min(max(0, index - 1), len(target_list) - 1)
if self.action == 'ADD':
item = target_list.add()
propertyGroup.list_index = index + 1 # we use this to force the change detection
#info = '"%s" added to list' % (item.name)
#self.report({'INFO'}, info)
return {"FINISHED"}

View File

@ -0,0 +1,121 @@
import json
from bpy_types import Operator, UIList
from bpy.props import (StringProperty, EnumProperty, PointerProperty, FloatVectorProperty, IntProperty)
from ..propGroups.conversions_from_prop_group import property_group_value_to_custom_property_value
class GENERIC_MAP_OT_actions(Operator):
"""Move items up and down, add and remove"""
bl_idname = "generic_map.map_action"
bl_label = "Map Actions"
bl_description = "Move items up and down, add and remove"
bl_options = {'REGISTER', 'UNDO'}
action: EnumProperty(
items=(
('UP', "Up", ""),
('DOWN', "Down", ""),
('REMOVE', "Remove", ""),
('ADD', "Add", ""))) # type: ignore
property_group_path: StringProperty(
name="property group path",
description="",
) # type: ignore
component_name: StringProperty(
name="component name",
description="",
) # type: ignore
target_index: IntProperty(name="target index", description="index of item to manipulate")# type: ignore
def invoke(self, context, event):
object = context.object
# information is stored in component meta
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == self.component_name, components_in_object), None)
propertyGroup = component_meta
for path_item in json.loads(self.property_group_path):
propertyGroup = getattr(propertyGroup, path_item)
keys_list = getattr(propertyGroup, "list")
index = getattr(propertyGroup, "list_index")
values_list = getattr(propertyGroup, "values_list")
values_index = getattr(propertyGroup, "values_list_index")
key_setter = getattr(propertyGroup, "keys_setter")
value_setter = getattr(propertyGroup, "values_setter")
if self.action == 'DOWN' and index < len(keys_list) - 1:
#item_next = scn.rule_list[index + 1].name
keys_list.move(index, index + 1)
propertyGroup.list_index += 1
elif self.action == 'UP' and index >= 1:
#item_prev = scn.rule_list[index - 1].name
keys_list.move(index, index - 1)
propertyGroup.list_index -= 1
elif self.action == 'REMOVE':
index = self.target_index
keys_list.remove(index)
values_list.remove(index)
propertyGroup.list_index = min(max(0, index - 1), len(keys_list) - 1)
propertyGroup.values_index = min(max(0, index - 1), len(keys_list) - 1)
if self.action == 'ADD':
print("keys_list", keys_list)
# first we gather all key/value pairs
hashmap = {}
for index, key in enumerate(keys_list):
key_entry = {}
for field_name in key.field_names:
key_entry[field_name] = getattr(key, field_name, None)
value_entry = {}
for field_name in values_list[index].field_names:
value_entry[field_name] = values_list[index][field_name]
hashmap[json.dumps(key_entry)] = index
print("hashmap", hashmap )
# then we need to find the index of a specific value if it exists
key_entry = {}
for field_name in key_setter.field_names:
key_entry[field_name] = getattr(key_setter, field_name, None)
key_to_add = json.dumps(key_entry)
existing_index = hashmap.get(key_to_add, None)
print("existing_index", existing_index)
if existing_index is None:
print("adding new value")
key = keys_list.add()
# copy the values over
for field_name in key_setter.field_names:
val = getattr(key_setter, field_name, None)
if val is not None:
key[field_name] = val
# TODO: add error handling
value = values_list.add()
# copy the values over
for field_name in value_setter.field_names:
val = getattr(value_setter, field_name, None)
if val is not None:
value[field_name] = val
# TODO: add error handling
propertyGroup.list_index = index + 1 # we use this to force the change detection
propertyGroup.values_index = index + 1 # we use this to force the change detection
else:
print("overriding value")
for field_name in value_setter.field_names:
values_list[existing_index][field_name] = value_setter[field_name]
#info = '"%s" added to list' % (item.name)
#self.report({'INFO'}, info)
return {"FINISHED"}

View File

@ -0,0 +1,344 @@
import bpy
from bpy.props import (StringProperty, BoolProperty, PointerProperty)
from bpy_types import (PropertyGroup)
from ..propGroups.conversions_from_prop_group import property_group_value_to_custom_property_value
from ..propGroups.conversions_to_prop_group import property_group_value_from_custom_property_value
class ComponentMetadata(bpy.types.PropertyGroup):
short_name : bpy.props.StringProperty(
name = "name",
default = ""
) # type: ignore
long_name : bpy.props.StringProperty(
name = "long name",
default = ""
) # type: ignore
values: bpy.props.StringProperty(
name = "Value",
default = ""
) # type: ignore
enabled: BoolProperty(
name="enabled",
description="component enabled",
default=True
) # type: ignore
invalid: BoolProperty(
name="invalid",
description="component is invalid, because of missing registration/ other issues",
default=False
) # type: ignore
invalid_details: StringProperty(
name="invalid details",
description="detailed information about why the component is invalid",
default=""
) # type: ignore
visible: BoolProperty( # REALLY dislike doing this for UI control, but ok hack for now
default=True
) # type: ignore
class ComponentsMeta(PropertyGroup):
infos_per_component: StringProperty(
name="infos per component",
description="component"
) # type: ignore
components: bpy.props.CollectionProperty(type = ComponentMetadata) # type: ignore
@classmethod
def register(cls):
bpy.types.Object.components_meta = PointerProperty(type=ComponentsMeta)
@classmethod
def unregister(cls):
del bpy.types.Object.components_meta
# remove no longer valid metadata from object
def cleanup_invalid_metadata(object):
bevy_components = get_bevy_components(object)
if len(bevy_components.keys()) == 0: # no components, bail out
return
components_metadata = object.components_meta.components
to_remove = []
for index, component_meta in enumerate(components_metadata):
long_name = component_meta.long_name
if long_name not in bevy_components.keys():
print("component:", long_name, "present in metadata, but not in object")
to_remove.append(index)
for index in to_remove:
components_metadata.remove(index)
# returns a component definition ( an entry in registry's type_infos) with matching long name or None if nothing has been found
def find_component_definition_from_long_name(long_name):
registry = bpy.context.window_manager.components_registry
return registry.type_infos.get(long_name, None)
# FIXME: feels a bit heavy duty, should only be done
# if the components panel is active ?
def ensure_metadata_for_all_objects():
for object in bpy.data.objects:
add_metadata_to_components_without_metadata(object)
# returns whether an object has custom properties without matching metadata
def do_object_custom_properties_have_missing_metadata(object):
components_metadata = getattr(object, "components_meta", None)
if components_metadata == None:
return True
components_metadata = components_metadata.components
missing_metadata = False
for component_name in get_bevy_components(object) :
if component_name == "components_meta":
continue
component_meta = next(filter(lambda component: component["long_name"] == component_name, components_metadata), None)
if component_meta == None:
# current component has no metadata but is there even a compatible type in the registry ?
# if not ignore it
component_definition = find_component_definition_from_long_name(component_name)
if component_definition != None:
missing_metadata = True
break
return missing_metadata
import json
def upsert_bevy_component(object, long_name, value):
if not 'bevy_components' in object:
object['bevy_components'] = '{}'
bevy_components = json.loads(object['bevy_components'])
bevy_components[long_name] = value
object['bevy_components'] = json.dumps(bevy_components)
#object['bevy_components'][long_name] = value # Sigh, this does not work, hits Blender's 63 char length limit
def remove_bevy_component(object, long_name):
if 'bevy_components' in object:
bevy_components = json.loads(object['bevy_components'])
if long_name in bevy_components:
del bevy_components[long_name]
object['bevy_components'] = json.dumps(bevy_components)
if long_name in object:
del object[long_name]
def get_bevy_components(object):
if 'bevy_components' in object:
bevy_components = json.loads(object['bevy_components'])
return bevy_components
return {}
def get_bevy_component_value_by_long_name(object, long_name):
bevy_components = get_bevy_components(object)
if len(bevy_components.keys()) == 0 :
return None
return bevy_components.get(long_name, None)
def is_bevy_component_in_object(object, long_name):
return get_bevy_component_value_by_long_name(object, long_name) is not None
# adds metadata to object only if it is missing
def add_metadata_to_components_without_metadata(object):
registry = bpy.context.window_manager.components_registry
for component_name in get_bevy_components(object) :
if component_name == "components_meta":
continue
upsert_component_in_object(object, component_name, registry)
# adds a component to an object (including metadata) using the provided component definition & optional value
def add_component_to_object(object, component_definition, value=None):
cleanup_invalid_metadata(object)
if object is not None:
# print("add_component_to_object", component_definition)
long_name = component_definition["long_name"]
registry = bpy.context.window_manager.components_registry
if not registry.has_type_infos():
raise Exception('registry type infos have not been loaded yet or are missing !')
definition = registry.type_infos[long_name]
# now we use our pre_generated property groups to set the initial value of our custom property
(_, propertyGroup) = upsert_component_in_object(object, long_name=long_name, registry=registry)
if value == None:
value = property_group_value_to_custom_property_value(propertyGroup, definition, registry, None)
else: # we have provided a value, that is a raw , custom property value, to set the value of the propertyGroup
object["__disable__update"] = True # disable update callback while we set the values of the propertyGroup "tree" (as a propertyGroup can contain other propertyGroups)
property_group_value_from_custom_property_value(propertyGroup, definition, registry, value)
del object["__disable__update"]
upsert_bevy_component(object, long_name, value)
def upsert_component_in_object(object, long_name, registry):
# print("upsert_component_in_object", object, "component name", component_name)
# TODO: upsert this part too ?
target_components_metadata = object.components_meta.components
component_definition = registry.type_infos.get(long_name, None)
if component_definition != None:
short_name = component_definition["short_name"]
long_name = component_definition["long_name"]
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
propertyGroup = None
component_meta = next(filter(lambda component: component["long_name"] == long_name, target_components_metadata), None)
if not component_meta:
component_meta = target_components_metadata.add()
component_meta.short_name = short_name
component_meta.long_name = long_name
propertyGroup = getattr(component_meta, property_group_name, None)
else: # this one has metadata but we check that the relevant property group is present
propertyGroup = getattr(component_meta, property_group_name, None)
# try to inject propertyGroup if not present
if propertyGroup == None:
#print("propertygroup not found in metadata attempting to inject")
if property_group_name in registry.component_propertyGroups:
# we have found a matching property_group, so try to inject it
# now inject property group
setattr(ComponentMetadata, property_group_name, registry.component_propertyGroups[property_group_name]) # FIXME: not ideal as all ComponentMetadata get the propGroup, but have not found a way to assign it per instance
propertyGroup = getattr(component_meta, property_group_name, None)
# now deal with property groups details
if propertyGroup != None:
if long_name in registry.invalid_components:
component_meta.enabled = False
component_meta.invalid = True
component_meta.invalid_details = "component contains fields that are not in the schema, disabling"
else:
# if we still have not found the property group, mark it as invalid
component_meta.enabled = False
component_meta.invalid = True
component_meta.invalid_details = "component not present in the schema, possibly renamed? Disabling for now"
# property_group_value_from_custom_property_value(propertyGroup, component_definition, registry, object[component_name])
return (component_meta, propertyGroup)
else:
return(None, None)
def copy_propertyGroup_values_to_another_object(source_object, target_object, component_name, registry):
if source_object == None or target_object == None or component_name == None:
raise Exception('missing input data, cannot copy component propertryGroup')
component_definition = find_component_definition_from_long_name(component_name)
long_name = component_name
property_group_name = registry.get_propertyGroupName_from_longName(long_name)
registry = bpy.context.window_manager.components_registry
source_components_metadata = source_object.components_meta.components
source_componentMeta = next(filter(lambda component: component["long_name"] == long_name, source_components_metadata), None)
# matching component means we already have this type of component
source_propertyGroup = getattr(source_componentMeta, property_group_name)
# now deal with the target object
(_, target_propertyGroup) = upsert_component_in_object(target_object, component_name, registry)
# add to object
value = property_group_value_to_custom_property_value(target_propertyGroup, component_definition, registry, None)
upsert_bevy_component(target_object, long_name, value)
# copy the values over
for field_name in source_propertyGroup.field_names:
if field_name in source_propertyGroup:
target_propertyGroup[field_name] = source_propertyGroup[field_name]
apply_propertyGroup_values_to_object_customProperties(target_object)
# TODO: move to propgroups ?
def apply_propertyGroup_values_to_object_customProperties(object):
cleanup_invalid_metadata(object)
registry = bpy.context.window_manager.components_registry
for component_name in get_bevy_components(object) :
"""if component_name == "components_meta":
continue"""
(_, propertyGroup) = upsert_component_in_object(object, component_name, registry)
component_definition = find_component_definition_from_long_name(component_name)
if component_definition != None:
value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
upsert_bevy_component(object=object, long_name=component_name, value=value)
# apply component value(s) to custom property of a single component
def apply_propertyGroup_values_to_object_customProperties_for_component(object, component_name):
registry = bpy.context.window_manager.components_registry
(_, propertyGroup) = upsert_component_in_object(object, component_name, registry)
component_definition = find_component_definition_from_long_name(component_name)
if component_definition != None:
value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
object[component_name] = value
components_metadata = object.components_meta.components
componentMeta = next(filter(lambda component: component["long_name"] == component_name, components_metadata), None)
if componentMeta:
componentMeta.invalid = False
componentMeta.invalid_details = ""
def apply_customProperty_values_to_object_propertyGroups(object):
print("apply custom properties to ", object.name)
registry = bpy.context.window_manager.components_registry
for component_name in get_bevy_components(object) :
if component_name == "components_meta":
continue
component_definition = find_component_definition_from_long_name(component_name)
if component_definition != None:
property_group_name = registry.get_propertyGroupName_from_longName(component_name)
components_metadata = object.components_meta.components
source_componentMeta = next(filter(lambda component: component["long_name"] == component_name, components_metadata), None)
# matching component means we already have this type of component
propertyGroup = getattr(source_componentMeta, property_group_name, None)
customProperty_value = get_bevy_component_value_by_long_name(object, component_name)
#value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
object["__disable__update"] = True # disable update callback while we set the values of the propertyGroup "tree" (as a propertyGroup can contain other propertyGroups)
property_group_value_from_custom_property_value(propertyGroup, component_definition, registry, customProperty_value)
del object["__disable__update"]
source_componentMeta.invalid = False
source_componentMeta.invalid_details = ""
# removes the given component from the object: removes both the custom property and the matching metadata from the object
def remove_component_from_object(object, component_name):
# remove the component value
remove_bevy_component(object, component_name)
# now remove the component's metadata
components_metadata = getattr(object, "components_meta", None)
if components_metadata == None:
return False
components_metadata = components_metadata.components
to_remove = []
for index, component_meta in enumerate(components_metadata):
long_name = component_meta.long_name
if long_name == component_name:
to_remove.append(index)
break
for index in to_remove:
components_metadata.remove(index)
return True
def add_component_from_custom_property(object):
add_metadata_to_components_without_metadata(object)
apply_customProperty_values_to_object_propertyGroups(object)
def rename_component(object, original_long_name, new_long_name):
registry = bpy.context.window_manager.components_registry
type_infos = registry.type_infos
component_definition = type_infos[new_long_name]
component_ron_value = get_bevy_component_value_by_long_name(object=object, long_name=original_long_name)
if component_ron_value is None and original_long_name in object:
component_ron_value = object[original_long_name]
remove_component_from_object(object, original_long_name)
add_component_to_object(object, component_definition, component_ron_value)
def toggle_component(object, component_name):
components_in_object = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == component_name, components_in_object), None)
if component_meta != None:
component_meta.visible = not component_meta.visible

View File

@ -0,0 +1,321 @@
import ast
import json
import bpy
from bpy_types import Operator
from bpy.props import (StringProperty)
from .metadata import add_component_from_custom_property, add_component_to_object, apply_propertyGroup_values_to_object_customProperties_for_component, copy_propertyGroup_values_to_another_object, get_bevy_component_value_by_long_name, get_bevy_components, is_bevy_component_in_object, remove_component_from_object, rename_component, toggle_component
class AddComponentOperator(Operator):
"""Add Bevy component to object"""
bl_idname = "object.add_bevy_component"
bl_label = "Add component to object Operator"
bl_options = {"UNDO"}
component_type: StringProperty(
name="component_type",
description="component type to add",
) # type: ignore
def execute(self, context):
object = context.object
print("adding component ", self.component_type, "to object '"+object.name+"'")
has_component_type = self.component_type != ""
if has_component_type and object != None:
type_infos = context.window_manager.components_registry.type_infos
component_definition = type_infos[self.component_type]
add_component_to_object(object, component_definition)
return {'FINISHED'}
class CopyComponentOperator(Operator):
"""Copy Bevy component from object"""
bl_idname = "object.copy_bevy_component"
bl_label = "Copy component Operator"
bl_options = {"UNDO"}
source_component_name: StringProperty(
name="source component_name (long)",
description="name of the component to copy",
) # type: ignore
source_object_name: StringProperty(
name="source object name",
description="name of the object to copy the component from",
) # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.copied_source_component_name = StringProperty()
bpy.types.WindowManager.copied_source_object = StringProperty()
@classmethod
def unregister(cls):
del bpy.types.WindowManager.copied_source_component_name
del bpy.types.WindowManager.copied_source_object
def execute(self, context):
if self.source_component_name != '' and self.source_object_name != "":
context.window_manager.copied_source_component_name = self.source_component_name
context.window_manager.copied_source_object = self.source_object_name
else:
self.report({"ERROR"}, "The source object name / component name to copy a component from have not been specified")
return {'FINISHED'}
class PasteComponentOperator(Operator):
"""Paste Bevy component to object"""
bl_idname = "object.paste_bevy_component"
bl_label = "Paste component to object Operator"
bl_options = {"UNDO"}
def execute(self, context):
source_object_name = context.window_manager.copied_source_object
source_object = bpy.data.objects.get(source_object_name, None)
print("source object", source_object)
if source_object == None:
self.report({"ERROR"}, "The source object to copy a component from does not exist")
else:
component_name = context.window_manager.copied_source_component_name
component_value = get_bevy_component_value_by_long_name(source_object, component_name)
if component_value is None:
self.report({"ERROR"}, "The source component to copy from does not exist")
else:
print("pasting component to object: component name:", str(component_name), "component value:" + str(component_value))
print (context.object)
registry = context.window_manager.components_registry
copy_propertyGroup_values_to_another_object(source_object, context.object, component_name, registry)
return {'FINISHED'}
class RemoveComponentOperator(Operator):
"""Remove Bevy component from object"""
bl_idname = "object.remove_bevy_component"
bl_label = "Remove component from object Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to delete",
) # type: ignore
object_name: StringProperty(
name="object name",
description="object whose component to delete",
default=""
) # type: ignore
def execute(self, context):
if self.object_name == "":
object = context.object
else:
object = bpy.data.objects[self.object_name]
print("removing component ", self.component_name, "from object '"+object.name+"'")
if object is not None and 'bevy_components' in object :
component_value = get_bevy_component_value_by_long_name(object, self.component_name)
if component_value is not None:
remove_component_from_object(object, self.component_name)
else :
self.report({"ERROR"}, "The component to remove ("+ self.component_name +") does not exist")
else:
self.report({"ERROR"}, "The object to remove ("+ self.component_name +") from does not exist")
return {'FINISHED'}
class RemoveComponentFromAllObjectsOperator(Operator):
"""Remove Bevy component from all object"""
bl_idname = "object.remove_bevy_component_all"
bl_label = "Remove component from all objects Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name (long name)",
description="component to delete",
) # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.components_remove_progress = bpy.props.FloatProperty(default=-1.0)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_remove_progress
def execute(self, context):
print("removing component ", self.component_name, "from all objects")
total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
if len(object.keys()) > 0:
if object is not None and is_bevy_component_in_object(object, self.component_name):
remove_component_from_object(object, self.component_name)
progress = index / total
context.window_manager.components_remove_progress = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
context.window_manager.components_remove_progress = -1.0
return {'FINISHED'}
class RenameHelper(bpy.types.PropertyGroup):
original_name: bpy.props.StringProperty(name="") # type: ignore
new_name: bpy.props.StringProperty(name="") # type: ignore
#object: bpy.props.PointerProperty(type=bpy.types.Object)
@classmethod
def register(cls):
bpy.types.WindowManager.bevy_component_rename_helper = bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
# remove handlers & co
del bpy.types.WindowManager.bevy_component_rename_helper
class OT_rename_component(Operator):
"""Rename Bevy component"""
bl_idname = "object.rename_bevy_component"
bl_label = "rename component"
bl_options = {"UNDO"}
original_name: bpy.props.StringProperty(default="") # type: ignore
new_name: StringProperty(
name="new_name",
description="new name of component",
) # type: ignore
target_objects: bpy.props.StringProperty() # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.components_rename_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_rename_progress
def execute(self, context):
registry = context.window_manager.components_registry
type_infos = registry.type_infos
settings = context.window_manager.bevy_component_rename_helper
original_name = settings.original_name if self.original_name == "" else self.original_name
new_name = self.new_name
print("renaming components: original name", original_name, "new_name", self.new_name, "targets", self.target_objects)
target_objects = json.loads(self.target_objects)
errors = []
total = len(target_objects)
if original_name != '' and new_name != '' and original_name != new_name and len(target_objects) > 0:
for index, object_name in enumerate(target_objects):
object = bpy.data.objects[object_name]
if object and original_name in get_bevy_components(object) or original_name in object:
try:
# attempt conversion
rename_component(object=object, original_long_name=original_name, new_long_name=new_name)
except Exception as error:
if '__disable__update' in object:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
components_metadata = getattr(object, "components_meta", None)
if components_metadata:
components_metadata = components_metadata.components
component_meta = next(filter(lambda component: component["long_name"] == new_name, components_metadata), None)
if component_meta:
component_meta.invalid = True
component_meta.invalid_details = "wrong custom property value, overwrite them by changing the values in the ui or change them & regenerate"
errors.append( "wrong custom property values to generate target component: object: '" + object.name + "', error: " + str(error))
progress = index / total
context.window_manager.components_rename_progress = progress
try:
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
except: pass # this is to allow this to run in cli/headless mode
if len(errors) > 0:
self.report({'ERROR'}, "Failed to rename component: Errors:" + str(errors))
else:
self.report({'INFO'}, "Sucessfully renamed component")
#clear data after we are done
self.original_name = ""
context.window_manager.bevy_component_rename_helper.original_name = ""
context.window_manager.components_rename_progress = -1.0
return {'FINISHED'}
class GenerateComponent_From_custom_property_Operator(Operator):
"""Generate Bevy components from custom property"""
bl_idname = "object.generate_bevy_component_from_custom_property"
bl_label = "Generate component from custom_property Operator"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to generate custom properties for",
) # type: ignore
def execute(self, context):
object = context.object
error = False
try:
add_component_from_custom_property(object)
except Exception as error:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Error:" + str(error))
if not error:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for selected object")
return {'FINISHED'}
class Fix_Component_Operator(Operator):
"""Attempt to fix Bevy component"""
bl_idname = "object.fix_bevy_component"
bl_label = "Fix component (attempts to)"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to fix",
) # type: ignore
def execute(self, context):
object = context.object
error = False
try:
apply_propertyGroup_values_to_object_customProperties_for_component(object, self.component_name)
except Exception as error:
if "__disable__update" in object:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True
self.report({'ERROR'}, "Failed to fix component: Error:" + str(error))
if not error:
self.report({'INFO'}, "Sucessfully fixed component (please double check component & its custom property value)")
return {'FINISHED'}
class Toggle_ComponentVisibility(Operator):
"""Toggle Bevy component's visibility"""
bl_idname = "object.toggle_bevy_component_visibility"
bl_label = "Toggle component visibility"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component name",
description="component to toggle",
) # type: ignore
def execute(self, context):
object = context.object
toggle_component(object, self.component_name)
return {'FINISHED'}

View File

@ -0,0 +1,279 @@
import json
import bpy
from ..registry.operators import COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT
from .metadata import do_object_custom_properties_have_missing_metadata, get_bevy_components
from .operators import AddComponentOperator, CopyComponentOperator, Fix_Component_Operator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, Toggle_ComponentVisibility
def draw_propertyGroup( propertyGroup, layout, nesting =[], rootName=None):
is_enum = getattr(propertyGroup, "with_enum")
is_list = getattr(propertyGroup, "with_list")
is_map = getattr(propertyGroup, "with_map")
# item in our components hierarchy can get the correct propertyGroup by STRINGS because of course, we cannot pass objects to operators...sigh
# if it is an enum, the first field name is always the list of enum variants, the others are the variants
field_names = propertyGroup.field_names
#print("")
#print("drawing", propertyGroup, nesting, "component_name", rootName)
if is_enum:
subrow = layout.row()
display_name = field_names[0] if propertyGroup.tupple_or_struct == "struct" else ""
subrow.prop(propertyGroup, field_names[0], text=display_name)
subrow.separator()
selection = getattr(propertyGroup, "selection")
for fname in field_names[1:]:
if fname == "variant_" + selection:
subrow = layout.row()
display_name = fname if propertyGroup.tupple_or_struct == "struct" else ""
nestedPropertyGroup = getattr(propertyGroup, fname)
nested = getattr(nestedPropertyGroup, "nested", False)
#print("nestedPropertyGroup", nestedPropertyGroup, fname, nested)
if nested:
draw_propertyGroup(nestedPropertyGroup, subrow.column(), nesting + [fname], rootName )
# if an enum variant is not a propertyGroup
break
elif is_list:
item_list = getattr(propertyGroup, "list")
list_index = getattr(propertyGroup, "list_index")
box = layout.box()
split = box.split(factor=0.9)
list_column, buttons_column = (split.column(),split.column())
list_column = list_column.box()
for index, item in enumerate(item_list):
row = list_column.row()
draw_propertyGroup(item, row, nesting, rootName)
icon = 'CHECKBOX_HLT' if list_index == index else 'CHECKBOX_DEHLT'
op = row.operator('generic_list.select_item', icon=icon, text="")
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
op.selection_index = index
#various control buttons
buttons_column.separator()
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='ADD', text="")
op.action = 'ADD'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='REMOVE', text="")
op.action = 'REMOVE'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
buttons_column.separator()
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='TRIA_UP', text="")
op.action = 'UP'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
row = buttons_column.row()
op = row.operator('generic_list.list_action', icon='TRIA_DOWN', text="")
op.action = 'DOWN'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
elif is_map:
root = layout.row().column()
if hasattr(propertyGroup, "list"): # TODO: improve handling of non drawable UI
keys_list = getattr(propertyGroup, "list")
values_list = getattr(propertyGroup, "values_list")
box = root.box()
row = box.row()
row.label(text="Add entry:")
keys_setter = getattr(propertyGroup, "keys_setter")
draw_propertyGroup(keys_setter, row, nesting, rootName)
values_setter = getattr(propertyGroup, "values_setter")
draw_propertyGroup(values_setter, row, nesting, rootName)
op = row.operator('generic_map.map_action', icon='ADD', text="")
op.action = 'ADD'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
box = root.box()
split = box.split(factor=0.9)
list_column, buttons_column = (split.column(),split.column())
list_column = list_column.box()
for index, item in enumerate(keys_list):
row = list_column.row()
draw_propertyGroup(item, row, nesting, rootName)
value = values_list[index]
draw_propertyGroup(value, row, nesting, rootName)
op = row.operator('generic_map.map_action', icon='REMOVE', text="")
op.action = 'REMOVE'
op.component_name = rootName
op.property_group_path = json.dumps(nesting)
op.target_index = index
#various control buttons
buttons_column.separator()
row = buttons_column.row()
else:
for fname in field_names:
#subrow = layout.row()
nestedPropertyGroup = getattr(propertyGroup, fname)
nested = getattr(nestedPropertyGroup, "nested", False)
display_name = fname if propertyGroup.tupple_or_struct == "struct" else ""
if nested:
layout.separator()
layout.separator()
layout.label(text=display_name) # this is the name of the field/sub field
layout.separator()
subrow = layout.row()
draw_propertyGroup(nestedPropertyGroup, subrow, nesting + [fname], rootName )
else:
subrow = layout.row()
subrow.prop(propertyGroup, fname, text=display_name)
subrow.separator()
class BEVY_COMPONENTS_PT_ComponentsPanel(bpy.types.Panel):
bl_idname = "BEVY_COMPONENTS_PT_ComponentsPanel"
bl_label = ""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BLENVY_PT_SidePanel"
@classmethod
def poll(cls, context):
return context.window_manager.blenvy.mode == 'COMPONENTS'
return context.object is not None
def draw_header(self, context):
layout = self.layout
name = context.object.name if context.object != None else ''
layout.label(text="Components For "+ name)
def draw(self, context):
object = context.object
layout = self.layout
# we get & load our component registry
registry = bpy.context.window_manager.components_registry
available_components = bpy.context.window_manager.components_list
registry_has_type_infos = registry.has_type_infos()
if object is not None:
row = layout.row(align=True)
row.prop(available_components, "list", text="Component")
row.prop(available_components, "filter",text="Filter")
# add components
row = layout.row(align=True)
op = row.operator(AddComponentOperator.bl_idname, text="Add", icon="ADD")
op.component_type = available_components.list
row.enabled = available_components.list != ''
layout.separator()
# paste components
row = layout.row(align=True)
row.operator(PasteComponentOperator.bl_idname, text="Paste component ("+bpy.context.window_manager.copied_source_component_name+")", icon="PASTEDOWN")
row.enabled = registry_has_type_infos and context.window_manager.copied_source_object != ''
layout.separator()
# upgrate custom props to components
upgradeable_customProperties = registry.has_type_infos() and do_object_custom_properties_have_missing_metadata(context.object)
if upgradeable_customProperties:
row = layout.row(align=True)
op = row.operator(GenerateComponent_From_custom_property_Operator.bl_idname, text="generate components from custom properties" , icon="LOOP_FORWARDS")
layout.separator()
components_in_object = object.components_meta.components
#print("components_names", dict(components_bla).keys())
for component_name in sorted(get_bevy_components(object)) : # sorted by component name, practical
#print("component_name", component_name)
if component_name == "components_meta":
continue
# anything withouth metadata gets skipped, we only want to see real components, not all custom props
component_meta = next(filter(lambda component: component["long_name"] == component_name, components_in_object), None)
if component_meta == None:
continue
component_invalid = getattr(component_meta, "invalid")
invalid_details = getattr(component_meta, "invalid_details")
component_visible = getattr(component_meta, "visible")
single_field = False
# our whole row
box = layout.box()
row = box.row(align=True)
# "header"
row.alert = component_invalid
row.prop(component_meta, "enabled", text="")
row.label(text=component_name)
# we fetch the matching ui property group
root_propertyGroup_name = registry.get_propertyGroupName_from_longName(component_name)
"""print("root_propertyGroup_name", root_propertyGroup_name)"""
print("component_meta", component_meta, component_invalid)
if root_propertyGroup_name:
propertyGroup = getattr(component_meta, root_propertyGroup_name, None)
"""print("propertyGroup", propertyGroup)"""
if propertyGroup:
# if the component has only 0 or 1 field names, display inline, otherwise change layout
single_field = len(propertyGroup.field_names) < 2
prop_group_location = box.row(align=True).column()
"""if single_field:
prop_group_location = row.column(align=True)#.split(factor=0.9)#layout.row(align=False)"""
if component_visible:
if component_invalid:
error_message = invalid_details if component_invalid else "Missing component UI data, please reload registry !"
prop_group_location.label(text=error_message)
draw_propertyGroup(propertyGroup, prop_group_location, [root_propertyGroup_name], component_name)
else :
row.label(text="details hidden, click on toggle to display")
else:
error_message = invalid_details if component_invalid else "Missing component UI data, please reload registry !"
row.label(text=error_message)
# "footer" with additional controls
if component_invalid:
if root_propertyGroup_name:
propertyGroup = getattr(component_meta, root_propertyGroup_name, None)
if propertyGroup:
unit_struct = len(propertyGroup.field_names) == 0
if unit_struct:
op = row.operator(Fix_Component_Operator.bl_idname, text="", icon="SHADERFX")
op.component_name = component_name
row.separator()
op = row.operator(RemoveComponentOperator.bl_idname, text="", icon="X")
op.component_name = component_name
row.separator()
op = row.operator(CopyComponentOperator.bl_idname, text="", icon="COPYDOWN")
op.source_component_name = component_name
op.source_object_name = object.name
row.separator()
#if not single_field:
toggle_icon = "TRIA_DOWN" if component_visible else "TRIA_RIGHT"
op = row.operator(Toggle_ComponentVisibility.bl_idname, text="", icon=toggle_icon)
op.component_name = component_name
#row.separator()
else:
layout.label(text ="Select an object to edit its components")

View File

@ -0,0 +1,19 @@
import bpy
import json
# Makes an empty, at the specified location, rotation, scale 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, rotation, scale, collection):
object_data = None
empty_obj = bpy.data.objects.new( name, object_data )
empty_obj.empty_display_size = 2
empty_obj.empty_display_type = 'PLAIN_AXES'
empty_obj.name = name
empty_obj.location = location
empty_obj.scale = scale
empty_obj.rotation_euler = rotation
collection.objects.link( empty_obj )
#bpy.context.view_layer.update()
return empty_obj

View File

@ -0,0 +1,178 @@
from bpy_types import PropertyGroup
conversion_tables = {
"bool": lambda value: value,
"char": lambda value: '"'+value+'"',
"str": lambda value: '"'+value+'"',
"alloc::string::String": lambda value: '"'+str(value)+'"',
"alloc::borrow::Cow<str>": lambda value: '"'+str(value)+'"',
"glam::Vec2": lambda value: "Vec2(x:"+str(value[0])+ ", y:"+str(value[1])+")",
"glam::DVec2": lambda value: "DVec2(x:"+str(value[0])+ ", y:"+str(value[1])+")",
"glam::UVec2": lambda value: "UVec2(x:"+str(value[0])+ ", y:"+str(value[1])+")",
"glam::Vec3": lambda value: "Vec3(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+")",
"glam::Vec3A": lambda value: "Vec3A(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+")",
"glam::UVec3": lambda value: "UVec3(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+")",
"glam::Vec4": lambda value: "Vec4(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+ ", w:"+str(value[3])+")",
"glam::DVec4": lambda value: "DVec4(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+ ", w:"+str(value[3])+")",
"glam::UVec4": lambda value: "UVec4(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+ ", w:"+str(value[3])+")",
"glam::Quat": lambda value: "Quat(x:"+str(value[0])+ ", y:"+str(value[1])+ ", z:"+str(value[2])+ ", w:"+str(value[3])+")",
"bevy_render::color::Color": lambda value: "Rgba(red:"+str(value[0])+ ", green:"+str(value[1])+ ", blue:"+str(value[2])+ ", alpha:"+str(value[3])+ ")",
}
#converts the value of a property group(no matter its complexity) into a single custom property value
# this is more or less a glorified "to_ron()" method (not quite but close to)
def property_group_value_to_custom_property_value(property_group, definition, registry, parent=None, value=None):
long_name = definition["long_name"]
type_info = definition["typeInfo"] if "typeInfo" in definition else None
type_def = definition["type"] if "type" in definition else None
is_value_type = long_name in conversion_tables
# print("computing custom property: component name:", long_name, "type_info", type_info, "type_def", type_def, "value", value)
if is_value_type:
value = conversion_tables[long_name](value)
elif type_info == "Struct":
values = {}
if len(property_group.field_names) ==0:
value = '()'
else:
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["properties"][field_name]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = property_group_value_to_custom_property_value(child_property_group, item_definition, registry, parent=long_name, value=value)
else:
value = '""'
values[field_name] = value
value = values
elif type_info == "Tuple":
values = {}
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = property_group_value_to_custom_property_value(child_property_group, item_definition, registry, parent=long_name, value=value)
else:
value = '""'
values[field_name] = value
value = tuple(e for e in list(values.values()))
elif type_info == "TupleStruct":
values = {}
for index, field_name in enumerate(property_group.field_names):
#print("toto", index, definition["prefixItems"][index]["type"]["$ref"])
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = property_group_value_to_custom_property_value(child_property_group, item_definition, registry, parent=long_name, value=value)
else:
value = '""'
values[field_name] = value
value = tuple(e for e in list(values.values()))
elif type_info == "Enum":
selected = getattr(property_group, "selection")
if type_def == "object":
selection_index = property_group.field_names.index("variant_"+selected)
variant_name = property_group.field_names[selection_index]
variant_definition = definition["oneOf"][selection_index-1]
if "prefixItems" in variant_definition:
value = getattr(property_group, variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
value = property_group_value_to_custom_property_value(child_property_group, variant_definition, registry, parent=long_name, value=value)
value = selected + str(value,) #"{}{},".format(selected ,value)
elif "properties" in variant_definition:
value = getattr(property_group, variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
value = property_group_value_to_custom_property_value(child_property_group, variant_definition, registry, parent=long_name, value=value)
value = selected + str(value,)
else:
value = getattr(property_group, variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if child_property_group:
value = property_group_value_to_custom_property_value(child_property_group, variant_definition, registry, parent=long_name, value=value)
value = selected + str(value,)
else:
value = selected # here the value of the enum is just the name of the variant
else:
value = selected
elif type_info == "List":
item_list = getattr(property_group, "list")
value = []
for item in item_list:
item_long_name = getattr(item, "long_name")
definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
if definition != None:
item_value = property_group_value_to_custom_property_value(item, definition, registry, long_name, None)
if item_long_name.startswith("wrapper_"): #if we have a "fake" tupple for aka for value types, we need to remove one nested level
item_value = item_value[0]
else:
item_value = '""'
value.append(item_value)
elif type_info == "Map":
keys_list = getattr(property_group, "list", {})
values_list = getattr(property_group, "values_list")
value = {}
for index, key in enumerate(keys_list):
# first get the keys
key_long_name = getattr(key, "long_name")
definition = registry.type_infos[key_long_name] if key_long_name in registry.type_infos else None
if definition != None:
key_value = property_group_value_to_custom_property_value(key, definition, registry, long_name, None)
if key_long_name.startswith("wrapper_"): #if we have a "fake" tupple for aka for value types, we need to remove one nested level
key_value = key_value[0]
else:
key_value = '""'
# and then the values
val = values_list[index]
value_long_name = getattr(val, "long_name")
definition = registry.type_infos[value_long_name] if value_long_name in registry.type_infos else None
if definition != None:
val_value = property_group_value_to_custom_property_value(val, definition, registry, long_name, None)
if value_long_name.startswith("wrapper_"): #if we have a "fake" tupple for aka for value types, we need to remove one nested level
val_value = val_value[0]
else:
val_value = '""'
value[key_value] = val_value
value = str(value).replace('{','@').replace('}','²') # FIXME: eeek !!
else:
value = conversion_tables[long_name](value) if is_value_type else value
value = '""' if isinstance(value, PropertyGroup) else value
#print("generating custom property value", value, type(value))
if isinstance(value, str):
value = value.replace("'", "")
if parent == None:
value = str(value).replace("'", "")
value = value.replace(",)",")")
value = value.replace("{", "(").replace("}", ")") # FIXME: deal with hashmaps
value = value.replace("True", "true").replace("False", "false")
value = value.replace('@', '{').replace('²', '}')
return value

View File

@ -0,0 +1,312 @@
from bpy_types import PropertyGroup
import re
def parse_struct_string(string, start_nesting=0):
#print("processing struct string", string, "start_nesting", start_nesting)
fields = {}
buff = []
current_fieldName = None
nesting_level = 0
start_offset = 0
end_offset = 0
for index, char in enumerate(string):
buff.append(char)
if char == "," and nesting_level == start_nesting:
#print("first case", end_offset)
end_offset = index
end_offset = len(string) if end_offset == 0 else end_offset
val = "".join(string[start_offset:end_offset])
fields[current_fieldName] = val.strip()
start_offset = index + 1
#print("done with field name", current_fieldName, "value", fields[current_fieldName])
if char == "[" or char == "(":
nesting_level += 1
if nesting_level == start_nesting:
start_offset = index + 1
#print("nesting & setting start offset", start_offset)
#print("nesting down", nesting_level)
if char == "]" or char == ")" :
#print("nesting up", nesting_level)
if nesting_level == start_nesting:
end_offset = index
#print("unesting & setting end offset", end_offset)
nesting_level -= 1
if char == ":" and nesting_level == start_nesting:
end_offset = index
fieldName = "".join(string[start_offset:end_offset])
current_fieldName = fieldName.strip()
start_offset = index + 1
end_offset = 0 #hack
#print("starting field name", fieldName, "index", index)
buff = []
end_offset = len(string) if end_offset == 0 else end_offset
#print("final start and end offset", start_offset, end_offset, "total length", len(string))
val = "".join(string[start_offset:end_offset])
fields[current_fieldName] = val.strip()
#print("done with all fields", fields)
return fields
def parse_tuplestruct_string(string, start_nesting=0):
#print("processing tuppleStruct", string, "start_nesting", start_nesting)
fields = []
buff = []
nesting_level = 0
field_index = 0
start_offset = 0
end_offset = 0
# todo: strip all stuff before start_nesting
for index, char in enumerate(string):
buff.append(char)
if char == "," and nesting_level == start_nesting:
end_offset = index
end_offset = len(string) if end_offset == 0 else end_offset
val = "".join(string[start_offset:end_offset])
fields.append(val.strip())
field_index += 1
#print("start and end offset", start_offset, end_offset, "total length", len(string))
#print("done with field name", field_index, "value", fields)
start_offset = index + 1
end_offset = 0 # hack
if char == "[" or char == "(":
nesting_level += 1
if nesting_level == start_nesting:
start_offset = index + 1
#print("nesting & setting start offset", start_offset)
#print("nesting down", nesting_level)
if char == "]" or char == ")" :
if nesting_level == start_nesting:
end_offset = index
#print("unesting & setting end offset", end_offset)
#print("nesting up", nesting_level)
nesting_level -= 1
end_offset = len(string) if end_offset == 0 else end_offset
#print("final start and end offset", start_offset, end_offset, "total length", len(string))
val = "".join(string[start_offset:end_offset]) #if end_offset != 0 else buff)
fields.append(val.strip())
fields = list(filter(lambda entry: entry != '', fields))
#print("done with all fields", fields)
return fields
def parse_vec2(value, caster, typeName):
parsed = parse_struct_string(value.replace(typeName,"").replace("(", "").replace(")","") )
return [caster(parsed['x']), caster(parsed['y'])]
def parse_vec3(value, caster, typeName):
parsed = parse_struct_string(value.replace(typeName,"").replace("(", "").replace(")","") )
return [caster(parsed['x']), caster(parsed['y']), caster(parsed['z'])]
def parse_vec4(value, caster, typeName):
parsed = parse_struct_string(value.replace(typeName,"").replace("(", "").replace(")","") )
return [caster(parsed['x']), caster(parsed['y']), caster(parsed['z']), caster(parsed['w'])]
def parse_color(value, caster, typeName):
parsed = parse_struct_string(value.replace(typeName,"").replace("(", "").replace(")","") )
return [caster(parsed['red']), caster(parsed['green']), caster(parsed['blue']), caster(parsed['alpha'])]
def to_int(input):
return int(float(input))
type_mappings = {
"bool": lambda value: True if value == "true" else False,
"u8": lambda value: int(value),
"u16": lambda value: int(value),
"u32": lambda value: int(value),
"u64": lambda value: int(value),
"u128": lambda value: int(value),
"u64": lambda value: int(value),
"usize": lambda value: int(value),
"i8": lambda value: int(value),
"i16": lambda value: int(value),
"i32": lambda value: int(value),
"i64": lambda value: int(value),
"i128": lambda value: int(value),
"isize": lambda value: int(value),
'f32': lambda value: float(value),
'f64': lambda value: float(value),
"glam::Vec2": lambda value: parse_vec2(value, float, "Vec2"),
"glam::DVec2": lambda value: parse_vec2(value, float, "DVec2"),
"glam::UVec2": lambda value: parse_vec2(value, to_int, "UVec2"),
'glam::Vec3': lambda value: parse_vec3(value, float, "Vec3"),
"glam::Vec3A": lambda value: parse_vec3(value, float, "Vec3A"),
"glam::UVec3": lambda value: parse_vec3(value, to_int, "UVec3"),
"glam::Vec4": lambda value: parse_vec4(value, float, "Vec4"),
"glam::DVec4": lambda value: parse_vec4(value, float, "DVec4"),
"glam::UVec4": lambda value: parse_vec4(value, to_int, "UVec4"),
"glam::Quat": lambda value: parse_vec4(value, float, "Quat"),
'alloc::string::String': lambda value: str(value.replace('"', "")),
'alloc::borrow::Cow<str>': lambda value: str(value.replace('"', "")),
'bevy_render::color::Color': lambda value: parse_color(value, float, "Rgba"),
'bevy_ecs::entity::Entity': lambda value: int(value),
}
def is_def_value_type(definition, registry):
if definition == None:
return True
value_types_defaults = registry.value_types_defaults
long_name = definition["long_name"]
is_value_type = long_name in value_types_defaults
return is_value_type
#converts the value of a single custom property into a value (values) of a property group
def property_group_value_from_custom_property_value(property_group, definition, registry, value, nesting = []):
value_types_defaults = registry.value_types_defaults
type_info = definition["typeInfo"] if "typeInfo" in definition else None
type_def = definition["type"] if "type" in definition else None
properties = definition["properties"] if "properties" in definition else {}
prefixItems = definition["prefixItems"] if "prefixItems" in definition else []
long_name = definition["long_name"]
#is_value_type = type_def in value_types_defaults or long_name in value_types_defaults
is_value_type = long_name in value_types_defaults
nesting = nesting + [definition["short_name"]]
if is_value_type:
value = value.replace("(", "").replace(")", "")# FIXME: temporary, incoherent use of nesting levels between parse_tuplestruct_string & parse_struct_string
value = type_mappings[long_name](value) if long_name in type_mappings else value
return value
elif type_info == "Struct":
if len(property_group.field_names) != 0 :
custom_property_values = parse_struct_string(value, start_nesting=1 if value.startswith("(") else 0)
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["properties"][field_name]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
custom_prop_value = custom_property_values[field_name]
#print("field name", field_name, "value", custom_prop_value)
propGroup_value = getattr(property_group, field_name)
is_property_group = isinstance(propGroup_value, PropertyGroup)
child_property_group = propGroup_value if is_property_group else None
if item_definition != None:
custom_prop_value = property_group_value_from_custom_property_value(child_property_group, item_definition, registry, value=custom_prop_value, nesting=nesting)
else:
custom_prop_value = custom_prop_value
if is_def_value_type(item_definition, registry):
setattr(property_group , field_name, custom_prop_value)
else:
if len(value) > 2: #a unit struct should be two chars long :()
#print("struct with zero fields")
raise Exception("input string too big for a unit struct")
elif type_info == "Tuple":
custom_property_values = parse_tuplestruct_string(value, start_nesting=1 if len(nesting) == 1 else 1)
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
custom_property_value = custom_property_values[index]
propGroup_value = getattr(property_group, field_name)
is_property_group = isinstance(propGroup_value, PropertyGroup)
child_property_group = propGroup_value if is_property_group else None
if item_definition != None:
custom_property_value = property_group_value_from_custom_property_value(child_property_group, item_definition, registry, value=custom_property_value, nesting=nesting)
if is_def_value_type(item_definition, registry):
setattr(property_group , field_name, custom_property_value)
elif type_info == "TupleStruct":
custom_property_values = parse_tuplestruct_string(value, start_nesting=1 if len(nesting) == 1 else 0)
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
custom_prop_value = custom_property_values[index]
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
custom_prop_value = property_group_value_from_custom_property_value(child_property_group, item_definition, registry, value=custom_prop_value, nesting=nesting)
if is_def_value_type(item_definition, registry):
setattr(property_group , field_name, custom_prop_value)
elif type_info == "Enum":
field_names = property_group.field_names
if type_def == "object":
regexp = re.search('(^[^\(]+)(\((.*)\))', value)
try:
chosen_variant_raw = regexp.group(1)
chosen_variant_value = regexp.group(3)
chosen_variant_name = "variant_" + chosen_variant_raw
except:
chosen_variant_raw = value
chosen_variant_value = ""
chosen_variant_name = "variant_" + chosen_variant_raw
selection_index = property_group.field_names.index(chosen_variant_name)
variant_definition = definition["oneOf"][selection_index-1]
# first we set WHAT variant is selected
setattr(property_group, "selection", chosen_variant_raw)
# and then we set the value of the variant
if "prefixItems" in variant_definition:
value = getattr(property_group, chosen_variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
chosen_variant_value = "(" +chosen_variant_value +")" # needed to handle nesting correctly
value = property_group_value_from_custom_property_value(child_property_group, variant_definition, registry, value=chosen_variant_value, nesting=nesting)
elif "properties" in variant_definition:
value = getattr(property_group, chosen_variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
value = property_group_value_from_custom_property_value(child_property_group, variant_definition, registry, value=chosen_variant_value, nesting=nesting)
else:
chosen_variant_raw = value
setattr(property_group, field_names[0], chosen_variant_raw)
elif type_info == "List":
item_list = getattr(property_group, "list")
item_long_name = getattr(property_group, "long_name")
custom_property_values = parse_tuplestruct_string(value, start_nesting=2 if item_long_name.startswith("wrapper_") and value.startswith('(') else 1) # TODO : the additional check here is wrong, there is an issue somewhere in higher level stuff
# clear list first
item_list.clear()
for raw_value in custom_property_values:
new_entry = item_list.add()
item_long_name = getattr(new_entry, "long_name") # we get the REAL type name
definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
if definition != None:
property_group_value_from_custom_property_value(new_entry, definition, registry, value=raw_value, nesting=nesting)
else:
try:
value = value.replace("(", "").replace(")", "")# FIXME: temporary, incoherent use of nesting levels between parse_tuplestruct_string & parse_struct_string
value = type_mappings[long_name](value) if long_name in type_mappings else value
return value
except:
pass

View File

@ -0,0 +1,95 @@
import bpy
from bpy_types import PropertyGroup
from bpy.props import (PointerProperty)
from . import process_structs
from . import process_tupples
from . import process_enum
from . import process_list
from . import process_map
def process_component(registry, definition, update, extras=None, nesting = [], nesting_long_names = []):
long_name = definition['long_name']
short_name = definition["short_name"]
type_info = definition["typeInfo"] if "typeInfo" in definition else None
type_def = definition["type"] if "type" in definition else None
properties = definition["properties"] if "properties" in definition else {}
prefixItems = definition["prefixItems"] if "prefixItems" in definition else []
has_properties = len(properties.keys()) > 0
has_prefixItems = len(prefixItems) > 0
is_enum = type_info == "Enum"
is_list = type_info == "List"
is_map = type_info == "Map"
__annotations__ = {}
tupple_or_struct = None
with_properties = False
with_items = False
with_enum = False
with_list = False
with_map = False
if has_properties:
__annotations__ = __annotations__ | process_structs.process_structs(registry, definition, properties, update, nesting, nesting_long_names)
with_properties = True
tupple_or_struct = "struct"
if has_prefixItems:
__annotations__ = __annotations__ | process_tupples.process_tupples(registry, definition, prefixItems, update, nesting, nesting_long_names)
with_items = True
tupple_or_struct = "tupple"
if is_enum:
__annotations__ = __annotations__ | process_enum.process_enum(registry, definition, update, nesting, nesting_long_names)
with_enum = True
if is_list:
__annotations__ = __annotations__ | process_list.process_list(registry, definition, update, nesting, nesting_long_names)
with_list= True
if is_map:
__annotations__ = __annotations__ | process_map.process_map(registry, definition, update, nesting, nesting_long_names)
with_map = True
field_names = []
for a in __annotations__:
field_names.append(a)
extras = extras if extras is not None else {
"long_name": long_name
}
root_component = nesting_long_names[0] if len(nesting_long_names) > 0 else long_name
# print("")
property_group_params = {
**extras,
'__annotations__': __annotations__,
'tupple_or_struct': tupple_or_struct,
'field_names': field_names,
**dict(with_properties = with_properties, with_items= with_items, with_enum= with_enum, with_list= with_list, with_map = with_map, short_name= short_name, long_name=long_name),
'root_component': root_component
}
#FIXME: YIKES, but have not found another way:
""" Withouth this ; the following does not work
-BasicTest
- NestingTestLevel2
-BasicTest => the registration & update callback of this one overwrites the first "basicTest"
have not found a cleaner workaround so far
"""
property_group_name = registry.generate_propGroup_name(nesting, long_name)
(property_group_pointer, property_group_class) = property_group_from_infos(property_group_name, property_group_params)
# add our component propertyGroup to the registry
registry.register_component_propertyGroup(property_group_name, property_group_pointer)
return (property_group_pointer, property_group_class)
def property_group_from_infos(property_group_name, property_group_parameters):
# print("creating property group", property_group_name)
property_group_class = type(property_group_name, (PropertyGroup,), property_group_parameters)
bpy.utils.register_class(property_group_class)
property_group_pointer = PointerProperty(type=property_group_class)
return (property_group_pointer, property_group_class)

View File

@ -0,0 +1,67 @@
from bpy.props import (StringProperty)
from . import process_component
def process_enum(registry, definition, update, nesting, nesting_long_names):
blender_property_mapping = registry.blender_property_mapping
short_name = definition["short_name"]
long_name = definition["long_name"]
type_def = definition["type"] if "type" in definition else None
variants = definition["oneOf"]
nesting = nesting + [short_name]
nesting_long_names = nesting_long_names = [long_name]
__annotations__ = {}
original_type_name = "enum"
# print("processing enum", short_name, long_name, definition)
if type_def == "object":
labels = []
additional_annotations = {}
for variant in variants:
variant_name = variant["long_name"]
variant_prefixed_name = "variant_" + variant_name
labels.append(variant_name)
if "prefixItems" in variant:
#print("tupple variant in enum", variant)
registry.add_custom_type(variant_name, variant)
(sub_component_group, _) = process_component.process_component(registry, variant, update, {"nested": True}, nesting, nesting_long_names)
additional_annotations[variant_prefixed_name] = sub_component_group
elif "properties" in variant:
#print("struct variant in enum", variant)
registry.add_custom_type(variant_name, variant)
(sub_component_group, _) = process_component.process_component(registry, variant, update, {"nested": True}, nesting, nesting_long_names)
additional_annotations[variant_prefixed_name] = sub_component_group
else: # for the cases where it's neither a tupple nor a structs: FIXME: not 100% sure of this
#print("other variant in enum")
annotations = {"variant_"+variant_name: StringProperty(default="----<ignore_field>----")}
additional_annotations = additional_annotations | annotations
items = tuple((e, e, e) for e in labels)
blender_property_def = blender_property_mapping[original_type_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
items=items, # this is needed by Blender's EnumProperty , which we are using here
update= update
)
__annotations__["selection"] = blender_property
for a in additional_annotations:
__annotations__[a] = additional_annotations[a]
# enum_value => what field to display
# a second field + property for the "content" of the enum
else:
items = tuple((e, e, "") for e in variants)
blender_property_def = blender_property_mapping[original_type_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
items=items,
update= update
)
__annotations__["selection"] = blender_property
return __annotations__

View File

@ -0,0 +1,37 @@
from bpy.props import (StringProperty, IntProperty, CollectionProperty)
from .utils import generate_wrapper_propertyGroup
from . import process_component
def process_list(registry, definition, update, nesting=[], nesting_long_names=[]):
value_types_defaults = registry.value_types_defaults
type_infos = registry.type_infos
short_name = definition["short_name"]
long_name = definition["long_name"]
ref_name = definition["items"]["type"]["$ref"].replace("#/$defs/", "")
nesting = nesting+[short_name]
nesting_long_names = nesting_long_names + [long_name]
item_definition = type_infos[ref_name]
item_long_name = item_definition["long_name"]
is_item_value_type = item_long_name in value_types_defaults
property_group_class = None
#if the content of the list is a unit type, we need to generate a fake wrapper, otherwise we cannot use layout.prop(group, "propertyName") as there is no propertyName !
if is_item_value_type:
property_group_class = generate_wrapper_propertyGroup(long_name, item_long_name, definition["items"]["type"]["$ref"], registry, update)
else:
(_, list_content_group_class) = process_component.process_component(registry, item_definition, update, {"nested": True, "long_name": item_long_name}, nesting)
property_group_class = list_content_group_class
item_collection = CollectionProperty(type=property_group_class)
item_long_name = item_long_name if not is_item_value_type else "wrapper_" + item_long_name
__annotations__ = {
"list": item_collection,
"list_index": IntProperty(name = "Index for list", default = 0, update=update),
"long_name": StringProperty(default=item_long_name)
}
return __annotations__

View File

@ -0,0 +1,85 @@
from bpy.props import (StringProperty, IntProperty, CollectionProperty, PointerProperty)
from .utils import generate_wrapper_propertyGroup
from . import process_component
def process_map(registry, definition, update, nesting=[], nesting_long_names=[]):
value_types_defaults = registry.value_types_defaults
type_infos = registry.type_infos
short_name = definition["short_name"]
long_name = definition["long_name"]
nesting = nesting + [short_name]
nesting_long_names = nesting_long_names + [long_name]
value_ref_name = definition["valueType"]["type"]["$ref"].replace("#/$defs/", "")
key_ref_name = definition["keyType"]["type"]["$ref"].replace("#/$defs/", "")
#print("definition", definition)
__annotations__ = {}
if key_ref_name in type_infos:
key_definition = type_infos[key_ref_name]
original_long_name = key_definition["long_name"]
is_key_value_type = original_long_name in value_types_defaults
definition_link = definition["keyType"]["type"]["$ref"]
#if the content of the list is a unit type, we need to generate a fake wrapper, otherwise we cannot use layout.prop(group, "propertyName") as there is no propertyName !
if is_key_value_type:
keys_property_group_class = generate_wrapper_propertyGroup(f"{long_name}_keys", original_long_name, definition_link, registry, update)
else:
(_, list_content_group_class) = process_component.process_component(registry, key_definition, update, {"nested": True, "long_name": original_long_name}, nesting, nesting_long_names)
keys_property_group_class = list_content_group_class
keys_collection = CollectionProperty(type=keys_property_group_class)
keys_property_group_pointer = PointerProperty(type=keys_property_group_class)
else:
__annotations__["list"] = StringProperty(default="N/A")
registry.add_missing_typeInfo(key_ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
if value_ref_name in type_infos:
value_definition = type_infos[value_ref_name]
original_long_name = value_definition["long_name"]
is_value_value_type = original_long_name in value_types_defaults
definition_link = definition["valueType"]["type"]["$ref"]
#if the content of the list is a unit type, we need to generate a fake wrapper, otherwise we cannot use layout.prop(group, "propertyName") as there is no propertyName !
if is_value_value_type:
values_property_group_class = generate_wrapper_propertyGroup(f"{long_name}_values", original_long_name, definition_link, registry, update)
else:
(_, list_content_group_class) = process_component.process_component(registry, value_definition, update, {"nested": True, "long_name": original_long_name}, nesting, nesting_long_names)
values_property_group_class = list_content_group_class
values_collection = CollectionProperty(type=values_property_group_class)
values_property_group_pointer = PointerProperty(type=values_property_group_class)
else:
#__annotations__["list"] = StringProperty(default="N/A")
registry.add_missing_typeInfo(value_ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
if key_ref_name in type_infos and value_ref_name in type_infos:
__annotations__ = {
"list": keys_collection,
"list_index": IntProperty(name = "Index for keys", default = 0, update=update),
"keys_setter":keys_property_group_pointer,
"values_list": values_collection,
"values_list_index": IntProperty(name = "Index for values", default = 0, update=update),
"values_setter":values_property_group_pointer,
}
"""__annotations__["list"] = StringProperty(default="N/A")
__annotations__["values_list"] = StringProperty(default="N/A")
__annotations__["keys_setter"] = StringProperty(default="N/A")"""
"""registry.add_missing_typeInfo(key_ref_name)
registry.add_missing_typeInfo(value_ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
print("setting invalid flag for", nesting_long_names[0])"""
return __annotations__

View File

@ -0,0 +1,48 @@
from bpy.props import (StringProperty)
from . import process_component
def process_structs(registry, definition, properties, update, nesting, nesting_long_names):
value_types_defaults = registry.value_types_defaults
blender_property_mapping = registry.blender_property_mapping
type_infos = registry.type_infos
long_name = definition["long_name"]
short_name = definition["short_name"]
__annotations__ = {}
default_values = {}
nesting = nesting + [short_name]
nesting_long_names = nesting_long_names + [long_name]
for property_name in properties.keys():
ref_name = properties[property_name]["type"]["$ref"].replace("#/$defs/", "")
if ref_name in type_infos:
original = type_infos[ref_name]
original_long_name = original["long_name"]
is_value_type = original_long_name in value_types_defaults
value = value_types_defaults[original_long_name] if is_value_type else None
default_values[property_name] = value
if is_value_type:
if original_long_name in blender_property_mapping:
blender_property_def = blender_property_mapping[original_long_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
name = property_name,
default = value,
update = update
)
__annotations__[property_name] = blender_property
else:
original_long_name = original["long_name"]
(sub_component_group, _) = process_component.process_component(registry, original, update, {"nested": True, "long_name": original_long_name}, nesting, nesting_long_names)
__annotations__[property_name] = sub_component_group
# if there are sub fields, add an attribute "sub_fields" possibly a pointer property ? or add a standard field to the type , that is stored under "attributes" and not __annotations (better)
else:
# component not found in type_infos, generating placeholder
__annotations__[property_name] = StringProperty(default="N/A")
registry.add_missing_typeInfo(ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
return __annotations__

View File

@ -0,0 +1,55 @@
from bpy.props import (StringProperty)
from . import process_component
def process_tupples(registry, definition, prefixItems, update, nesting=[], nesting_long_names=[]):
value_types_defaults = registry.value_types_defaults
blender_property_mapping = registry.blender_property_mapping
type_infos = registry.type_infos
long_name = definition["long_name"]
short_name = definition["short_name"]
nesting = nesting + [short_name]
nesting_long_names = nesting_long_names + [long_name]
__annotations__ = {}
default_values = []
prefix_infos = []
for index, item in enumerate(prefixItems):
ref_name = item["type"]["$ref"].replace("#/$defs/", "")
property_name = str(index)# we cheat a bit, property names are numbers here, as we do not have a real property name
if ref_name in type_infos:
original = type_infos[ref_name]
original_long_name = original["long_name"]
is_value_type = original_long_name in value_types_defaults
value = value_types_defaults[original_long_name] if is_value_type else None
default_values.append(value)
prefix_infos.append(original)
if is_value_type:
if original_long_name in blender_property_mapping:
blender_property_def = blender_property_mapping[original_long_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
name = property_name,
default=value,
update= update
)
__annotations__[property_name] = blender_property
else:
original_long_name = original["long_name"]
(sub_component_group, _) = process_component.process_component(registry, original, update, {"nested": True, "long_name": original_long_name}, nesting)
__annotations__[property_name] = sub_component_group
else:
# component not found in type_infos, generating placeholder
__annotations__[property_name] = StringProperty(default="N/A")
registry.add_missing_typeInfo(ref_name)
# the root component also becomes invalid (in practice it is not always a component, but good enough)
registry.add_invalid_component(nesting_long_names[0])
return __annotations__

View File

@ -0,0 +1,44 @@
import bpy
from .conversions_from_prop_group import property_group_value_to_custom_property_value
from .process_component import process_component
from .utils import update_calback_helper
import json
## main callback function, fired whenever any property changes, no matter the nesting level
def update_component(self, context, definition, component_name):
registry = bpy.context.window_manager.components_registry
current_object = bpy.context.object
update_disabled = current_object["__disable__update"] if "__disable__update" in current_object else False
update_disabled = registry.disable_all_object_updates or update_disabled # global settings
if update_disabled:
return
print("")
print("update in component", component_name, self, "current_object", current_object.name)
components_in_object = current_object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == component_name, components_in_object), None)
if component_meta != None:
property_group_name = registry.get_propertyGroupName_from_longName(component_name)
property_group = getattr(component_meta, property_group_name)
# we use our helper to set the values
object = context.object
previous = json.loads(object['bevy_components'])
previous[component_name] = property_group_value_to_custom_property_value(property_group, definition, registry, None)
object['bevy_components'] = json.dumps(previous)
def generate_propertyGroups_for_components():
registry = bpy.context.window_manager.components_registry
if not registry.has_type_infos():
registry.load_type_infos()
type_infos = registry.type_infos
for component_name in type_infos:
definition = type_infos[component_name]
is_component = definition['isComponent'] if "isComponent" in definition else False
root_property_name = component_name if is_component else None
process_component(registry, definition, update_calback_helper(definition, update_component, root_property_name), None, [])
# if we had to add any wrapper types on the fly, process them now
registry.process_custom_types()

View File

@ -0,0 +1,63 @@
# helper function that returns a lambda, used for the PropertyGroups update function
def update_calback_helper(definition, update, component_name_override):
return lambda self, context: update(self, context, definition, component_name_override)
import bpy
from bpy.props import (StringProperty)
from bpy_types import PropertyGroup
# this helper creates a "fake"/wrapper property group that is NOT a real type in the registry
# usefull for things like value types in list items etc
def generate_wrapper_propertyGroup(wrapped_type_long_name_name, item_long_name, definition_link, registry, update):
value_types_defaults = registry.value_types_defaults
blender_property_mapping = registry.blender_property_mapping
is_item_value_type = item_long_name in value_types_defaults
wrapper_name = "wrapper_" + wrapped_type_long_name_name
wrapper_definition = {
"isComponent": False,
"isResource": False,
"items": False,
"prefixItems": [
{
"type": {
"$ref": definition_link
}
}
],
"short_name": wrapper_name, # FIXME !!!
"long_name": wrapper_name,
"type": "array",
"typeInfo": "TupleStruct"
}
# we generate a very small 'hash' for the component name
property_group_name = registry.generate_propGroup_name(nesting=[], longName=wrapper_name)
registry.add_custom_type(wrapper_name, wrapper_definition)
blender_property = StringProperty(default="", update=update)
if item_long_name in blender_property_mapping:
value = value_types_defaults[item_long_name] if is_item_value_type else None
blender_property_def = blender_property_mapping[item_long_name]
blender_property = blender_property_def["type"](
**blender_property_def["presets"],# we inject presets first
name = "property_name",
default = value,
update = update
)
wrapper_annotations = {
'0' : blender_property
}
property_group_params = {
'__annotations__': wrapper_annotations,
'tupple_or_struct': "tupple",
'field_names': ['0'],
**dict(with_properties = False, with_items= True, with_enum= False, with_list= False, with_map =False, short_name=wrapper_name, long_name=wrapper_name),
}
property_group_class = type(property_group_name, (PropertyGroup,), property_group_params)
bpy.utils.register_class(property_group_class)
return property_group_class

View File

@ -0,0 +1,238 @@
import os
import bpy
from bpy_types import (Operator)
from bpy.props import (StringProperty)
from bpy_extras.io_utils import ImportHelper
from ...settings import upsert_settings
from ..components.metadata import apply_customProperty_values_to_object_propertyGroups, apply_propertyGroup_values_to_object_customProperties, ensure_metadata_for_all_objects
from ..propGroups.prop_groups import generate_propertyGroups_for_components
class ReloadRegistryOperator(Operator):
"""Reloads registry (schema file) from disk, generates propertyGroups for components & ensures all objects have metadata """
bl_idname = "object.reload_registry"
bl_label = "Reload Registry"
bl_options = {"UNDO"}
component_type: StringProperty(
name="component_type",
description="component type to add",
) # type: ignore
def execute(self, context):
print("reload registry")
context.window_manager.components_registry.load_schema()
generate_propertyGroups_for_components()
print("")
print("")
print("")
ensure_metadata_for_all_objects()
# now force refresh the ui
for area in context.screen.areas:
for region in area.regions:
if region.type == "UI":
region.tag_redraw()
return {'FINISHED'}
class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL(Operator):
"""Apply registry to ALL objects: update the custom property values of all objects based on their definition, if any"""
bl_idname = "object.refresh_custom_properties_all"
bl_label = "Apply Registry to all objects"
bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.custom_properties_from_components_progress_all = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.custom_properties_from_components_progress_all
def execute(self, context):
print("apply registry to all")
#context.window_manager.components_registry.load_schema()
total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
apply_propertyGroup_values_to_object_customProperties(object)
progress = index / total
context.window_manager.custom_properties_from_components_progress_all = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
context.window_manager.custom_properties_from_components_progress_all = -1.0
return {'FINISHED'}
class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT(Operator):
"""Apply registry to CURRENT object: update the custom property values of current object based on their definition, if any"""
bl_idname = "object.refresh_custom_properties_current"
bl_label = "Apply Registry to current object"
bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.custom_properties_from_components_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.custom_properties_from_components_progress
def execute(self, context):
print("apply registry to current object")
object = context.object
context.window_manager.custom_properties_from_components_progress = 0.5
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
apply_propertyGroup_values_to_object_customProperties(object)
context.window_manager.custom_properties_from_components_progress = -1.0
return {'FINISHED'}
class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT(Operator):
"""Update UI values from custom properties to CURRENT object"""
bl_idname = "object.refresh_ui_from_custom_properties_current"
bl_label = "Apply custom_properties to current object"
bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.components_from_custom_properties_progress = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_from_custom_properties_progress
def execute(self, context):
print("apply custom properties to current object")
object = context.object
error = False
try:
apply_customProperty_values_to_object_propertyGroups(object)
progress = 0.5
context.window_manager.components_from_custom_properties_progress = progress
try:
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
except:pass # ony run in ui
except Exception as error_message:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Error:" + str(error_message))
if not error:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for selected object")
context.window_manager.components_from_custom_properties_progress = -1.0
return {'FINISHED'}
class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL(Operator):
"""Update UI values from custom properties to ALL object"""
bl_idname = "object.refresh_ui_from_custom_properties_all"
bl_label = "Apply custom_properties to all objects"
bl_options = {"UNDO"}
@classmethod
def register(cls):
bpy.types.WindowManager.components_from_custom_properties_progress_all = bpy.props.FloatProperty(default=-1.0) #bpy.props.PointerProperty(type=RenameHelper)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.components_from_custom_properties_progress_all
def execute(self, context):
print("apply custom properties to all object")
bpy.context.window_manager.components_registry.disable_all_object_updates = True
errors = []
total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
try:
apply_customProperty_values_to_object_propertyGroups(object)
except Exception as error:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
errors.append( "object: '" + object.name + "', error: " + str(error))
progress = index / total
context.window_manager.components_from_custom_properties_progress_all = progress
# now force refresh the ui
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
if len(errors) > 0:
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Errors:" + str(errors))
else:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for all objects")
bpy.context.window_manager.components_registry.disable_all_object_updates = False
context.window_manager.components_from_custom_properties_progress_all = -1.0
return {'FINISHED'}
class OT_OpenFilebrowser(Operator, ImportHelper):
"""Browse for registry json file"""
bl_idname = "generic.open_filebrowser"
bl_label = "Open the file browser"
filter_glob: StringProperty(
default='*.json',
options={'HIDDEN'}
) # type: ignore
def execute(self, context):
"""Do something with the selected file(s)."""
#filename, extension = os.path.splitext(self.filepath)
file_path = bpy.data.filepath
# Get the folder
folder_path = os.path.dirname(file_path)
relative_path = os.path.relpath(self.filepath, folder_path)
registry = context.window_manager.components_registry
registry.schemaPath = relative_path
upsert_settings(registry.settings_save_path, {"schemaPath": relative_path})
return {'FINISHED'}
class OT_select_object(Operator):
"""Select object by name"""
bl_idname = "object.select"
bl_label = "Select object"
bl_options = {"UNDO"}
object_name: StringProperty(
name="object_name",
description="object to select's name ",
) # type: ignore
def execute(self, context):
if self.object_name:
object = bpy.data.objects[self.object_name]
scenes_of_object = list(object.users_scene)
if len(scenes_of_object) > 0:
bpy.ops.object.select_all(action='DESELECT')
bpy.context.window.scene = scenes_of_object[0]
object.select_set(True)
bpy.context.view_layer.objects.active = object
return {'FINISHED'}
class OT_select_component_name_to_replace(Operator):
"""Select component name to replace"""
bl_idname = "object.select_component_name_to_replace"
bl_label = "Select component name for bulk replace"
bl_options = {"UNDO"}
component_name: StringProperty(
name="component_name",
description="component name to replace",
) # type: ignore
def execute(self, context):
context.window_manager.bevy_component_rename_helper.original_name = self.component_name
return {'FINISHED'}

View File

@ -0,0 +1,363 @@
import bpy
import json
import os
import uuid
from pathlib import Path
from bpy_types import (PropertyGroup)
from bpy.props import (StringProperty, BoolProperty, FloatProperty, FloatVectorProperty, IntProperty, IntVectorProperty, EnumProperty, PointerProperty, CollectionProperty)
from ...settings import load_settings
from ..propGroups.prop_groups import generate_propertyGroups_for_components
from ..components.metadata import ComponentMetadata, ensure_metadata_for_all_objects
# helper class to store missing bevy types information
class MissingBevyType(bpy.types.PropertyGroup):
long_name: bpy.props.StringProperty(
name="type",
) # type: ignore
# helper function to deal with timer
def toggle_watcher(self, context):
#print("toggling watcher", self.watcher_enabled, watch_schema, self, bpy.app.timers)
if not self.watcher_enabled:
try:
bpy.app.timers.unregister(watch_schema)
except Exception as error:
pass
else:
self.watcher_active = True
bpy.app.timers.register(watch_schema)
def watch_schema():
self = bpy.context.window_manager.components_registry
# print("watching schema file for changes")
try:
stamp = os.stat(self.schemaFullPath).st_mtime
stamp = str(stamp)
if stamp != self.schemaTimeStamp and self.schemaTimeStamp != "":
print("FILE CHANGED !!", stamp, self.schemaTimeStamp)
# see here for better ways : https://stackoverflow.com/questions/11114492/check-if-a-file-is-not-open-nor-being-used-by-another-process
"""try:
os.rename(path, path)
#return False
except OSError: # file is in use
print("in use")
#return True"""
#bpy.ops.object.reload_registry()
# we need to add an additional delay as the file might not have loaded yet
bpy.app.timers.register(lambda: bpy.ops.object.reload_registry(), first_interval=1)
self.schemaTimeStamp = stamp
except Exception as error:
pass
return self.watcher_poll_frequency if self.watcher_enabled else None
# this is where we store the information for all available components
class ComponentsRegistry(PropertyGroup):
settings_save_path = ".bevy_components_settings" # where to store data in bpy.texts
schemaPath: bpy.props.StringProperty(
name="schema path",
description="path to the registry schema file",
default="registry.json"
)# type: ignore
schemaFullPath : bpy.props.StringProperty(
name="schema full path",
description="path to the registry schema file",
)# type: ignore
registry: bpy.props. StringProperty(
name="registry",
description="component registry"
)# type: ignore
missing_type_infos: StringProperty(
name="missing type infos",
description="unregistered/missing type infos"
)# type: ignore
disable_all_object_updates: BoolProperty(name="disable_object_updates", default=False) # type: ignore
## file watcher
watcher_enabled: BoolProperty(name="Watcher_enabled", default=True, update=toggle_watcher)# type: ignore
watcher_active: BoolProperty(name = "Flag for watcher status", default = False)# type: ignore
watcher_poll_frequency: IntProperty(
name="watcher poll frequency",
description="frequency (s) at wich to poll for changes to the registry file",
min=1,
max=10,
default=1
)# type: ignore
schemaTimeStamp: StringProperty(
name="last timestamp of schema file",
description="",
default=""
)# type: ignore
missing_types_list: CollectionProperty(name="missing types list", type=MissingBevyType)# type: ignore
missing_types_list_index: IntProperty(name = "Index for missing types list", default = 0)# type: ignore
blender_property_mapping = {
"bool": dict(type=BoolProperty, presets=dict()),
"u8": dict(type=IntProperty, presets=dict(min=0, max=255)),
"u16": dict(type=IntProperty, presets=dict(min=0, max=65535)),
"u32": dict(type=IntProperty, presets=dict(min=0)),
"u64": dict(type=IntProperty, presets=dict(min=0)),
"u128": dict(type=IntProperty, presets=dict(min=0)),
"u64": dict(type=IntProperty, presets=dict(min=0)),
"usize": dict(type=IntProperty, presets=dict(min=0)),
"i8": dict(type=IntProperty, presets=dict()),
"i16":dict(type=IntProperty, presets=dict()),
"i32":dict(type=IntProperty, presets=dict()),
"i64":dict(type=IntProperty, presets=dict()),
"i128":dict(type=IntProperty, presets=dict()),
"isize": dict(type=IntProperty, presets=dict()),
"f32": dict(type=FloatProperty, presets=dict()),
"f64": dict(type=FloatProperty, presets=dict()),
"glam::Vec2": {"type": FloatVectorProperty, "presets": dict(size = 2) },
"glam::DVec2": {"type": FloatVectorProperty, "presets": dict(size = 2) },
"glam::UVec2": {"type": FloatVectorProperty, "presets": dict(size = 2) },
"glam::Vec3": {"type": FloatVectorProperty, "presets": {"size":3} },
"glam::Vec3A":{"type": FloatVectorProperty, "presets": {"size":3} },
"glam::DVec3":{"type": FloatVectorProperty, "presets": {"size":3} },
"glam::UVec3":{"type": FloatVectorProperty, "presets": {"size":3} },
"glam::Vec4": {"type": FloatVectorProperty, "presets": {"size":4} },
"glam::Vec4A": {"type": FloatVectorProperty, "presets": {"size":4} },
"glam::DVec4": {"type": FloatVectorProperty, "presets": {"size":4} },
"glam::UVec4":{"type": FloatVectorProperty, "presets": {"size":4, "min":0.0} },
"glam::Quat": {"type": FloatVectorProperty, "presets": {"size":4} },
"bevy_render::color::Color": dict(type = FloatVectorProperty, presets=dict(subtype='COLOR', size=4)),
"char": dict(type=StringProperty, presets=dict()),
"str": dict(type=StringProperty, presets=dict()),
"alloc::string::String": dict(type=StringProperty, presets=dict()),
"alloc::borrow::Cow<str>": dict(type=StringProperty, presets=dict()),
"enum": dict(type=EnumProperty, presets=dict()),
'bevy_ecs::entity::Entity': {"type": IntProperty, "presets": {"min":0} },
'bevy_utils::Uuid': dict(type=StringProperty, presets=dict()),
}
value_types_defaults = {
"string":" ",
"boolean": True,
"float": 0.0,
"uint": 0,
"int":0,
# todo : we are re-doing the work of the bevy /rust side here, but it seems more pratical to alway look for the same field name on the blender side for matches
"bool": True,
"u8": 0,
"u16":0,
"u32":0,
"u64":0,
"u128":0,
"usize":0,
"i8": 0,
"i16":0,
"i32":0,
"i64":0,
"i128":0,
"isize":0,
"f32": 0.0,
"f64":0.0,
"char": " ",
"str": " ",
"alloc::string::String": " ",
"alloc::borrow::Cow<str>": " ",
"glam::Vec2": [0.0, 0.0],
"glam::DVec2": [0.0, 0.0],
"glam::UVec2": [0, 0],
"glam::Vec3": [0.0, 0.0, 0.0],
"glam::Vec3A":[0.0, 0.0, 0.0],
"glam::UVec3": [0, 0, 0],
"glam::Vec4": [0.0, 0.0, 0.0, 0.0],
"glam::DVec4": [0.0, 0.0, 0.0, 0.0],
"glam::UVec4": [0, 0, 0, 0],
"glam::Quat": [0.0, 0.0, 0.0, 0.0],
"bevy_render::color::Color": [1.0, 1.0, 0.0, 1.0],
'bevy_ecs::entity::Entity': 0,#4294967295, # this is the same as Bevy's Entity::Placeholder, too big for Blender..sigh
'bevy_utils::Uuid': '"'+str(uuid.uuid4())+'"'
}
type_infos = {}
type_infos_missing = []
component_propertyGroups = {}
custom_types_to_add = {}
invalid_components = []
@classmethod
def register(cls):
bpy.types.WindowManager.components_registry = PointerProperty(type=ComponentsRegistry)
bpy.context.window_manager.components_registry.watcher_active = False
@classmethod
def unregister(cls):
bpy.context.window_manager.components_registry.watcher_active = False
for propgroup_name in cls.component_propertyGroups.keys():
try:
delattr(ComponentMetadata, propgroup_name)
#print("unregistered propertyGroup", propgroup_name)
except Exception as error:
pass
#print("failed to remove", error, "ComponentMetadata")
try:
bpy.app.timers.unregister(watch_schema)
except Exception as error:
pass
del bpy.types.WindowManager.components_registry
def load_schema(self):
print("load schema", self)
# cleanup previous data if any
self.propGroupIdCounter = 0
self.long_names_to_propgroup_names.clear()
self.missing_types_list.clear()
self.type_infos.clear()
self.type_infos_missing.clear()
self.component_propertyGroups.clear()
self.custom_types_to_add.clear()
self.invalid_components.clear()
# now prepare paths to load data
file_path = bpy.data.filepath
# Get the folder
folder_path = os.path.dirname(file_path)
path = os.path.join(folder_path, self.schemaPath)
self.schemaFullPath = path
f = Path(bpy.path.abspath(path)) # make a path object of abs path
with open(path) as f:
data = json.load(f)
defs = data["$defs"]
self.registry = json.dumps(defs) # FIXME:meh ?
# start timer
if not self.watcher_active and self.watcher_enabled:
self.watcher_active = True
print("registering function", watch_schema)
bpy.app.timers.register(watch_schema)
# we load the json once, so we do not need to do it over & over again
def load_type_infos(self):
print("load type infos")
ComponentsRegistry.type_infos = json.loads(self.registry)
def has_type_infos(self):
return len(self.type_infos.keys()) != 0
def load_settings(self):
print("loading settings")
settings = load_settings(self.settings_save_path)
if settings!= None:
print("settings", settings)
self.schemaPath = settings["schemaPath"]
self.load_schema()
generate_propertyGroups_for_components()
ensure_metadata_for_all_objects()
# we keep a list of component propertyGroup around
def register_component_propertyGroup(self, name, propertyGroup):
self.component_propertyGroups[name] = propertyGroup
# to be able to give the user more feedback on any missin/unregistered types in their schema file
def add_missing_typeInfo(self, long_name):
if not long_name in self.type_infos_missing:
self.type_infos_missing.append(long_name)
setattr(self, "missing_type_infos", str(self.type_infos_missing))
item = self.missing_types_list.add()
item.long_name = long_name
def add_custom_type(self, long_name, type_definition):
self.custom_types_to_add[long_name] = type_definition
def process_custom_types(self):
for long_name in self.custom_types_to_add:
self.type_infos[long_name] = self.custom_types_to_add[long_name]
self.custom_types_to_add.clear()
# add an invalid component to the list (long name)
def add_invalid_component(self, component_name):
self.invalid_components.append(component_name)
###########
propGroupIdCounter: IntProperty(
name="propGroupIdCounter",
description="",
min=0,
max=1000000000,
default=0
) # type: ignore
long_names_to_propgroup_names = {}
# generate propGroup name from nesting level & shortName: each shortName + nesting is unique
def generate_propGroup_name(self, nesting, longName):
#print("gen propGroup name for", shortName, nesting)
self.propGroupIdCounter += 1
propGroupIndex = str(self.propGroupIdCounter)
propGroupName = propGroupIndex + "_ui"
key = str(nesting) + longName if len(nesting) > 0 else longName
self.long_names_to_propgroup_names[key] = propGroupName
return propGroupName
def get_propertyGroupName_from_longName(self, longName):
return self.long_names_to_propgroup_names.get(longName, None)
def long_name_to_key():
pass
###########
"""
object[component_definition.name] = 0.5
property_manager = object.id_properties_ui(component_definition.name)
property_manager.update(min=-10, max=10, soft_min=-5, soft_max=5)
print("property_manager", property_manager)
object[component_definition.name] = [0.8,0.2,1.0]
property_manager = object.id_properties_ui(component_definition.name)
property_manager.update(subtype='COLOR')
#IDPropertyUIManager
#rna_ui = object[component_definition.name].get('_RNA_UI')
"""

View File

@ -0,0 +1,339 @@
import json
import bpy
from bpy_types import (UIList)
from bpy.props import (StringProperty)
from ..components.operators import OT_rename_component, RemoveComponentFromAllObjectsOperator, RemoveComponentOperator
from .operators import(
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT,
OT_OpenFilebrowser,
OT_select_component_name_to_replace,
OT_select_object, ReloadRegistryOperator,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT)
class BEVY_COMPONENTS_PT_Configuration(bpy.types.Panel):
bl_idname = "BEVY_COMPONENTS_PT_Configuration"
bl_label = "Components"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BLENVY_PT_SidePanel"
bl_options = {'DEFAULT_CLOSED'}
bl_description = "list of missing/unregistered type from the bevy side"
@classmethod
def poll(cls, context):
return context.window_manager.blenvy.mode == 'SETTINGS'
return context.object is not None
def draw(self, context):
layout = self.layout
registry = context.window_manager.components_registry
row = layout.row()
col = row.column()
col.enabled = False
col.prop(registry, "schemaPath", text="Registry Schema path")
col = row.column()
col.operator(OT_OpenFilebrowser.bl_idname, text="Browse for registry schema file (json)")
layout.separator()
layout.operator(ReloadRegistryOperator.bl_idname, text="reload registry" , icon="FILE_REFRESH")
layout.separator()
row = layout.row()
row.prop(registry, "watcher_enabled", text="enable registry file polling")
row.prop(registry, "watcher_poll_frequency", text="registry file poll frequency (s)")
layout.separator()
layout.separator()
class BEVY_COMPONENTS_PT_AdvancedToolsPanel(bpy.types.Panel):
"""panel listing all the missing bevy types in the schema"""
bl_idname = "BEVY_COMPONENTS_PT_AdvancedToolsPanel"
bl_label = "Advanced tools"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BLENVY_PT_SidePanel"
bl_options = {'DEFAULT_CLOSED'}
bl_description = "advanced tooling"
@classmethod
def poll(cls, context):
return context.window_manager.blenvy.mode == 'TOOLS'
def draw_invalid_or_unregistered_header(self, layout, items):
row = layout.row()
for item in items:
col = row.column()
col.label(text=item)
def draw_invalid_or_unregistered(self, layout, status, component_name, object):
available_components = bpy.context.window_manager.components_list
registry = bpy.context.window_manager.components_registry
registry_has_type_infos = registry.has_type_infos()
row = layout.row()
col = row.column()
col.label(text=component_name)
col = row.column()
operator = col.operator(OT_select_object.bl_idname, text=object.name)
operator.object_name = object.name
col = row.column()
col.label(text=status)
col = row.column()
col.prop(available_components, "list", text="")
col = row.column()
operator = col.operator(OT_rename_component.bl_idname, text="", icon="SHADERFX") #rename
new_name = registry.type_infos[available_components.list]['long_name'] if available_components.list in registry.type_infos else ""
operator.original_name = component_name
operator.target_objects = json.dumps([object.name])
operator.new_name = new_name
col.enabled = registry_has_type_infos and component_name != "" and component_name != new_name
col = row.column()
operator = col.operator(RemoveComponentOperator.bl_idname, text="", icon="X")
operator.object_name = object.name
operator.component_name = component_name
col = row.column()
col = row.column()
operator = col.operator(OT_select_component_name_to_replace.bl_idname, text="", icon="EYEDROPPER") #text="select for rename",
operator.component_name = component_name
def draw(self, context):
layout = self.layout
registry = bpy.context.window_manager.components_registry
registry_has_type_infos = registry.has_type_infos()
selected_object = context.selected_objects[0] if len(context.selected_objects) > 0 else None
available_components = bpy.context.window_manager.components_list
row = layout.row()
box= row.box()
box.label(text="Invalid/ unregistered components")
objects_with_invalid_components = []
invalid_component_names = []
self.draw_invalid_or_unregistered_header(layout, ["Component", "Object", "Status", "Target"])
for object in bpy.data.objects: # TODO: very inneficent
if len(object.keys()) > 0:
if "components_meta" in object:
components_metadata = object.components_meta.components
comp_names = []
for index, component_meta in enumerate(components_metadata):
long_name = component_meta.long_name
if component_meta.invalid:
self.draw_invalid_or_unregistered(layout, "Invalid", long_name, object)
if not object.name in objects_with_invalid_components:
objects_with_invalid_components.append(object.name)
if not long_name in invalid_component_names:
invalid_component_names.append(long_name)
comp_names.append(long_name)
for custom_property in object.keys():
if custom_property != 'components_meta' and custom_property != 'bevy_components' and custom_property not in comp_names:
self.draw_invalid_or_unregistered(layout, "Unregistered", custom_property, object)
if not object.name in objects_with_invalid_components:
objects_with_invalid_components.append(object.name)
"""if not long_name in invalid_component_names:
invalid_component_names.append(custom_property)""" # FIXME
layout.separator()
layout.separator()
original_name = bpy.context.window_manager.bevy_component_rename_helper.original_name
row = layout.row()
col = row.column()
col.label(text="Original")
col = row.column()
col.label(text="New")
col = row.column()
col.label(text="------")
row = layout.row()
col = row.column()
box = col.box()
box.label(text=original_name)
col = row.column()
col.prop(available_components, "list", text="")
#row.prop(available_components, "filter",text="Filter")
col = row.column()
components_rename_progress = context.window_manager.components_rename_progress
if components_rename_progress == -1.0:
operator = col.operator(OT_rename_component.bl_idname, text="apply", icon="SHADERFX")
operator.target_objects = json.dumps(objects_with_invalid_components)
new_name = registry.type_infos[available_components.list]['short_name'] if available_components.list in registry.type_infos else ""
operator.new_name = new_name
col.enabled = registry_has_type_infos and original_name != "" and original_name != new_name
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
col.progress(factor = components_rename_progress, text=f"updating {components_rename_progress * 100.0:.2f}%")
col = row.column()
remove_components_progress = context.window_manager.components_remove_progress
if remove_components_progress == -1.0:
operator = row.operator(RemoveComponentFromAllObjectsOperator.bl_idname, text="", icon="X")
operator.component_name = context.window_manager.bevy_component_rename_helper.original_name
col.enabled = registry_has_type_infos and original_name != ""
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
col.progress(factor = remove_components_progress, text=f"updating {remove_components_progress * 100.0:.2f}%")
layout.separator()
layout.separator()
row = layout.row()
box= row.box()
box.label(text="Conversions between custom properties and components & vice-versa")
row = layout.row()
row.label(text="WARNING ! The following operations will overwrite your existing custom properties if they have matching types on the bevy side !")
row.alert = True
##
row = layout.row()
custom_properties_from_components_progress_current = context.window_manager.custom_properties_from_components_progress
if custom_properties_from_components_progress_current == -1.0:
row.operator(COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT.bl_idname, text="update custom properties of current object" , icon="LOOP_FORWARDS")
row.enabled = registry_has_type_infos and selected_object is not None
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = custom_properties_from_components_progress_current, text=f"updating {custom_properties_from_components_progress_current * 100.0:.2f}%")
layout.separator()
row = layout.row()
custom_properties_from_components_progress_all = context.window_manager.custom_properties_from_components_progress_all
if custom_properties_from_components_progress_all == -1.0:
row.operator(COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL.bl_idname, text="update custom properties of ALL objects" , icon="LOOP_FORWARDS")
row.enabled = registry_has_type_infos
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = custom_properties_from_components_progress_all, text=f"updating {custom_properties_from_components_progress_all * 100.0:.2f}%")
########################
row = layout.row()
row.label(text="WARNING ! The following operations will try to overwrite your existing ui values if they have matching types on the bevy side !")
row.alert = True
components_from_custom_properties_progress_current = context.window_manager.components_from_custom_properties_progress
row = layout.row()
if components_from_custom_properties_progress_current == -1.0:
row.operator(COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT.bl_idname, text="update UI FROM custom properties of current object" , icon="LOOP_BACK")
row.enabled = registry_has_type_infos and selected_object is not None
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = components_from_custom_properties_progress_current, text=f"updating {components_from_custom_properties_progress_current * 100.0:.2f}%")
layout.separator()
row = layout.row()
components_from_custom_properties_progress_all = context.window_manager.components_from_custom_properties_progress_all
if components_from_custom_properties_progress_all == -1.0:
row.operator(COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL.bl_idname, text="update UI FROM custom properties of ALL objects" , icon="LOOP_BACK")
row.enabled = registry_has_type_infos
else:
if hasattr(layout,"progress") : # only for Blender > 4.0
layout.progress(factor = components_from_custom_properties_progress_all, text=f"updating {components_from_custom_properties_progress_all * 100.0:.2f}%")
class BEVY_COMPONENTS_PT_MissingTypesPanel(bpy.types.Panel):
"""panel listing all the missing bevy types in the schema"""
bl_idname = "BEVY_COMPONENTS_PT_MissingTypesPanel"
bl_label = "Missing/Unregistered Types"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy Components"
bl_context = "objectmode"
bl_parent_id = "BLENVY_PT_SidePanel"
bl_options = {'DEFAULT_CLOSED'}
bl_description = "list of missing/unregistered type from the bevy side"
@classmethod
def poll(cls, context):
return context.window_manager.blenvy.mode == 'TOOLS'
def draw(self, context):
layout = self.layout
registry = bpy.context.window_manager.components_registry
layout.label(text="Missing types ")
layout.template_list("MISSING_TYPES_UL_List", "Missing types list", registry, "missing_types_list", registry, "missing_types_list_index")
class MISSING_TYPES_UL_List(UIList):
"""Missing components UIList."""
use_filter_name_reverse: bpy.props.BoolProperty(
name="Reverse Name",
default=False,
options=set(),
description="Reverse name filtering",
) # type: ignore
use_order_name = bpy.props.BoolProperty(name="Name", default=False, options=set(),
description="Sort groups by their name (case-insensitive)")
def filter_items__(self, context, data, propname):
"""Filter and order items in the list."""
# We initialize filtered and ordered as empty lists. Notice that # if all sorting and filtering is disabled, we will return # these empty.
filtered = []
ordered = []
items = getattr(data, propname)
helper_funcs = bpy.types.UI_UL_list
print("filter, order", items, self, dict(self))
if self.filter_name:
print("ssdfs", self.filter_name)
filtered= helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, items, "long_name", reverse=self.use_filter_name_reverse)
if not filtered:
filtered = [self.bitflag_filter_item] * len(items)
if self.use_order_name:
ordered = helper_funcs.sort_items_by_name(items, "name")
return filtered, ordered
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
row = layout.row()
#row.enabled = False
#row.alert = True
row.prop(item, "long_name", text="")
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
row = layout.row()
row.prop(item, "long_name", text="")

View File

View File

@ -28,6 +28,10 @@ class OT_select_blueprint(Operator):
bpy.context.window.scene = scene bpy.context.window.scene = scene
bpy.context.view_layer.objects.active = None bpy.context.view_layer.objects.active = None
bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection.children[self.blueprint_collection_name] bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection.children[self.blueprint_collection_name]
#bpy.context.view_layer.collections.active = collection
# bpy.context.view_layer.active_layer_collection = collection # bpy.context.view_layer.active_layer_collection = collection
"""for o in collection.objects:
o.select_set(True)"""
return {'FINISHED'} return {'FINISHED'}

View File

@ -1,25 +1,39 @@
import bpy import bpy
import json
from ..assets.ui import draw_assets
class GLTF_PT_auto_export_blueprints_list(bpy.types.Panel): class GLTF_PT_auto_export_blueprints_list(bpy.types.Panel):
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type = 'UI'
bl_label = "Blueprints" bl_label = "Blueprints"
bl_parent_id = "GLTF_PT_auto_export_SidePanel" bl_parent_id = "BLENVY_PT_SidePanel"
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED','HIDE_HEADER'}
@classmethod
def poll(cls, context):
return context.window_manager.blenvy.mode == 'BLUEPRINTS'
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.use_property_split = True layout.use_property_split = True
layout.use_property_decorate = False # No animation. layout.use_property_decorate = False # No animation.
asset_registry = context.window_manager.assets_registry
for blueprint in context.window_manager.blueprints_registry.blueprints_list: for blueprint in context.window_manager.blueprints_registry.blueprints_list:
row = layout.row() row = layout.row()
row.label(icon="RIGHTARROW")
row.label(text=blueprint.name) row.label(text=blueprint.name)
if blueprint.local: if blueprint.local:
select_blueprint = row.operator(operator="blueprint.select", text="Select") select_blueprint = row.operator(operator="blueprint.select", text="", icon="RESTRICT_SELECT_OFF")
select_blueprint.blueprint_collection_name = blueprint.collection.name select_blueprint.blueprint_collection_name = blueprint.collection.name
select_blueprint.blueprint_scene_name = blueprint.scene.name select_blueprint.blueprint_scene_name = blueprint.scene.name
assets = json.loads(blueprint.collection["assets"]) if "assets" in blueprint.collection else []
draw_assets(layout=layout, name=blueprint.name, title="Assets", asset_registry=asset_registry, assets=assets, target_type="BLUEPRINT", target_name=blueprint.name)
else: else:
row.label(text="External") row.label(text="External")

View File

View File

@ -0,0 +1,29 @@
import bpy
from bpy_types import (PropertyGroup)
from bpy.props import (EnumProperty, PointerProperty)
class BlenvyManager(PropertyGroup):
mode: EnumProperty(
items=(
('COMPONENTS', "Components", ""),
('BLUEPRINTS', "Blueprints", ""),
('ASSETS', "Assets", ""),
('SETTINGS', "Settings", ""),
('TOOLS', "Tools", ""),
)
) # type: ignore
@classmethod
def register(cls):
bpy.types.WindowManager.blenvy = PointerProperty(type=BlenvyManager)
@classmethod
def unregister(cls):
del bpy.types.WindowManager.blenvy
def add_asset(self, name, type, path, internal): # internal means it cannot be edited by the user, aka auto generated
pass

View File

@ -0,0 +1,31 @@
from bpy_types import (Operator)
from bpy.props import (EnumProperty)
class OT_switch_bevy_tooling(Operator):
"""Switch bevy tooling"""
bl_idname = "bevy.tooling_switch"
bl_label = "Switch bevy tooling"
bl_options = {"UNDO"}
tool: EnumProperty(
items=(
('COMPONENTS', "Components", "Switch to components"),
('BLUEPRINTS', "Blueprints", ""),
('ASSETS', "Assets", ""),
('SETTINGS', "Settings", ""),
('TOOLS', "Tools", ""),
)
) # type: ignore
@classmethod
def description(cls, context, properties):
return properties.tool
def execute(self, context):
context.window_manager.blenvy.mode = self.tool
return {'FINISHED'}

95
tools/blenvy/core/ui.py Normal file
View File

@ -0,0 +1,95 @@
import bpy
from ..settings import load_settings
######################################################
## ui logic & co
# side panel
class BLENVY_PT_SidePanel(bpy.types.Panel):
bl_idname = "BLENVY_PT_SidePanel"
bl_label = ""
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Bevy"
bl_context = "objectmode"
def draw_header(self, context):
layout = self.layout
layout.label(text="Blenvy")
def draw(self, context):
layout = self.layout
row = layout.row()
blenvy = context.window_manager.blenvy
active_mode = blenvy.mode
world_scene_active = False
library_scene_active = False
active_collection = context.collection
current_auto_settings = load_settings(".gltf_auto_export_settings")
current_gltf_settings = load_settings(".gltf_auto_export_gltf_settings")
if current_auto_settings is not None:
#print("current_auto_settings", current_auto_settings)
main_scene_names = current_auto_settings["main_scene_names"]
library_scene_names = current_auto_settings["library_scene_names"]
world_scene_active = context.scene.name in main_scene_names
library_scene_active = context.scene.name in library_scene_names
# Now to actual drawing of the UI
target = row.box() if active_mode == 'COMPONENTS' else row
tool_switch_components = target.operator(operator="bevy.tooling_switch", text="", icon="PROPERTIES")
tool_switch_components.tool = "COMPONENTS"
target = row.box() if active_mode == 'BLUEPRINTS' else row
tool_switch_components = target.operator(operator="bevy.tooling_switch", text="", icon="PACKAGE")
tool_switch_components.tool = "BLUEPRINTS"
target = row.box() if active_mode == 'ASSETS' else row
tool_switch_components = target.operator(operator="bevy.tooling_switch", text="", icon="ASSET_MANAGER")
tool_switch_components.tool = "ASSETS"
target = row.box() if active_mode == 'SETTINGS' else row
tool_switch_components = target.operator(operator="bevy.tooling_switch", text="", icon="SETTINGS")
tool_switch_components.tool = "SETTINGS"
target = row.box() if active_mode == 'TOOLS' else row
tool_switch_components = target.operator(operator="bevy.tooling_switch", text="", icon="TOOL_SETTINGS")
tool_switch_components.tool = "TOOLS"
# Debug stuff
"""layout.label(text="Active Blueprint: "+ active_collection.name.upper())
layout.label(text="World scene active: "+ str(world_scene_active))
layout.label(text="Library scene active: "+ str(library_scene_active))
layout.label(text=blenvy.mode)"""
"""if blenvy.mode == "SETTINGS":
header, panel = layout.panel("auto_export", default_closed=False)
header.label(text="Auto Export")
if panel:
layout = panel
layout.label(text="MAKE SURE TO KEEP 'REMEMBER EXPORT SETTINGS' TOGGLED !!")
op = layout.operator("EXPORT_SCENE_OT_gltf", text='Gltf Settings')#'glTF 2.0 (.glb/.gltf)')
#op.export_format = 'GLTF_SEPARATE'
op.use_selection=True
op.will_save_settings=True
op.use_visible=True # Export visible and hidden objects. See Object/Batch Export to skip.
op.use_renderable=True
op.use_active_collection = True
op.use_active_collection_with_nested=True
op.use_active_scene = True
op.filepath="____dummy____"
op.gltf_export_id = "gltf_auto_export" # we specify that we are in a special case
op = layout.operator("EXPORT_SCENES_OT_auto_gltf", text="Auto Export Settings")
op.auto_export = True"""
"""header, panel = layout.panel("components", default_closed=False)
header.label(text="Components")
if panel:
panel.label(text="YOOO")"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,881 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="process.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.72426347"
inkscape:cx="-439.06674"
inkscape:cy="597.15838"
inkscape:window-width="2560"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<marker
style="overflow:visible"
id="RoundedArrow"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Rounded arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.7)"
d="m -0.21114562,-4.1055728 6.42229122,3.21114561 a 1,1 90 0 1 0,1.78885438 L -0.21114562,4.1055728 A 1.236068,1.236068 31.717474 0 1 -2,3 v -6 a 1.236068,1.236068 148.28253 0 1 1.78885438,-1.1055728 z"
style="fill:context-stroke;fill-rule:evenodd;stroke:none"
id="path8" />
</marker>
<marker
style="overflow:visible"
id="ArrowWide"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Wide arrow"
markerWidth="1"
markerHeight="1"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:butt"
d="M 3,-3 0,0 3,3"
transform="rotate(180,0.125,0)"
sodipodi:nodetypes="ccc"
id="path1" />
</marker>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect77"
width="220.61754"
height="312.25241"
x="-4.5802779"
y="-3.4230094"
ry="4.6700392" />
<rect
style="fill:none;stroke:#000000;stroke-width:0.309086;stroke-dasharray:none"
id="rect1"
width="201.30733"
height="72.947769"
x="4.9794211"
y="3.6587322"
ry="2.5855451" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583"
x="102.90446"
y="11.789397"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="102.90446"
y="11.789397">Original</tspan></text>
<rect
style="fill:none;stroke:#000000;stroke-width:0.294902;stroke-dasharray:none"
id="rect6"
width="82.234131"
height="56.321247"
x="115.08189"
y="16.034706"
ry="5.4638991" />
<g
id="g16"
transform="translate(-90.659631,6.5443841)">
<rect
style="fill:none;stroke:#000000;stroke-width:0.307482;stroke-dasharray:none"
id="rect5"
width="89.355553"
height="56.348793"
x="104.23762"
y="9.3243046"
ry="5.4665713" />
<g
id="g15"
transform="translate(-8.7898623,-68.268248)"
style="fill:#efff81;fill-opacity:1">
<rect
style="fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect7"
width="21.240173"
height="10.947481"
x="120.16953"
y="92.749634"
ry="1.0620492" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="124.65015"
y="97.640045"
id="text7"><tspan
sodipodi:role="line"
id="tspan7"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.65015"
y="97.640045">Object A</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.65015"
y="101.76033"
id="tspan72">(unique)</tspan></text>
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583"
x="126.33414"
y="14.70156"
id="text8"><tspan
sodipodi:role="line"
id="tspan8"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="126.33414"
y="14.70156">Main Scene (world/level)</tspan></text>
<g
id="g14"
transform="translate(-15.875,-65.181456)"
style="fill:#81ffc7;fill-opacity:1">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.355848;stroke-dasharray:none"
id="rect8"
width="38.74361"
height="10.856215"
x="157.82838"
y="84.181671"
ry="1.0531952" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="162.26337"
y="90.613953"
id="text9"><tspan
sodipodi:role="line"
id="tspan9"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="90.613953">Object B1 (instance)</tspan></text>
</g>
<g
id="g13"
transform="translate(-11.892453,-70.473123)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.358688;stroke-dasharray:none"
id="rect9"
width="39.374626"
height="10.853376"
x="153.73479"
y="107.09112"
ry="1.0529197" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text10"><tspan
sodipodi:role="line"
id="tspan10"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Object C0 (instance)</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,25.28543 H 183.09695"
id="path16" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,42.010871 H 183.09695"
id="path17" />
<g
id="g17"
transform="translate(-11.892453,-56.675475)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.358688;stroke-dasharray:none"
id="rect17"
width="39.374626"
height="10.853376"
x="153.73479"
y="107.09112"
ry="1.0529197" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583;stroke-dasharray:none;-inkscape-font-specification:Ubuntu;font-family:Ubuntu;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;fill-opacity:1"
x="158.16835"
y="113.52198"
id="text17"><tspan
sodipodi:role="line"
id="tspan17"
style="stroke-width:0.264583;-inkscape-font-specification:Ubuntu;font-family:Ubuntu;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;stroke:none;fill:#000000;fill-opacity:1"
x="158.16835"
y="113.52198">Object C1 (instance)</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="m 226.2693,42.010871 -42.64565,15.18405"
id="path18"
sodipodi:nodetypes="cc" />
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583"
x="145.92572"
y="22.124336"
id="text11"><tspan
sodipodi:role="line"
id="tspan11"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="145.92572"
y="22.124336">Library Scene</tspan></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect11"
width="56.246941"
height="10.784719"
x="138.19809"
y="26.06472"
ry="1.046259" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="32.46125"
id="text12"><tspan
sodipodi:role="line"
id="tspan12"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="32.46125">Object B: collection/ blueprint</tspan></text>
<g
id="g69"
transform="translate(0,-5.0567386)">
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect12"
width="56.246941"
height="10.784719"
x="138.19809"
y="46.759216"
ry="1.046259" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="53.155746"
id="text13"><tspan
sodipodi:role="line"
id="tspan13"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="53.155746">Object C: collection/ blueprint</tspan></text>
</g>
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="m 108.69996,76.300286 v 9.304509"
id="path28"
sodipodi:nodetypes="cc" />
<rect
style="fill:none;stroke:#000000;stroke-width:0.336158;stroke-dasharray:none"
id="rect28"
width="201.28026"
height="86.298004"
x="4.9929576"
y="91.43959"
ry="3.0587275" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583;fill-opacity:1"
x="97.815292"
y="99.943169"
id="text28"><tspan
sodipodi:role="line"
id="tspan28"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="97.815292"
y="99.943169">Transform Step</tspan></text>
<rect
style="fill:none;stroke:#000000;stroke-width:0.320846;stroke-dasharray:none"
id="rect29"
width="82.208183"
height="66.687843"
x="115.09486"
y="106.46085"
ry="6.4695954" />
<g
id="g36"
transform="translate(-90.659631,96.957538)">
<rect
style="fill:none;stroke:#000000;stroke-width:0.336001;stroke-dasharray:none"
id="rect30"
width="89.327034"
height="67.307678"
x="104.25188"
y="9.3385649"
ry="6.5297275" />
<g
id="g31"
transform="translate(-8.7898623,-68.268248)"
style="fill:#efff81;fill-opacity:1">
<rect
style="fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect31"
width="21.240173"
height="10.947481"
x="120.16953"
y="92.749634"
ry="1.0620492" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="124.65015"
y="97.640045"
id="text31"><tspan
sodipodi:role="line"
id="tspan31"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
x="124.65015"
y="97.640045">Object A</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
x="124.65015"
y="101.76033"
id="tspan73">(unique)</tspan></text>
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583"
x="126.33414"
y="14.70156"
id="text32"><tspan
sodipodi:role="line"
id="tspan32"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="126.33414"
y="14.70156">Temporary Scene (world/level)</tspan></text>
<g
id="g33"
transform="translate(-19.939658,-65.832448)"
style="fill:#81ffc7;fill-opacity:1">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.505434;stroke-dasharray:none"
id="rect32"
width="52.860485"
height="16.052649"
x="157.90318"
y="84.256462"
ry="1.5573175" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="162.26337"
y="90.613953"
id="text33"><tspan
sodipodi:role="line"
id="tspan33"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="90.613953">Object B1 (Empty)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="94.734238"
id="tspan44">+ blueprintName(&quot;ObjectB&quot;)</tspan><tspan
sodipodi:role="line"
style="fill:#81ffc7;fill-opacity:1;stroke-width:0.264583"
x="162.26337"
y="98.854515"
id="tspan47"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none"
id="tspan78">+ spawnHere</tspan> </tspan></text>
</g>
<g
id="g34"
transform="translate(-15.528392,-70.796885)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.528181;stroke-dasharray:none"
id="rect33"
width="51.925056"
height="17.845793"
x="153.81953"
y="107.17587"
ry="1.7312757" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text34"><tspan
sodipodi:role="line"
id="tspan34"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Object C0 (Empty)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="117.64227"
id="tspan45">+ blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="158.16835"
y="121.76254"
id="tspan48"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none"
id="tspan79">+ spawnHere</tspan> </tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,25.28543 H 183.09695"
id="path34" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,42.010871 H 183.09695"
id="path35" />
<g
id="g35"
transform="translate(-15.952034,-50.670754)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.526025;stroke-dasharray:none"
id="rect35"
width="52.478481"
height="17.513748"
x="153.81845"
y="107.17479"
ry="1.6990631" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text35"><tspan
sodipodi:role="line"
id="tspan35"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Object C1 (Empty)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="117.64227"
id="tspan46">+ blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="158.16835"
y="121.76254"
id="tspan49"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none"
id="tspan80">+ spawnHere</tspan> </tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="m 226.2693,42.010871 -42.64565,15.18405"
id="path36"
sodipodi:nodetypes="cc" />
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.89855"
y="112.52866"
id="text36"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.89855"
y="112.52866"
id="tspan39">Determine used Collections to export</tspan></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.32101;stroke-dasharray:none"
id="rect36"
width="56.190514"
height="15.891233"
x="138.2263"
y="116.5061"
ry="1.5416578" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="122.87442"
id="text37"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="122.87442"
id="tspan40">Object B</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="142.59734"
y="126.84317"
id="tspan41" /></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.321404;stroke-dasharray:none"
id="rect37"
width="56.190121"
height="15.930397"
x="138.2265"
y="134.55496"
ry="1.5454572" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="140.92308"
id="text38"><tspan
sodipodi:role="line"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="140.92308"
id="tspan42">Object C</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="142.59734"
y="144.89183"
id="tspan43" /></text>
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="m 108.69996,177.9456 v 9.30451"
id="path49"
sodipodi:nodetypes="cc" />
<rect
style="fill:none;stroke:#000000;stroke-width:0.336158;stroke-dasharray:none"
id="rect49"
width="201.28026"
height="86.298004"
x="4.9929576"
y="193.0849"
ry="3.0587275" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583;fill-opacity:1"
x="106.60861"
y="201.20203"
id="text50"><tspan
sodipodi:role="line"
id="tspan50"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="106.60861"
y="201.20203">Result</tspan></text>
<rect
style="fill:none;stroke:#000000;stroke-width:0.272675;stroke-dasharray:none"
id="rect50"
width="82.256355"
height="48.138226"
x="115.07077"
y="207.5529"
ry="4.6700392" />
<g
id="g63"
transform="translate(-90.659631,198.60285)">
<g
id="g52"
transform="translate(-8.7898623,-68.268248)"
style="fill:#efff81;fill-opacity:1">
<rect
style="fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
id="rect52"
width="21.240173"
height="10.947481"
x="120.16953"
y="92.749634"
ry="1.0620492" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#efff81;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="124.65015"
y="97.110878"
id="text52"><tspan
sodipodi:role="line"
id="tspan52"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.65015"
y="97.110878">Entity A</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="124.65015"
y="101.23116"
id="tspan74">(unique)</tspan></text>
</g>
<rect
style="fill:none;stroke:#000000;stroke-width:0.336001;stroke-dasharray:none"
id="rect51"
width="89.327034"
height="67.307678"
x="104.25188"
y="9.3385649"
ry="6.5297275" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#0090ff;fill-opacity:1;stroke:#0090ff;stroke-width:0.264583;stroke-opacity:1"
x="135.33"
y="14.70156"
id="text53"><tspan
sodipodi:role="line"
id="tspan53"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#0090ff;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
x="135.33"
y="14.70156">world/level.gltf</tspan></text>
<g
id="g56"
transform="translate(-19.939658,-65.832448)"
style="fill:#81ffc7;fill-opacity:1">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.505434;stroke-dasharray:none"
id="rect53"
width="52.860485"
height="16.052649"
x="157.90318"
y="84.256462"
ry="1.5573175" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="162.26337"
y="90.613953"
id="text56"><tspan
sodipodi:role="line"
id="tspan54"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="90.613953">Entity B1</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="94.734238"
id="tspan55">+ blueprintName(&quot;ObjectB&quot;)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="162.26337"
y="98.854515"
id="tspan56">+ spawnHere </tspan></text>
</g>
<g
id="g59"
transform="translate(-15.528392,-70.796885)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.528181;stroke-dasharray:none"
id="rect56"
width="51.925056"
height="17.845793"
x="153.81953"
y="107.17587"
ry="1.7312757" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text59"><tspan
sodipodi:role="line"
id="tspan57"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Entity C0</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="117.64227"
id="tspan58">+ blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="121.76254"
id="tspan59">+ spawnHere </tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,25.28543 H 183.09695"
id="path59" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="M 226.2693,42.010871 H 183.09695"
id="path60" />
<g
id="g62"
transform="translate(-15.952034,-50.670754)">
<rect
style="fill:#81ffc7;fill-opacity:1;stroke:#000000;stroke-width:0.526025;stroke-dasharray:none"
id="rect60"
width="52.478481"
height="17.513748"
x="153.81845"
y="107.17479"
ry="1.6990631" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="158.16835"
y="113.52198"
id="text62"><tspan
sodipodi:role="line"
id="tspan60"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="113.52198">Entity C1</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="117.64227"
id="tspan61">+ blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="158.16835"
y="121.76254"
id="tspan62">+ spawnHere </tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:2.11666, 0.264583;stroke-dashoffset:0;marker-end:url(#ArrowWide)"
d="m 226.2693,42.010871 -42.64565,15.18405"
id="path62"
sodipodi:nodetypes="cc" />
</g>
<text
xml:space="preserve"
style="font-size:3.175px;fill:#0090ff;fill-opacity:1;stroke:#0090ff;stroke-width:0.264583;stroke-opacity:1"
x="117.87988"
y="214.18282"
id="text63"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#0090ff;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
x="117.87988"
y="214.18282"
id="tspan63">Library of gltf files (one per Collection/Blueprint)</tspan></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.32101;stroke-dasharray:none"
id="rect63"
width="56.190514"
height="15.891233"
x="138.2263"
y="218.15141"
ry="1.5416578" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="224.51973"
id="text66"><tspan
sodipodi:role="line"
id="tspan64"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="224.51973">Object B.gltf </tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="228.64001"
id="tspan65"> + blueprintName(&quot;ObjectB&quot;)</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="142.59734"
y="232.60876"
id="tspan66" /></text>
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.321404;stroke-dasharray:none"
id="rect66"
width="56.190121"
height="15.930397"
x="138.2265"
y="236.20027"
ry="1.5454572" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="242.56839"
id="text69"><tspan
sodipodi:role="line"
id="tspan67"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="242.56839">Object C:.gltf</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="246.68867"
id="tspan68"> + blueprintName(&quot;ObjectC&quot;)</tspan><tspan
sodipodi:role="line"
style="stroke-width:0.264583"
x="142.59734"
y="250.65742"
id="tspan69" /></text>
<g
id="g70"
transform="translate(0,11.775225)">
<rect
style="fill:#ff9342;fill-opacity:1;stroke:#000000;stroke-width:0.27923;stroke-dasharray:none"
id="rect69"
width="56.232292"
height="12.014979"
x="138.20541"
y="46.766541"
ry="1.1656103" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:none;stroke:#000000;stroke-width:0.264583;stroke-dasharray:none"
x="142.59734"
y="52.097412"
id="text70"><tspan
sodipodi:role="line"
id="tspan70"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="52.097412">Object D: unused collection/ </tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="142.59734"
y="56.217693"
id="tspan71">blueprint</tspan></text>
</g>
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="M 56.761407,72.285855 V 101.91653"
id="path74"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="m 56.761407,173.65448 v 29.63068"
id="path75"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="M 157.79469,72.285855 V 101.91653"
id="path76"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-dasharray:none;stroke-dashoffset:0;marker-end:url(#RoundedArrow)"
d="m 157.79469,173.40943 v 29.63068"
id="path77"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,183 @@
import copy
import json
import os
from types import SimpleNamespace
import bpy
import traceback
from .preferences import AutoExportGltfAddonPreferences
from .get_blueprints_to_export import get_blueprints_to_export
from .get_levels_to_export import get_levels_to_export
from .get_standard_exporter_settings import get_standard_exporter_settings
from .export_main_scenes import export_main_scene
from .export_blueprints import export_blueprints
from ..helpers.helpers_scenes import (get_scenes, )
from ..helpers.helpers_blueprints import blueprints_scan
from ..modules.export_materials import cleanup_materials, export_materials
from ..modules.bevy_scene_components import remove_scene_components, upsert_scene_components
"""this is the main 'central' function for all auto export """
def auto_export(changes_per_scene, changed_export_parameters, addon_prefs):
# 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:
# path to the current blend file
file_path = bpy.data.filepath
# Get the folder
blend_file_path = os.path.dirname(file_path)
# get the preferences for our addon
export_root_folder = getattr(addon_prefs, "export_root_folder")
export_output_folder = getattr(addon_prefs,"export_output_folder")
export_models_path = os.path.join(blend_file_path, export_output_folder)
#should we use change detection or not
export_change_detection = getattr(addon_prefs, "export_change_detection")
export_scene_settings = getattr(addon_prefs,"export_scene_settings")
do_export_blueprints = getattr(addon_prefs,"export_blueprints")
export_materials_library = getattr(addon_prefs,"export_materials_library")
print("export_materials_library", export_materials_library)
# standard gltf export settings are stored differently
standard_gltf_exporter_settings = get_standard_exporter_settings()
gltf_extension = standard_gltf_exporter_settings.get("export_format", 'GLB')
gltf_extension = '.glb' if gltf_extension == 'GLB' else '.gltf'
# generate the actual complete output path
export_blueprints_path = os.path.join(blend_file_path, export_root_folder, getattr(addon_prefs,"export_blueprints_path"))
export_levels_path = os.path.join(blend_file_path, export_root_folder, getattr(addon_prefs, "export_levels_path"))
print("export_blueprints_path", export_blueprints_path)
# here we do a bit of workaround by creating an override # TODO: do this at the "UI" level
print("collection_instances_combine_mode", addon_prefs.collection_instances_combine_mode)
"""if hasattr(addon_prefs, "__annotations__") :
tmp = {}
for k in AutoExportGltfAddonPreferences.__annotations__:
item = AutoExportGltfAddonPreferences.__annotations__[k]
#print("tutu",k, item.keywords.get('default', None) )
default = item.keywords.get('default', None)
tmp[k] = default
for (k, v) in addon_prefs.properties.items():
tmp[k] = v
addon_prefs = SimpleNamespace(**tmp) #copy.deepcopy(addon_prefs)
addon_prefs.__annotations__ = tmp"""
addon_prefs.export_blueprints_path = export_blueprints_path
addon_prefs.export_levels_path = export_levels_path
addon_prefs.export_gltf_extension = gltf_extension
addon_prefs.export_models_path = export_models_path
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
print("main scenes", main_scene_names, "library_scenes", library_scene_names)
print("export_output_folder", export_output_folder)
blueprints_data = blueprints_scan(level_scenes, library_scenes, addon_prefs)
blueprints_per_scene = blueprints_data.blueprints_per_scenes
internal_blueprints = [blueprint.name for blueprint in blueprints_data.internal_blueprints]
external_blueprints = [blueprint.name for blueprint in blueprints_data.external_blueprints]
if export_scene_settings:
# inject/ update scene components
upsert_scene_components(level_scenes)
#inject/ update light shadow information
for light in bpy.data.lights:
enabled = 'true' if light.use_shadow else 'false'
light['BlenderLightShadows'] = f"(enabled: {enabled}, buffer_bias: {light.shadow_buffer_bias})"
# export
if do_export_blueprints:
print("EXPORTING")
# get blueprints/collections infos
(blueprints_to_export) = get_blueprints_to_export(changes_per_scene, changed_export_parameters, blueprints_data, addon_prefs)
# get level/main scenes infos
(main_scenes_to_export) = get_levels_to_export(changes_per_scene, changed_export_parameters, blueprints_data, 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(blueprints_data.blueprint_names, library_scenes, blend_file_path, addon_prefs)
# update the list of tracked exports
exports_total = len(blueprints_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
bpy.context.window_manager.exportedCollections.clear()
for blueprint in blueprints_to_export:
bla = bpy.context.window_manager.exportedCollections.add()
bla.name = blueprint.name
print("-------------------------------")
#print("collections: all:", collections)
#print("collections: not found on disk:", collections_not_on_disk)
print("BLUEPRINTS: local/internal:", internal_blueprints)
print("BLUEPRINTS: external:", external_blueprints)
print("BLUEPRINTS: per_scene:", blueprints_per_scene)
print("-------------------------------")
print("BLUEPRINTS: to export:", [blueprint.name for blueprint in blueprints_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
old_selections = bpy.context.selected_objects
# first export any main/level/world scenes
if len(main_scenes_to_export) > 0:
print("export MAIN scenes")
for scene_name in main_scenes_to_export:
print(" exporting scene:", scene_name)
export_main_scene(bpy.data.scenes[scene_name], blend_file_path, addon_prefs, blueprints_data)
# now deal with blueprints/collections
do_export_library_scene = not export_change_detection or changed_export_parameters or len(blueprints_to_export) > 0
if do_export_library_scene:
print("export LIBRARY")
# we only want to go through the library scenes where our blueprints to export are present
"""for (scene_name, blueprints_to_export) in blueprints_per_scene.items():
print(" exporting blueprints from scene:", scene_name)
print(" blueprints to export", blueprints_to_export)"""
export_blueprints(blueprints_to_export, blend_file_path, addon_prefs, blueprints_data)
# reset current scene from backup
bpy.context.window.scene = old_current_scene
# reset selections
for obj in old_selections:
obj.select_set(True)
if export_materials_library:
cleanup_materials(blueprints_data.blueprint_names, library_scenes)
else:
for scene_name in main_scene_names:
export_main_scene(bpy.data.scenes[scene_name], blend_file_path, addon_prefs, [])
except Exception as error:
print(traceback.format_exc())
def error_message(self, context):
self.layout.label(text="Failure during auto_export: Error: "+ str(error))
bpy.context.window_manager.popup_menu(error_message, title="Error", icon='ERROR')
finally:
# FIXME: error handling ? also redundant
[main_scene_names, main_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
if export_scene_settings:
# inject/ update scene components
remove_scene_components(main_scenes)

View File

@ -0,0 +1,39 @@
import json
import bpy
"""
This should ONLY be run when actually doing exports/aka calling auto_export function, because we only care about the difference in settings between EXPORTS
"""
def did_export_settings_change():
# compare both the auto export settings & the gltf settings
previous_auto_settings = bpy.data.texts[".gltf_auto_export_settings_previous"] if ".gltf_auto_export_settings_previous" in bpy.data.texts else None
previous_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings_previous"] if ".gltf_auto_export_gltf_settings_previous" in bpy.data.texts else None
current_auto_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else None
current_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] if ".gltf_auto_export_gltf_settings" in bpy.data.texts else None
#check if params have changed
# if there were no setting before, it is new, we need export
changed = False
if previous_auto_settings == None:
print("previous settings missing, exporting")
changed = True
elif previous_gltf_settings == None:
print("previous gltf settings missing, exporting")
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
"""print("auto settings previous", sorted(json.loads(previous_auto_settings.as_string()).items()))
print("auto settings current", sorted(json.loads(current_auto_settings.as_string()).items()))
print("auto_settings_changed", auto_settings_changed)"""
"""print("gltf settings previous", sorted(json.loads(previous_gltf_settings.as_string()).items()))
print("gltf settings current", sorted(json.loads(current_gltf_settings.as_string()).items()))
print("gltf_settings_changed", gltf_settings_changed)"""
changed = auto_settings_changed or gltf_settings_changed
return changed

View File

@ -0,0 +1,44 @@
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
def export_blueprints(blueprints, blend_file_path, addon_prefs, blueprints_data):
export_blueprints_path = getattr(addon_prefs,"export_blueprints_path")
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
try:
# save current active collection
active_collection = bpy.context.view_layer.active_layer_collection
export_materials_library = getattr(addon_prefs,"export_materials_library")
for blueprint in blueprints:
print("exporting collection", blueprint.name)
gltf_output_path = os.path.join(export_blueprints_path, blueprint.name)
export_settings = { **gltf_export_preferences, 'use_active_scene': True, 'use_active_collection': True, 'use_active_collection_with_nested':True}
# if we are using the material library option, do not export materials, use placeholder instead
if export_materials_library:
export_settings['export_materials'] = 'PLACEHOLDER'
collection = bpy.data.collections[blueprint.name]
generate_and_export(
addon_prefs,
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, blueprints_data=blueprints_data, addon_prefs=addon_prefs),
tempScene_cleaner= lambda temp_scene, params: clear_hollow_scene(original_root_collection=collection, temp_scene=temp_scene, **params)
)
# reset active collection to the one we save before
bpy.context.view_layer.active_layer_collection = active_collection
except Exception as error:
print("failed to export collections to gltf: ", error)
raise error

View File

@ -0,0 +1,77 @@
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):
# default values
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_cameras=True,
export_extras=True, # For custom exported properties.
export_lights=True,
#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_yup=True,
#export_skins=True,
#export_morph=False,
#export_apply=False,
#export_animations=False,
#export_optimize_animation_size=False
)
for key in addon_prefs.__annotations__.keys():
if str(key) not in AutoExportGltfPreferenceNames:
#print("overriding setting", key, "value", getattr(addon_prefs,key))
gltf_export_preferences[key] = getattr(addon_prefs, key)
standard_gltf_exporter_settings = get_standard_exporter_settings()
constant_keys = [
'use_selection',
'use_visible',
'use_active_collection',
'use_active_collection_with_nested',
'use_active_scene',
'export_cameras',
'export_extras', # For custom exported properties.
'export_lights',
]
# a certain number of essential params should NEVER be overwritten , no matter the settings of the standard exporter
for key in standard_gltf_exporter_settings.keys():
if str(key) not in constant_keys:
gltf_export_preferences[key] = standard_gltf_exporter_settings.get(key)
return gltf_export_preferences
#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}
# print("export settings",settings)
os.makedirs(os.path.dirname(path), exist_ok=True)
#bpy.ops.export_scene.gltf(**settings)

View File

@ -0,0 +1,77 @@
import os
import bpy
from pathlib import Path
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
from ..helpers.helpers_scenes import clear_hollow_scene, copy_hollowed_collection_into
from ..helpers.helpers_blueprints import inject_blueprints_list_into_main_scene, remove_blueprints_list_from_main_scene
def export_main_scene(scene, blend_file_path, addon_prefs, blueprints_data):
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
export_root_folder = getattr(addon_prefs, "export_root_folder")
export_output_folder = getattr(addon_prefs,"export_output_folder")
export_levels_path = getattr(addon_prefs,"export_levels_path")
export_blueprints = getattr(addon_prefs,"export_blueprints")
export_separate_dynamic_and_static_objects = getattr(addon_prefs, "export_separate_dynamic_and_static_objects")
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
}
if export_blueprints :
gltf_output_path = os.path.join(export_levels_path, scene.name)
inject_blueprints_list_into_main_scene(scene, blueprints_data, addon_prefs)
return
if export_separate_dynamic_and_static_objects:
#print("SPLIT STATIC AND DYNAMIC")
# first export static objects
generate_and_export(
addon_prefs,
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, blueprints_data=blueprints_data, filter=is_object_static, addon_prefs=addon_prefs),
tempScene_cleaner= lambda temp_scene, params: clear_hollow_scene(original_root_collection=scene.collection, temp_scene=temp_scene, **params)
)
# then export all dynamic objects
gltf_output_path = os.path.join(export_levels_path, scene.name+ "_dynamic")
generate_and_export(
addon_prefs,
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, blueprints_data=blueprints_data, filter=is_object_dynamic, addon_prefs=addon_prefs),
tempScene_cleaner= lambda temp_scene, params: clear_hollow_scene(original_root_collection=scene.collection, temp_scene=temp_scene, **params)
)
else:
#print("NO SPLIT")
generate_and_export(
addon_prefs,
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, blueprints_data=blueprints_data, addon_prefs=addon_prefs),
tempScene_cleaner= lambda temp_scene, params: clear_hollow_scene(original_root_collection=scene.collection, temp_scene=temp_scene, **params)
)
else:
gltf_output_path = os.path.join(export_root_folder, export_output_folder, scene.name)
print(" exporting gltf to", gltf_output_path, ".gltf/glb")
export_gltf(gltf_output_path, export_settings)
remove_blueprints_list_from_main_scene(scene)

View File

@ -0,0 +1,60 @@
import bpy
import os
from ..helpers.helpers_scenes import (get_scenes, )
from ..helpers.helpers_blueprints import find_blueprints_not_on_disk
# 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_blueprints_to_export(changes_per_scene, changed_export_parameters, blueprints_data, addon_prefs):
export_change_detection = getattr(addon_prefs, "export_change_detection")
export_gltf_extension = getattr(addon_prefs, "export_gltf_extension", ".glb")
export_blueprints_path = getattr(addon_prefs,"export_blueprints_path", "")
collection_instances_combine_mode = getattr(addon_prefs, "collection_instances_combine_mode")
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
internal_blueprints = blueprints_data.internal_blueprints
blueprints_to_export = internal_blueprints # just for clarity
# 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
if export_change_detection and not changed_export_parameters:
changed_blueprints = []
# 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)
blueprints_not_on_disk = find_blueprints_not_on_disk(internal_blueprints, export_blueprints_path, export_gltf_extension)
for scene in library_scenes:
if scene.name in changes_per_scene:
changed_objects = list(changes_per_scene[scene.name].keys())
changed_blueprints = [blueprints_data.blueprints_from_objects[changed] for changed in changed_objects if changed in blueprints_data.blueprints_from_objects]
# we only care about local blueprints/collections
changed_local_blueprints = [blueprint for blueprint in changed_blueprints if blueprint.name in blueprints_data.blueprints_per_name.keys() and blueprint.local]
# FIXME: double check this: why are we combining these two ?
changed_blueprints += changed_local_blueprints
blueprints_to_export = list(set(changed_blueprints + blueprints_not_on_disk))
# filter out blueprints that are not marked & deal with the different combine modes
# we check for blueprint & object specific overrides ...
filtered_blueprints = []
for blueprint in blueprints_to_export:
if blueprint.marked:
filtered_blueprints.append(blueprint)
else:
blueprint_instances = blueprints_data.internal_collection_instances.get(blueprint.name, [])
# print("INSTANCES", blueprint_instances, blueprints_data.internal_collection_instances)
# marked blueprints that have changed are always exported, regardless of whether they are in use (have instances) or not
for blueprint_instance in blueprint_instances:
combine_mode = blueprint_instance['_combine'] if '_combine' in blueprint_instance else collection_instances_combine_mode
if combine_mode == "Split": # we only keep changed blueprints if mode is set to split for at least one instance (aka if ALL instances of a blueprint are merged, do not export ? )
filtered_blueprints.append(blueprint)
blueprints_to_export = list(set(filtered_blueprints))
# changed/all blueprints to export
return (blueprints_to_export)

View File

@ -0,0 +1,51 @@
import bpy
from ..helpers.helpers_blueprints import check_if_blueprint_on_disk
from ..helpers.helpers_scenes import (get_scenes, )
# IF collection_instances_combine_mode is not 'split' check for each scene if any object in changes_per_scene has an instance in the scene
def changed_object_in_scene(scene_name, changes_per_scene, blueprints_data, collection_instances_combine_mode):
# Embed / EmbedExternal
blueprints_from_objects = blueprints_data.blueprints_from_objects
blueprint_instances_in_scene = blueprints_data.blueprint_instances_per_main_scene.get(scene_name, None)
if blueprint_instances_in_scene is not None:
changed_objects = [object_name for change in changes_per_scene.values() for object_name in change.keys()]
changed_blueprints = [blueprints_from_objects[changed] for changed in changed_objects if changed in blueprints_from_objects]
changed_blueprints_with_instances_in_scene = [blueprint for blueprint in changed_blueprints if blueprint.name in blueprint_instances_in_scene.keys()]
changed_blueprint_instances= [object for blueprint in changed_blueprints_with_instances_in_scene for object in blueprint_instances_in_scene[blueprint.name]]
# print("changed_blueprint_instances", changed_blueprint_instances,)
level_needs_export = False
for blueprint_instance in changed_blueprint_instances:
blueprint = blueprints_data.blueprint_name_from_instances[blueprint_instance]
combine_mode = blueprint_instance['_combine'] if '_combine' in blueprint_instance else collection_instances_combine_mode
#print("COMBINE MODE FOR OBJECT", combine_mode)
if combine_mode == 'Embed':
level_needs_export = True
break
elif combine_mode == 'EmbedExternal' and not blueprint.local:
level_needs_export = True
break
# changes => list of changed objects (regardless of wether they have been changed in main scene or in lib scene)
# wich of those objects are blueprint instances
# we need a list of changed objects that are blueprint instances
return level_needs_export
return False
# this also takes 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, blueprints_data, addon_prefs):
export_change_detection = getattr(addon_prefs, "export_change_detection")
export_gltf_extension = getattr(addon_prefs, "export_gltf_extension")
export_levels_path = getattr(addon_prefs, "export_levels_path")
collection_instances_combine_mode = getattr(addon_prefs, "collection_instances_combine_mode")
[main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs)
# 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 changed_object_in_scene(scene_name, changes_per_scene, blueprints_data, collection_instances_combine_mode) or not check_if_blueprint_on_disk(scene_name, export_levels_path, export_gltf_extension) ]
return (main_scenes_to_export)

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

@ -0,0 +1,22 @@
import bpy
class SceneLink(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="") # type: ignore
scene: bpy.props.PointerProperty(type=bpy.types.Scene) # type: ignore
class SceneLinks(bpy.types.PropertyGroup):
name = bpy.props.StringProperty(name="List of scenes to export", default="Unknown")
items: bpy.props.CollectionProperty(type = SceneLink) # type: ignore
class CUSTOM_PG_sceneName(bpy.types.PropertyGroup):
name: bpy.props.StringProperty() # type: ignore
display: bpy.props.BoolProperty() # type: ignore
class CollectionToExport(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="") # type: ignore
class BlueprintsToExport(bpy.types.PropertyGroup):
name = bpy.props.StringProperty(name="List of collections to export", default="Unknown")
items: bpy.props.CollectionProperty(type = CollectionToExport) # type: ignore

View File

@ -0,0 +1,468 @@
import json
import bpy
from bpy.types import Operator
from bpy_extras.io_utils import ExportHelper
from bpy.props import (IntProperty, StringProperty, BoolProperty)
from ..ui.operators import OT_OpenFolderbrowser, draw_folder_browser
#from ..ui.main import GLTF_PT_auto_export_general, GLTF_PT_auto_export_main, GLTF_PT_auto_export_root
from .preferences import (AutoExportGltfAddonPreferences, AutoExportGltfPreferenceNames)
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'} # 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 = ''
#filepath: bpy.props.StringProperty(subtype="FILE_PATH", default="") # type: ignore
#list of settings (other than purely gltf settings) whose change should trigger a re-generation of gltf files
white_list = [
'auto_export',
'export_root_folder',
'export_output_folder',
'export_change_detection',
'export_scene_settings',
'main_scene_names',
'library_scene_names',
'export_blueprints',
'export_blueprints_path',
'export_marked_assets',
'collection_instances_combine_mode',
'export_levels_path',
'export_separate_dynamic_and_static_objects',
'export_materials_library',
'export_materials_path',
]
@classmethod
def register(cls):
bpy.types.WindowManager.main_scene = bpy.props.PointerProperty(type=bpy.types.Scene, name="main scene", description="main_scene_picker", poll=cls.is_scene_ok)
bpy.types.WindowManager.library_scene = bpy.props.PointerProperty(type=bpy.types.Scene, name="library scene", description="library_scene_picker", poll=cls.is_scene_ok)
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)
cls.main_scenes_index = 0
cls.library_scenes_index = 0
@classmethod
def unregister(cls):
del bpy.types.WindowManager.main_scene
del bpy.types.WindowManager.library_scene
del bpy.types.WindowManager.main_scenes_list_index
del bpy.types.WindowManager.library_scenes_list_index
def is_scene_ok(self, scene):
try:
operator = bpy.context.space_data.active_operator
return scene.name not in operator.main_scenes and scene.name not in operator.library_scenes
except:
return True
def format_settings(self):
# find all props to save
exceptional = [
# options that don't start with 'export_'
'collection_instances_combine_mode',
]
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
}
# we inject all that we need, the above is not sufficient
for (k, v) in self.properties.items():
if k in self.white_list or k not in AutoExportGltfPreferenceNames:
value = v
# FIXME: really weird having to do this
if k == "collection_instances_combine_mode":
value = self.collection_instances_combine_mode
if k == "export_materials":
value = self.export_materials
export_props[k] = value
# we add main & library scene names to our preferences
export_props['main_scene_names'] = list(map(lambda scene_data: scene_data.name, getattr(self,"main_scenes")))
export_props['library_scene_names'] = list(map(lambda scene_data: scene_data.name, getattr(self,"library_scenes")))
return export_props
def save_settings(self, context):
print("save settings")
auto_export_settings = self.format_settings()
self.properties['main_scene_names'] = auto_export_settings['main_scene_names']
self.properties['library_scene_names'] = auto_export_settings['library_scene_names']
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()
auto_export_settings = generate_complete_preferences_dict_auto(auto_export_settings)
stored_settings.write(json.dumps(auto_export_settings))
print("saved settings", auto_export_settings)
#print("saving settings", bpy.data.texts[".gltf_auto_export_settings"].as_string(), "raw", json.dumps(export_props))
def load_settings(self, context):
print("loading settings")
settings = None
try:
settings = bpy.data.texts[".gltf_auto_export_settings"].as_string()
settings = json.loads(settings)
except: pass
self.will_save_settings = False
if settings:
#print("loading settings in invoke AutoExportGLTF", 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'
# inject scenes data
if hasattr(self, 'main_scene_names'):
main_scenes = self.main_scenes
main_scenes.clear()
for item_name in self.main_scene_names:
item = main_scenes.add()
item.name = item_name
if hasattr(self, 'library_scene_names'):
library_scenes = self.library_scenes
library_scenes.clear()
for item_name in self.library_scene_names:
item = library_scenes.add()
item.name = item_name
except Exception as error:
print("error", error)
self.report({"ERROR"}, "Loading export settings failed. Removed corrupted settings")
bpy.data.texts.remove(bpy.data.texts[".gltf_auto_export_settings"])
else:
self.will_save_settings = True
"""
This should ONLY be run when actually doing exports/aka calling auto_export function, because we only care about the difference in settings between EXPORTS
"""
def did_export_settings_change(self):
# compare both the auto export settings & the gltf settings
previous_auto_settings = bpy.data.texts[".gltf_auto_export_settings_previous"] if ".gltf_auto_export_settings_previous" in bpy.data.texts else None
previous_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings_previous"] if ".gltf_auto_export_gltf_settings_previous" in bpy.data.texts else None
current_auto_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else None
current_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings"] if ".gltf_auto_export_gltf_settings" in bpy.data.texts else None
#check if params have changed
# if there were no setting before, it is new, we need export
changed = False
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.new(".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
"""print("auto settings previous", sorted(json.loads(previous_auto_settings.as_string()).items()))
print("auto settings current", sorted(json.loads(current_auto_settings.as_string()).items()))
print("auto_settings_changed", auto_settings_changed)
print("gltf settings previous", sorted(json.loads(previous_gltf_settings.as_string()).items()))
print("gltf settings current", sorted(json.loads(current_gltf_settings.as_string()).items()))
print("gltf_settings_changed", gltf_settings_changed)"""
changed = auto_settings_changed or gltf_settings_changed
# now write the current settings to the "previous settings"
if current_auto_settings != None:
previous_auto_settings = bpy.data.texts[".gltf_auto_export_settings_previous"] if ".gltf_auto_export_settings_previous" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings_previous")
previous_auto_settings.clear()
previous_auto_settings.write(current_auto_settings.as_string()) # TODO : check if this is always valid
if current_gltf_settings != None:
previous_gltf_settings = bpy.data.texts[".gltf_auto_export_gltf_settings_previous"] if ".gltf_auto_export_gltf_settings_previous" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_gltf_settings_previous")
previous_gltf_settings.clear()
previous_gltf_settings.write(current_gltf_settings.as_string())
return changed
def did_objects_change(self):
# 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:
previous_stored = bpy.data.texts.new(".TESTING")
previous_stored.write(current)
return {}
previous = json.loads(previous_stored.as_string())
current = json.loads(current)
changes_per_scene = {}
# TODO : how do we deal with changed scene names ???
for scene in current:
# print('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)
#print("current_object_names", len(current_object_names), current_object_names)
"""if len(previous_object_names) > len(current_object_names):
print("removed")
if len(current_object_names) > len(previous_object_names):
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)"""
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
#print("ob", object_name)
if object_name in previous[scene]:
# print("object", object_name,"in previous scene, comparing")
current_obj = current[scene][object_name]
prev_obj = previous[scene][object_name]
same = str(current_obj) == str(prev_obj)
if "Camera" in object_name:
pass#print(" current", current_obj, prev_obj)
"""if "Fox" in object_name:
print(" current", current_obj)
print(" previou", prev_obj)
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
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):
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)
#print("self", self.auto_export)
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
# 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)
# 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")
return {'FINISHED'}
def invoke(self, context, event):
#print("invoke")
bpy.context.window_manager.auto_export_tracker.disable_change_detection()
self.load_settings(context)
wm = context.window_manager
#wm.fileselect_add(self)
return context.window_manager.invoke_props_dialog(self, title="Auto export", width=640)
return {'RUNNING_MODAL'}
"""def modal(self, context, event):
if event.type == 'SPACE':
wm = context.window_manager
wm.invoke_popup(self)
#wm.invoke_props_dialog(self)
if event.type in {'ESC'}:
return {'CANCELLED'}
return {'RUNNING_MODAL'}"""
def draw(self, context):
layout = self.layout
operator = self
controls_enabled = self.auto_export
layout.prop(self, "auto_export")
layout.separator()
toggle_icon = "TRIA_DOWN" if self.show_general_settings else "TRIA_RIGHT"
layout.prop(self, "show_general_settings", text="General", icon=toggle_icon)
if self.show_general_settings:
section = layout.box()
section.enabled = controls_enabled
draw_folder_browser(section, "Export root folder", self.export_root_folder, "export_root_folder")
row = section.row()
draw_folder_browser(row, "Assets Folder (non blueprints mode only)", self.export_root_folder, "export_output_folder")
row.enabled = not self.export_blueprints
section.prop(operator, "export_blueprints")
section.prop(operator, "export_scene_settings")
"""header, panel = layout.panel("my_panel_id", default_closed=False)
header.label(text="Hello World")
if panel:
panel.label(text="Success")"""
toggle_icon = "TRIA_DOWN" if self.show_change_detection_settings else "TRIA_RIGHT"
layout.prop(operator, "show_change_detection_settings", text="Change Detection", icon=toggle_icon)
if self.show_change_detection_settings:
section = layout.box()
section.enabled = controls_enabled
section.prop(operator, "export_change_detection", text="Use change detection")
# main/level scenes
toggle_icon = "TRIA_DOWN" if self.show_scene_settings else "TRIA_RIGHT"
layout.prop(operator, "show_scene_settings", text="Scenes", icon=toggle_icon)
if self.show_scene_settings:
section = layout.box()
section.enabled = controls_enabled
rows = 2
row = section.row()
row.label(text="main scenes")
row.prop(context.window_manager, "main_scene", text='')
row = section.row()
row.template_list("SCENE_UL_GLTF_auto_export", "level scenes", operator, "main_scenes", operator, "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.operator = operator
sub_row.enabled = context.window_manager.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()
# library scenes
row = section.row()
row.label(text="library scenes")
row.prop(context.window_manager, "library_scene", text='')
row = section.row()
row.template_list("SCENE_UL_GLTF_auto_export", "library scenes", operator, "library_scenes", operator, "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.window_manager.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()
toggle_icon = "TRIA_DOWN" if self.show_blueprint_settings else "TRIA_RIGHT"
layout.prop(operator, "show_blueprint_settings", text="Blueprints", icon=toggle_icon)
if self.show_blueprint_settings:
section = layout.box()
section.enabled = controls_enabled
section = section.box()
section.enabled = controls_enabled and self.export_blueprints
# collections/blueprints
draw_folder_browser(section, "Blueprints folder", self.export_root_folder, "export_blueprints_path")
#section.prop(operator, "export_blueprints_path")
section.prop(operator, "collection_instances_combine_mode")
section.prop(operator, "export_marked_assets")
section.separator()
draw_folder_browser(section, "Levels folder", self.export_root_folder, "export_levels_path")
#section.prop(operator, "export_levels_path")
section.prop(operator, "export_separate_dynamic_and_static_objects")
section.separator()
# materials
section.prop(operator, "export_materials_library")
section = section.box()
section.enabled = controls_enabled and self.export_materials_library
draw_folder_browser(section, 'Materials folder', self.export_root_folder, "export_materials_path")
#section.prop(operator, "export_materials_path")
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

@ -0,0 +1,208 @@
import os
from bpy.types import AddonPreferences
from bpy.props import (BoolProperty,
IntProperty,
StringProperty,
EnumProperty,
CollectionProperty
)
from .internals import (CUSTOM_PG_sceneName)
AutoExportGltfPreferenceNames = [
'will_save_settings',
'direct_mode',# specific to main auto_export operator
'show_general_settings',
'auto_export',
'export_root_folder',
'export_output_folder',
'export_scene_settings',
'show_change_detection_settings',
'export_change_detection',
'show_scene_settings',
'main_scenes',
'library_scenes',
'main_scenes_index',
'library_scenes_index',
'main_scene_names',
'library_scene_names',
'show_blueprint_settings',
'export_blueprints',
'export_blueprints_path',
'export_marked_assets',
'collection_instances_combine_mode',
'export_levels_path',
'export_separate_dynamic_and_static_objects',
'export_materials_library',
'export_materials_path',
]
def on_export_output_folder_updated(self, context):
#self.export_root_folder = os.path.relpath(self.export_root_folder)
#self.export_output_folder = os.path.join(self.export_root_folder, self.export_output_folder)
print("on_foo_updated", self.export_root_folder, self.export_output_folder)
class AutoExportGltfAddonPreferences(AddonPreferences):
# this must match the add-on name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __package__
bl_options = {'PRESET'}
#### these are for the operator
will_save_settings: BoolProperty(
name='Remember Export Settings',
description='Store glTF export settings in the Blender project',
default=True
) # type: ignore
# use when operator is called directly, works a bit differently than inside the ui
direct_mode: BoolProperty(
default=False
) # type: ignore
auto_export: BoolProperty(
name='Auto export',
description='Automatically export to gltf on save',
default=False
) # type: ignore
#### general
# for UI only, workaround for lacking panels
show_general_settings: BoolProperty(
name="show_general settings",
description="show/hide general settings (UI only: has no impact on exports)",
default=True
) # type: ignore
export_root_folder: StringProperty(
name = "Project Root Path",
description="The root folder of your (Bevy) project (not assets!)",
# subtype='DIR_PATH',
default='../'
#update=on_export_output_folder_updated) # type: ignore
)
export_output_folder: StringProperty(
name='Export folder',
description='The root folder for all exports(relative to the root folder/path) Defaults to "assets" ',
default='./assets',
#subtype='DIR_PATH',
options={'HIDDEN'}
# update=on_export_output_folder_updated
) # type: ignore
# for UI only, workaround for lacking panels
show_change_detection_settings: BoolProperty(
name="show change detection settings",
description="show/hide change detection settings (UI only: has no impact on exports)",
default=True
) # type: ignore
export_change_detection: BoolProperty(
name='Change detection',
description='Use change detection to determine what/if should be exported',
default=True
) # type: ignore
# scenes
# for UI only, workaround for lacking panels
show_scene_settings: BoolProperty(
name="show scene settings",
description="show/hide scene settings (UI only: has no impact on exports)",
default=True
) # type: ignore
# scene components
export_scene_settings: BoolProperty(
name='Export scene settings',
description='Export scene settings ie AmbientLighting, Bloom, AO etc',
default=False
) # type: ignore
# blueprint settings
# for UI only, workaround for lacking panels
show_blueprint_settings: BoolProperty(
name="show blueprint settings",
description="show/hide blueprint settings (UI only: has no impact on exports)",
default=True
) # type: ignore
export_blueprints: BoolProperty(
name='Export Blueprints',
description='Replaces collection instances with an Empty with a BlueprintName custom property, and enabled a lot more features !',
default=True
) # type: ignore
export_blueprints_path: StringProperty(
name='Blueprints path',
description='path to export the blueprints to (relative to the export folder)',
default='assets/blueprints',
#subtype='DIR_PATH'
) # type: ignore
export_levels_path: StringProperty(
name='Levels path',
description='path to export the levels (main scenes) to (relative to the export folder)',
default='assets/levels',
#subtype='DIR_PATH'
) # type: ignore
export_separate_dynamic_and_static_objects: BoolProperty(
name="Export levels' dynamic and static objects seperatly",
description="""For MAIN scenes only (aka levels), toggle this to generate 2 files per level:
- one with all dynamic data: collection or instances marked as dynamic/ saveable
- one with all static data: anything else that is NOT marked as dynamic""",
default=False
) # type: ignore
export_materials_library: BoolProperty(
name='Export materials library',
description='remove materials from blueprints and use the material library instead',
default=False
) # type: ignore
export_materials_path: StringProperty(
name='Materials path',
description='path to export the materials libraries to (relative to the export folder)',
default='assets/materials',
#subtype='DIR_PATH'
) # type: ignore
""" combine mode can be
- 'Split' (default): replace with an empty, creating links to sub blueprints
- 'Embed' : treat it as an embeded object and do not replace it with an empty
- 'EmbedExternal': embed any instance of a non local collection (ie external assets)
- 'Inject': inject components from sub collection instances into the curent object => this is now a seperate custom property that you can apply to a collecion instance
"""
collection_instances_combine_mode : EnumProperty(
name='Collection instances',
items=(
('Split', 'Split', 'replace collection instances with an empty + blueprint, creating links to sub blueprints (Default, Recomended)'),
('Embed', 'Embed', 'treat collection instances as embeded objects and do not replace them with an empty'),
('EmbedExternal', 'EmbedExternal', 'treat instances of external (not specifified in the current blend file) collections (aka assets etc) as embeded objects and do not replace them with empties'),
#('Inject', 'Inject', 'inject components from sub collection instances into the curent object')
),
default='Split'
) # type: ignore
export_marked_assets: BoolProperty(
name='Auto export marked assets',
description='Collections that have been marked as assets will be systematically exported, even if not in use in another scene',
default=True
) # type: ignore
main_scenes: CollectionProperty(name="main scenes", type=CUSTOM_PG_sceneName) # type: ignore
main_scenes_index: IntProperty(name = "Index for main scenes list", default = 0) # type: ignore
library_scenes: CollectionProperty(name="library scenes", type=CUSTOM_PG_sceneName) # type: ignore
library_scenes_index: IntProperty(name = "Index for library scenes list", default = 0) # type: ignore

View File

@ -0,0 +1,195 @@
import json
import bpy
from bpy.types import (PropertyGroup)
from bpy.props import (PointerProperty, IntProperty, StringProperty)
from .get_blueprints_to_export import get_blueprints_to_export
from ..constants import TEMPSCENE_PREFIX
from .internals import BlueprintsToExport
from ..helpers.helpers_scenes import (get_scenes)
from .preferences import AutoExportGltfAddonPreferences
class AutoExportTracker(PropertyGroup):
changed_objects_per_scene = {}
change_detection_enabled = True
export_params_changed = False
gltf_settings_backup = None
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)
# register list of exportable collections
bpy.types.WindowManager.exportedCollections = bpy.props.CollectionProperty(type=BlueprintsToExport)
# setup handlers for updates & saving
#bpy.app.handlers.save_post.append(cls.save_handler)
#bpy.app.handlers.depsgraph_update_post.append(cls.deps_update_handler)
@classmethod
def unregister(cls):
# remove handlers & co
"""try:
bpy.app.handlers.depsgraph_update_post.remove(cls.deps_update_handler)
except:pass
try:
bpy.app.handlers.save_post.remove(cls.save_handler)
except:pass"""
del bpy.types.WindowManager.auto_export_tracker
del bpy.types.WindowManager.exportedCollections
@classmethod
def save_handler(cls, scene, depsgraph):
print("-------------")
print("saved", bpy.data.filepath)
# auto_export(changes_per_scene, export_parameters_changed)
bpy.ops.export_scenes.auto_gltf(direct_mode= True)
# (re)set a few things after exporting
# reset wether the gltf export paramters were changed since the last save
cls.export_params_changed = False
# reset whether there have been changed objects since the last save
cls.changed_objects_per_scene.clear()
# all our logic is done, mark this as done
@classmethod
def deps_post_update_handler(cls, scene, depsgraph):
# 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)
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
if "glTF2ExportSettings" in scene:
existing_setting = scene["glTF2ExportSettings"]
bpy.context.window_manager.gltf_settings_backup = json.dumps(dict(existing_setting))
# we force saving params
active_operator.will_save_settings = True
# we set the last operator here so we can clear the specific settings (yeah for overly complex logic)
cls.last_operator = active_operator
#print("active_operator", active_operator.has_active_exporter_extensions, active_operator.__annotations__.keys(), active_operator.filepath, active_operator.gltf_export_id)
return
if active_operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf":
# we force saving params
active_operator.will_save_settings = True
active_operator.auto_export = True
# if we are using the operator, bail out for the rest
print("setting stuff for auto_export")
return
# 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 ""
#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()
for obj in depsgraph.updates:
#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, "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]
#now find which objects are using the material
for obj in bpy.data.objects:
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)
# filter out invalid objects
"""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]"""
#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]
#cls.changed_objects_per_scene[scene.name]= bla
#cls.changed_objects_per_scene[scene_name] = [o for o in cls.changed_objects_per_scene[scene_name] if not o.invalid]
# get a list of exportable collections for display
# keep it simple, just use Simplenamespace for compatibility with the rest of our code
# TODO: debounce
def disable_change_detection(self):
#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
#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()
def export_finished(self):
#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

View File

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

View File

@ -0,0 +1,58 @@
import bpy
from ..auto_export.export_gltf import export_gltf
from .helpers_collections import (set_active_collection)
"""
generates a temporary scene, fills it with data, cleans up after itself
* named using temp_scene_name
* filled using the tempScene_filler
* written on disk to gltf_output_path, with the gltf export parameters in export_settings
* cleaned up using tempScene_cleaner
"""
def generate_and_export(addon_prefs, export_settings, gltf_output_path, temp_scene_name="__temp_scene", tempScene_filler=None, tempScene_cleaner=None):
temp_scene = bpy.data.scenes.new(name=temp_scene_name)
temp_root_collection = temp_scene.collection
# save active scene
original_scene = bpy.context.window.scene
# and selected collection
original_collection = bpy.context.view_layer.active_layer_collection
# and mode
original_mode = bpy.context.active_object.mode if bpy.context.active_object != None else None
# we change the mode to object mode, otherwise the gltf exporter is not happy
if original_mode != None and original_mode != 'OBJECT':
print("setting to object mode", original_mode)
bpy.ops.object.mode_set(mode='OBJECT')
# we set our active scene to be this one : this is needed otherwise the stand-in empties get generated in the wrong scene
bpy.context.window.scene = temp_scene
area = [area for area in bpy.context.screen.areas if area.type == "VIEW_3D"][0]
region = [region for region in area.regions if region.type == 'WINDOW'][0]
with bpy.context.temp_override(scene=temp_scene, area=area, region=region):
# detect scene mistmatch
scene_mismatch = bpy.context.scene.name != bpy.context.window.scene.name
if scene_mismatch:
raise Exception("Context scene mismatch, aborting", bpy.context.scene.name, bpy.context.window.scene.name)
set_active_collection(bpy.context.scene, temp_root_collection.name)
# generate contents of temporary scene
scene_filler_data = tempScene_filler(temp_root_collection)
# export the temporary scene
try:
export_gltf(gltf_output_path, export_settings)
except Exception as error:
print("failed to export gltf !", error)
raise error
# restore everything
tempScene_cleaner(temp_scene, scene_filler_data)
# reset active scene
bpy.context.window.scene = original_scene
# reset active collection
bpy.context.view_layer.active_layer_collection = original_collection
# reset mode
if original_mode != None:
bpy.ops.object.mode_set( mode = original_mode )

View File

@ -0,0 +1,47 @@
from ..auto_export.preferences import AutoExportGltfAddonPreferences
from io_scene_gltf2 import (ExportGLTF2_Base)
# given the input (actual) gltf settings, filters out any invalid/useless params & params that are equal to defaults
def generate_complete_preferences_dict_gltf(settings):
complete_preferences = {}
defaults = {}
gltf_parameters_to_ignore = ["use_active_collection", "use_active_collection_with_nested", "use_active_scene", "use_selection", "will_save_settings", "gltf_export_id"]
def filter_out(pair):
key, value = pair
if key in gltf_parameters_to_ignore:
return False
return True
for k in ExportGLTF2_Base.__annotations__: # we use parameters from the base class of the standard gltf exporter, that contains all relevant parameters
item = ExportGLTF2_Base.__annotations__[k]
#print("item", item)
default = item.keywords.get('default', None)
#complete_preferences[k] = default
defaults[k] = default
for key in list(settings.keys()):
if key in defaults and settings[key] != defaults[key]: # only write out values different from defaults
complete_preferences[key] = settings[key]
complete_preferences = dict(filter(filter_out, dict(complete_preferences).items()))
return complete_preferences
# given the input (actual) auto settings, filters out any invalid/useless params & params that are equal to defaults
def generate_complete_preferences_dict_auto(settings):
complete_preferences = {}
defaults = {}
for k in AutoExportGltfAddonPreferences.__annotations__:
item = AutoExportGltfAddonPreferences.__annotations__[k]
default = item.keywords.get('default', None)
#complete_preferences[k] = default
defaults[k] = default
for key in list(settings.keys()):
if key in defaults:
if settings[key] != defaults[key]: # only write out values different from defaults
complete_preferences[key] = settings[key]
else:
complete_preferences[key] = settings[key]
return complete_preferences

View File

@ -0,0 +1,400 @@
import os
from types import SimpleNamespace
import bpy
class Blueprint:
def __init__(self, name):
self.name = name
self.local = True
self.marked = False # If marked as asset or with auto_export flag, always export if changed
self.scene = None # Not sure, could be usefull for tracking
self.instances = []
self.objects = []
self.nested_blueprints = []
self.collection = None # should we just sublclass ?
def __repr__(self):
return f'Name: {self.name} Local: {self.local}, Scene: {self.scene}, Instances: {self.instances}, Objects: {self.objects}, nested_blueprints: {self.nested_blueprints}'
def __str__(self):
return f'Name: "{self.name}", Local: {self.local}, Scene: {self.scene}, Instances: {self.instances}, Objects: {self.objects}, nested_blueprints: {self.nested_blueprints}'
def find_blueprints_not_on_disk(blueprints, folder_path, extension):
not_found_blueprints = []
for blueprint in blueprints:
gltf_output_path = os.path.join(folder_path, blueprint.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(blueprint)
return not_found_blueprints
def check_if_blueprint_on_disk(scene_name, folder_path, extension):
gltf_output_path = os.path.join(folder_path, scene_name + extension)
found = os.path.exists(gltf_output_path) and os.path.isfile(gltf_output_path)
print("level", scene_name, "found", found, "path", gltf_output_path)
return found
# blueprints: any collection with either
# - an instance
# - marked as asset
# - with the "auto_export" flag
# https://blender.stackexchange.com/questions/167878/how-to-get-all-collections-of-the-current-scene
def blueprints_scan(main_scenes, library_scenes, addon_prefs):
export_marked_assets = getattr(addon_prefs,"export_marked_assets")
blueprints = {}
blueprints_from_objects = {}
blueprint_name_from_instances = {}
collections = []
# main scenes
blueprint_instances_per_main_scene = {}
internal_collection_instances = {}
external_collection_instances = {}
# meh
def add_object_to_collection_instances(collection_name, object, internal=True):
collection_category = internal_collection_instances if internal else external_collection_instances
if not collection_name in collection_category.keys():
#print("ADDING INSTANCE OF", collection_name, "object", object.name, "categ", collection_category)
collection_category[collection_name] = [] #.append(collection_name)
collection_category[collection_name].append(object)
for scene in main_scenes:# should it only be main scenes ? what about collection instances inside other scenes ?
for object in scene.objects:
#print("object", object.name)
if object.instance_type == 'COLLECTION':
collection = object.instance_collection
collection_name = object.instance_collection.name
#print(" from collection:", collection_name)
collection_from_library = False
for library_scene in library_scenes: # should be only in library scenes
collection_from_library = library_scene.user_of_id(collection) > 0 # TODO: also check if it is an imported asset
if collection_from_library:
break
add_object_to_collection_instances(collection_name=collection_name, object=object, internal = collection_from_library)
# experiment with custom properties from assets stored in other blend files
"""if not collection_from_library:
for property_name in object.keys():
print("stuff", property_name)
for property_name in collection.keys():
print("OTHER", property_name)"""
# blueprints[collection_name].instances.append(object)
# FIXME: this only account for direct instances of blueprints, not for any nested blueprint inside a blueprint
if scene.name not in blueprint_instances_per_main_scene.keys():
blueprint_instances_per_main_scene[scene.name] = {}
if collection_name not in blueprint_instances_per_main_scene[scene.name].keys():
blueprint_instances_per_main_scene[scene.name][collection_name] = []
blueprint_instances_per_main_scene[scene.name][collection_name].append(object)
blueprint_name_from_instances[object] = collection_name
"""# add any indirect ones
# FIXME: needs to be recursive, either here or above
for nested_blueprint in blueprints[collection_name].nested_blueprints:
if not nested_blueprint in blueprint_instances_per_main_scene[scene.name]:
blueprint_instances_per_main_scene[scene.name].append(nested_blueprint)"""
for collection in bpy.data.collections:
#print("collection", collection, collection.name_full, "users", collection.users)
collection_from_library = False
defined_in_scene = None
for scene in library_scenes: # should be only in library scenes
collection_from_library = scene.user_of_id(collection) > 0
if collection_from_library:
defined_in_scene = scene
break
if not collection_from_library:
continue
if (
'AutoExport' in collection and collection['AutoExport'] == True # get marked collections
or export_marked_assets and collection.asset_data is not None # or if you have marked collections as assets you can auto export them too
or collection.name in list(internal_collection_instances.keys()) # or if the collection has an instance in one of the main scenes
):
blueprint = Blueprint(collection.name)
blueprint.local = True
blueprint.marked = 'AutoExport' in collection and collection['AutoExport'] == True or export_marked_assets and collection.asset_data is not None
blueprint.objects = [object.name for object in collection.all_objects if not object.instance_type == 'COLLECTION'] # inneficient, double loop
blueprint.nested_blueprints = [object.instance_collection.name for object in collection.all_objects if object.instance_type == 'COLLECTION'] # FIXME: not precise enough, aka "what is a blueprint"
blueprint.collection = collection
blueprint.instances = internal_collection_instances[collection.name] if collection.name in internal_collection_instances else []
blueprint.scene = defined_in_scene
blueprints[collection.name] = blueprint
# add nested collections to internal/external_collection instances
# FIXME: inneficient, third loop over all_objects
for object in collection.all_objects:
if object.instance_type == 'COLLECTION':
add_object_to_collection_instances(collection_name=object.instance_collection.name, object=object, internal = blueprint.local)
# now create reverse lookup , so you can find the collection from any of its contained objects
for object in collection.all_objects:
blueprints_from_objects[object.name] = blueprint#collection.name
#
collections.append(collection)
# add any collection that has an instance in the main scenes, but is not present in any of the scenes (IE NON LOCAL/ EXTERNAL)
for collection_name in external_collection_instances:
collection = bpy.data.collections[collection_name]
blueprint = Blueprint(collection.name)
blueprint.local = False
blueprint.marked = True #external ones are always marked, as they have to have been marked in their original file #'AutoExport' in collection and collection['AutoExport'] == True
blueprint.objects = [object.name for object in collection.all_objects if not object.instance_type == 'COLLECTION'] # inneficient, double loop
blueprint.nested_blueprints = [object.instance_collection.name for object in collection.all_objects if object.instance_type == 'COLLECTION'] # FIXME: not precise enough, aka "what is a blueprint"
blueprint.collection = collection
blueprint.instances = external_collection_instances[collection.name] if collection.name in external_collection_instances else []
blueprints[collection.name] = blueprint
#print("EXTERNAL COLLECTION", collection, dict(collection))
# add nested collections to internal/external_collection instances
# FIXME: inneficient, third loop over all_objects
"""for object in collection.all_objects:
if object.instance_type == 'COLLECTION':
add_object_to_collection_instances(collection_name=object.instance_collection.name, object=object, internal = blueprint.local)"""
# now create reverse lookup , so you can find the collection from any of its contained objects
for object in collection.all_objects:
blueprints_from_objects[object.name] = blueprint#collection.name
# then add any nested collections at root level (so we can have a flat list, regardless of nesting)
# TODO: do this recursively
for blueprint_name in list(blueprints.keys()):
parent_blueprint = blueprints[blueprint_name]
for nested_blueprint_name in parent_blueprint.nested_blueprints:
if not nested_blueprint_name in blueprints.keys():
collection = bpy.data.collections[nested_blueprint_name]
blueprint = Blueprint(collection.name)
blueprint.local = parent_blueprint.local
blueprint.objects = [object.name for object in collection.all_objects if not object.instance_type == 'COLLECTION'] # inneficient, double loop
blueprint.nested_blueprints = [object.instance_collection.name for object in collection.all_objects if object.instance_type == 'COLLECTION'] # FIXME: not precise enough, aka "what is a blueprint"
blueprint.collection = collection
blueprint.instances = external_collection_instances[collection.name] if collection.name in external_collection_instances else []
blueprint.scene = parent_blueprint.scene if parent_blueprint.local else None
blueprints[collection.name] = blueprint
# now create reverse lookup , so you can find the collection from any of its contained objects
for object in collection.all_objects:
blueprints_from_objects[object.name] = blueprint#collection.name
blueprints = dict(sorted(blueprints.items()))
'''print("BLUEPRINTS")
for blueprint_name in blueprints:
print(" ", blueprints[blueprint_name])
"""print("BLUEPRINTS LOOKUP")
print(blueprints_from_objects)"""
print("BLUEPRINT INSTANCES PER MAIN SCENE")
print(blueprint_instances_per_main_scene)'''
"""changes_test = {'Library': {
'Blueprint1_mesh': bpy.data.objects['Blueprint1_mesh'],
'Fox_mesh': bpy.data.objects['Fox_mesh'],
'External_blueprint2_Cylinder': bpy.data.objects['External_blueprint2_Cylinder']}
}
# which main scene has been impacted by this
# does one of the main scenes contain an INSTANCE of an impacted blueprint
for scene in main_scenes:
changed_objects = list(changes_test["Library"].keys()) # just a hack for testing
#bluprint_instances_in_scene = blueprint_instances_per_main_scene[scene.name]
#print("instances per scene", bluprint_instances_in_scene, "changed_objects", changed_objects)
changed_blueprints_with_instances_in_scene = [blueprints_from_objects[changed] for changed in changed_objects if changed in blueprints_from_objects]
print("changed_blueprints_with_instances_in_scene", changed_blueprints_with_instances_in_scene)
level_needs_export = len(changed_blueprints_with_instances_in_scene) > 0
if level_needs_export:
print("level needs export", scene.name)
for scene in library_scenes:
changed_objects = list(changes_test[scene.name].keys())
changed_blueprints = [blueprints_from_objects[changed] for changed in changed_objects if changed in blueprints_from_objects]
# we only care about local blueprints/collections
changed_local_blueprints = [blueprint_name for blueprint_name in changed_blueprints if blueprint_name in blueprints.keys() and blueprints[blueprint_name].local]
print("changed blueprints", changed_local_blueprints)"""
# additional helper data structures for lookups etc
blueprints_per_name = blueprints
blueprints = [] # flat list
internal_blueprints = []
external_blueprints = []
blueprints_per_scenes = {}
blueprint_instances_per_library_scene = {}
for blueprint in blueprints_per_name.values():
blueprints.append(blueprint)
if blueprint.local:
internal_blueprints.append(blueprint)
if blueprint.scene:
if not blueprint.scene.name in blueprints_per_scenes:
blueprints_per_scenes[blueprint.scene.name] = []
blueprints_per_scenes[blueprint.scene.name].append(blueprint.name) # meh
else:
external_blueprints.append(blueprint)
# we also need to have blueprint instances for
data = {
"blueprints": blueprints,
"blueprints_per_name": blueprints_per_name,
"blueprint_names": list(blueprints_per_name.keys()),
"blueprints_from_objects": blueprints_from_objects,
"internal_blueprints": internal_blueprints,
"external_blueprints": external_blueprints,
"blueprints_per_scenes": blueprints_per_scenes,
"blueprint_instances_per_main_scene": blueprint_instances_per_main_scene,
"blueprint_instances_per_library_scene": blueprint_instances_per_library_scene,
# not sure about these two
"internal_collection_instances": internal_collection_instances,
"external_collection_instances": external_collection_instances,
"blueprint_name_from_instances": blueprint_name_from_instances
}
return SimpleNamespace(**data)
import json
from .object_makers import (make_empty)
def add_scene_property(scene, property_name, property_data):
root_collection = scene.collection
scene_property = None
for object in scene.objects:
if object.name == property_name:
scene_property = object
break
if scene_property is None:
scene_property = make_empty(property_name, [0,0,0], [0,0,0], [0,0,0], root_collection)
for key in property_data.keys():
scene_property[key] = property_data[key]
def inject_blueprints_list_into_main_scene(scene, blueprints_data, addon_prefs):
export_root_folder = getattr(addon_prefs, "export_root_folder")
export_output_folder = getattr(addon_prefs,"export_output_folder")
export_levels_path = getattr(addon_prefs,"export_levels_path")
export_blueprints_path = getattr(addon_prefs, "export_blueprints_path")
export_gltf_extension = getattr(addon_prefs, "export_gltf_extension")
# print("injecting assets/blueprints data into scene")
assets_list_name = f"assets_list_{scene.name}_components"
assets_list_data = {}
# FIXME: temporary hack
for blueprint in blueprints_data.blueprints:
bpy.context.window_manager.blueprints_registry.add_blueprint(blueprint)
blueprint_instance_names_for_scene = blueprints_data.blueprint_instances_per_main_scene.get(scene.name, None)
# find all blueprints used in a scene
blueprints_in_scene = []
if blueprint_instance_names_for_scene: # what are the blueprints used in this scene, inject those into the assets list component
children_per_blueprint = {}
for blueprint_name in blueprint_instance_names_for_scene:
blueprint = blueprints_data.blueprints_per_name.get(blueprint_name, None)
if blueprint:
children_per_blueprint[blueprint_name] = blueprint.nested_blueprints
blueprints_in_scene += blueprint.nested_blueprints
assets_list_data["BlueprintsList"] = f"({json.dumps(dict(children_per_blueprint))})"
print(blueprint_instance_names_for_scene)
add_scene_property(scene, assets_list_name, assets_list_data)
relative_blueprints_path = os.path.relpath(export_blueprints_path, export_root_folder)
blueprint_assets_list = []
if blueprint_instance_names_for_scene:
for blueprint_name in blueprint_instance_names_for_scene:
blueprint = blueprints_data.blueprints_per_name.get(blueprint_name, None)
if blueprint is not None:
print("BLUEPRINT", blueprint)
blueprint_exported_path = None
if blueprint.local:
blueprint_exported_path = os.path.join(relative_blueprints_path, f"{blueprint.name}{export_gltf_extension}")
else:
# get the injected path of the external blueprints
blueprint_exported_path = blueprint.collection['Export_path'] if 'Export_path' in blueprint.collection else None
print("foo", dict(blueprint.collection))
if blueprint_exported_path is not None:
blueprint_assets_list.append({"name": blueprint.name, "path": blueprint_exported_path, "type": "MODEL", "internal": True})
# fetch images/textures
# see https://blender.stackexchange.com/questions/139859/how-to-get-absolute-file-path-for-linked-texture-image
textures = []
for ob in bpy.data.objects:
if ob.type == "MESH":
for mat_slot in ob.material_slots:
if mat_slot.material:
if mat_slot.material.node_tree:
textures.extend([x.image.filepath for x in mat_slot.material.node_tree.nodes if x.type=='TEX_IMAGE'])
print("textures", textures)
assets_list_name = f"assets_{scene.name}"
assets_list_data = {"blueprints": json.dumps(blueprint_assets_list), "sounds":[], "images":[]}
scene["assets"] = json.dumps(blueprint_assets_list)
print("blueprint assets", blueprint_assets_list)
add_scene_property(scene, assets_list_name, assets_list_data)
for blueprint in blueprint_assets_list:
bpy.context.window_manager.assets_registry.add_asset(**blueprint)
'''root_collection = scene.collection
assets_list = None
for object in scene.objects:
if object.name == assets_list_name:
assets_list = object
break
if assets_list is None:
assets_list = make_empty(assets_list_name, [0,0,0], [0,0,0], [0,0,0], root_collection)
blueprint_names_for_scene = blueprints_data.blueprint_instances_per_main_scene.get(scene.name, None)
# find all blueprints used in a scene
if blueprint_names_for_scene: # what are the blueprints used in this scene, inject those into the assets list component
children_per_blueprint = {}
for blueprint_name in blueprint_names_for_scene:
blueprint = blueprints_data.blueprints_per_name.get(blueprint_name, None)
if blueprint:
children_per_blueprint[blueprint_name] = blueprint.nested_blueprints
assets_list["BlueprintsList"] = f"({json.dumps(dict(children_per_blueprint))})"'''
def remove_blueprints_list_from_main_scene(scene):
assets_list = None
assets_list_name = f"assets_list_{scene.name}_components"
for object in scene.objects:
if object.name == assets_list_name:
assets_list = object
if assets_list is not None:
bpy.data.objects.remove(assets_list, do_unlink=True)

View File

@ -0,0 +1,23 @@
import bpy
# traverse all collections
def traverse_tree(t):
yield t
for child in t.children:
yield from traverse_tree(child)
#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 set_active_collection(scene, collection_name):
layer_collection = bpy.data.scenes[scene.name].view_layers['ViewLayer'].layer_collection
layerColl = recurLayerCollection(layer_collection, collection_name)
# set active collection to the collection
bpy.context.view_layer.active_layer_collection = layerColl

View File

@ -0,0 +1,222 @@
import json
import bpy
from .object_makers import (make_empty)
# these are mostly for when using this add-on together with the bevy_components add-on
custom_properties_to_filter_out = ['_combine', 'template', 'components_meta']
def is_component_valid(object, component_name):
if "components_meta" in object or hasattr(object, "components_meta"):
target_components_metadata = object.components_meta.components
component_meta = next(filter(lambda component: component["long_name"] == component_name, target_components_metadata), None)
if component_meta != None:
return component_meta.enabled and not component_meta.invalid
return True
def remove_unwanted_custom_properties(object):
to_remove = []
component_names = list(object.keys()) # to avoid 'IDPropertyGroup changed size during iteration' issues
for component_name in component_names:
if not is_component_valid(object, component_name):
to_remove.append(component_name)
for cp in custom_properties_to_filter_out + to_remove:
if cp in object:
del object[cp]
# TODO: rename actions ?
# reference https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py#L481
def copy_animation_data(source, target):
if source.animation_data:
ad = source.animation_data
blender_actions = []
blender_tracks = {}
# TODO: this might need to be modified/ adapted to match the standard gltf exporter settings
for track in ad.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:
# 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
# Remove duplicate actions.
blender_actions = list(set(blender_actions))
# sort animations alphabetically (case insensitive) so they have a defined order and match Blender's Action list
blender_actions.sort(key = lambda a: a.name.lower())
markers_per_animation = {}
animations_infos = []
for action in blender_actions:
animation_name = blender_tracks[action.name]
animations_infos.append(
f'(name: "{animation_name}", frame_start: {action.frame_range[0]}, frame_end: {action.frame_range[1]}, frames_length: {action.frame_range[1] - action.frame_range[0]}, frame_start_override: {action.frame_start}, frame_end_override: {action.frame_end})'
)
markers_per_animation[animation_name] = {}
for marker in action.pose_markers:
if marker.frame not in markers_per_animation[animation_name]:
markers_per_animation[animation_name][marker.frame] = []
markers_per_animation[animation_name][marker.frame].append(marker.name)
# best method, using the built-in link animation operator
with bpy.context.temp_override(active_object=source, selected_editable_objects=[target]):
bpy.ops.object.make_links_data(type='ANIMATION')
"""if target.animation_data == None:
target.animation_data_create()
target.animation_data.action = source.animation_data.action.copy()
print("copying animation data for", source.name, target.animation_data)
properties = [p.identifier for p in source.animation_data.bl_rna.properties if not p.is_readonly]
for prop in properties:
print("copying stuff", prop)
setattr(target.animation_data, prop, getattr(source.animation_data, prop))"""
# we add an "AnimationInfos" component
target['AnimationInfos'] = f'(animations: {animations_infos})'.replace("'","")
# and animation markers
markers_formated = '{'
for animation in markers_per_animation.keys():
markers_formated += f'"{animation}":'
markers_formated += "{"
for frame in markers_per_animation[animation].keys():
markers = markers_per_animation[animation][frame]
markers_formated += f"{frame}:{markers}, ".replace("'", '"')
markers_formated += '}, '
markers_formated += '}'
target["AnimationMarkers"] = f'( {markers_formated} )'
def duplicate_object(object, parent, combine_mode, destination_collection, blueprints_data, nester=""):
copy = None
internal_blueprint_names = [blueprint.name for blueprint in blueprints_data.internal_blueprints]
# print("COMBINE MODE", combine_mode)
if object.instance_type == 'COLLECTION' and (combine_mode == 'Split' or (combine_mode == 'EmbedExternal' and (object.instance_collection.name in internal_blueprint_names)) ):
#print("creating empty for", object.name, object.instance_collection.name, internal_blueprint_names, combine_mode)
collection_name = object.instance_collection.name
original_name = object.name
object.name = original_name + "____bak"
empty_obj = make_empty(original_name, object.location, object.rotation_euler, object.scale, destination_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["BlueprintPath"] = ''
empty_obj['SpawnHere'] = '()'
# we also inject a list of all sub blueprints, so that the bevy side can preload them
blueprint_name = collection_name
children_per_blueprint = {}
blueprint = blueprints_data.blueprints_per_name.get(blueprint_name, None)
if blueprint:
children_per_blueprint[blueprint_name] = blueprint.nested_blueprints
empty_obj["BlueprintsList"] = f"({json.dumps(dict(children_per_blueprint))})"
# we copy custom properties over from our original object to our empty
for component_name, component_value in object.items():
if component_name not in custom_properties_to_filter_out and is_component_valid(object, component_name): #copy only valid properties
empty_obj[component_name] = component_value
copy = empty_obj
else:
# for objects which are NOT collection instances or when embeding
# we create a copy of our object and its children, to leave the original one as it is
original_name = object.name
object.name = original_name + "____bak"
copy = object.copy()
copy.name = original_name
destination_collection.objects.link(copy)
"""if object.parent == None:
if parent_empty is not None:
copy.parent = parent_empty
"""
# do this both for empty replacements & normal copies
if parent is not None:
copy.parent = parent
remove_unwanted_custom_properties(copy)
copy_animation_data(object, copy)
for child in object.children:
duplicate_object(child, copy, combine_mode, destination_collection, blueprints_data, nester+" ")
# copies the contents of a collection into another one while replacing library instances with empties
def copy_hollowed_collection_into(source_collection, destination_collection, parent_empty=None, filter=None, blueprints_data=None, addon_prefs={}):
collection_instances_combine_mode = getattr(addon_prefs, "collection_instances_combine_mode")
for object in source_collection.objects:
if object.name.endswith("____bak"): # some objects could already have been handled, ignore them
continue
if filter is not None and filter(object) is False:
continue
#check if a specific collection instance does not have an ovveride for combine_mode
combine_mode = object['_combine'] if '_combine' in object else collection_instances_combine_mode
parent = parent_empty
duplicate_object(object, parent, combine_mode, destination_collection, blueprints_data)
# for every child-collection of the source, copy its content into a new sub-collection of the destination
for collection in source_collection.children:
original_name = collection.name
collection.name = original_name + "____bak"
collection_placeholder = make_empty(original_name, [0,0,0], [0,0,0], [1,1,1], destination_collection)
if parent_empty is not None:
collection_placeholder.parent = parent_empty
copy_hollowed_collection_into(
source_collection = collection,
destination_collection = destination_collection,
parent_empty = collection_placeholder,
filter = filter,
blueprints_data = blueprints_data,
addon_prefs=addon_prefs
)
return {}
# clear & remove "hollow scene"
def clear_hollow_scene(temp_scene, original_root_collection):
def restore_original_names(collection):
if collection.name.endswith("____bak"):
collection.name = collection.name.replace("____bak", "")
for object in collection.objects:
if object.instance_type == 'COLLECTION':
if object.name.endswith("____bak"):
object.name = object.name.replace("____bak", "")
else:
if object.name.endswith("____bak"):
object.name = object.name.replace("____bak", "")
for child_collection in collection.children:
restore_original_names(child_collection)
# remove any data we created
temp_root_collection = temp_scene.collection
temp_scene_objects = [o for o in temp_root_collection.all_objects]
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):
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))
level_scenes = list(map(lambda name: bpy.data.scenes[name], level_scene_names))
library_scenes = list(map(lambda name: bpy.data.scenes[name], library_scene_names))
return [level_scene_names, level_scenes, library_scene_names, library_scenes]

View File

@ -0,0 +1,47 @@
import bmesh
import bpy
import mathutils
# Makes an empty, at the specified location, rotation, scale 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, rotation, scale, collection):
object_data = None
empty_obj = bpy.data.objects.new( name, object_data )
empty_obj.empty_display_size = 2
empty_obj.empty_display_type = 'PLAIN_AXES'
empty_obj.name = name
empty_obj.location = location
empty_obj.scale = scale
empty_obj.rotation_euler = rotation
collection.objects.link( empty_obj )
#bpy.context.view_layer.update()
return empty_obj
def make_cube(name, location=[0,0,0], rotation=[0,0,0], scale=[1,1,1], collection=None):
new_mesh = bpy.data.meshes.new(name+"_Mesh") #None
"""verts = [( 1.0, 1.0, 0.0),
( 1.0, -1.0, 0.0),
(-1.0, -1.0, 0.0),
(-1.0, 1.0, 0.0),
] # 4 verts made with XYZ coords
edges = []
faces = [[0, 1, 2, 3]]
new_mesh.from_pydata(verts, edges, faces)"""
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=0.1, matrix=mathutils.Matrix.Translation(location)) # FIXME: other ways to set position seems to fail ?
bm.to_mesh(new_mesh)
bm.free()
new_object = bpy.data.objects.new(name, new_mesh)
new_object.name = name
new_object.location = location
new_object.scale = scale
new_object.rotation_euler = rotation
if collection != None:
collection.objects.link( new_object )
return new_object

View File

@ -0,0 +1,10 @@
import bpy
import rna_prop_ui
# fake way to make our operator's changes be visible to the change/depsgraph update handler in gltf_auto_export
def ping_depsgraph_update(object=None):
if object == None:
object = bpy.data.scenes[0]
rna_prop_ui.rna_idprop_ui_create(object, "________temp", default=0)
rna_prop_ui.rna_idprop_ui_prop_clear(object, "________temp")
return None

View File

@ -0,0 +1,233 @@
import json
from mathutils import Color
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',
'library', 'library_weak_reference', 'make_local','name', 'name_full', 'original',
'override_create', 'override_hierarchy_create', 'override_library', 'preview', 'preview_ensure', 'rna_type',
'session_uid', 'copy', 'id_type', 'is_embedded_data', 'is_evaluated', 'is_library_indirect', 'is_missing', 'is_runtime_data']
# possible alternatives https://blender.stackexchange.com/questions/286010/bpy-detect-modified-mesh-data-vertices-edges-loops-or-polygons-for-cachin
def mesh_hash(obj):
# this is incomplete, how about edges ?
vertex_count = len(obj.data.vertices)
vertices_np = np.empty(vertex_count * 3, dtype=np.float32)
obj.data.vertices.foreach_get("co", vertices_np)
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
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:
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
# Remove duplicate actions.
blender_actions = list(set(blender_actions))
# sort animations alphabetically (case insensitive) so they have a defined order and match Blender's Action list
blender_actions.sort(key = lambda a: a.name.lower())
markers_per_animation = {}
animations_infos = []
for action in blender_actions:
animation_name = blender_tracks[action.name]
animations_infos.append(
f'(name: "{animation_name}", frame_start: {action.frame_range[0]}, frame_end: {action.frame_range[1]}, frames_length: {action.frame_range[1] - action.frame_range[0]}, frame_start_override: {action.frame_start}, frame_end_override: {action.frame_end})'
)
markers_per_animation[animation_name] = {}
for marker in action.pose_markers:
if marker.frame not in markers_per_animation[animation_name]:
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"]
camera_data = obj.data
fields_to_ignore= fields_to_ignore_generic
all_field_names = dir(camera_data)
fields = [getattr(camera_data, prop, None) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
# TODO: the above is not enough, certain fields are left as bpy.data.xx
#print("camera", obj, fields)
return str(fields)
def light_hash(obj):
light_data = obj.data
fields_to_ignore = fields_to_ignore_generic
all_field_names = dir(light_data)
fields = [getattr(light_data, prop, None) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
return str(fields)
def bones_hash(bones):
fields_to_ignore = fields_to_ignore_generic + ['AxisRollFromMatrix', 'MatrixFromAxisRoll', 'evaluate_envelope', 'convert_local_to_pose', 'foreach_get', 'foreach_set', 'get', 'set', 'find', 'items', 'keys', 'values']
bones_result = []
for bone in 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)
return str(hash(str(bones_result)))
# fixme: not good enough ?
def armature_hash(obj):
fields_to_ignore = fields_to_ignore_generic + ['display_type', 'is_editmode', 'pose_position', 'foreach_get', 'get']
fields_to_convert = {'bones': bones_hash}#, 'collections_all': bones_hash}
armature_data = obj.data
all_field_names = dir(armature_data)
fields = [getattr(armature_data, prop, None) if not prop in fields_to_convert.keys() else fields_to_convert[prop](getattr(armature_data, prop)) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
#print("ARMATURE", fields)
"""for bone in armature_data.bones:
print("bone", bone, bone_hash(bone))"""
return str(fields)
def field_value(data):
pass
def color(color_data):
# print("color", color_data, type(color_data))
return str(peel_value(color_data))
def lineart(lineart_data):
fields_to_ignore = fields_to_ignore_generic
all_field_names = dir(lineart_data)
fields = [getattr(lineart_data, prop, None) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
return str(fields)
def node_tree(nodetree_data):
fields_to_ignore = fields_to_ignore_generic+ ['contains_tree','get_output_node', 'interface_update', 'override_template_create']
all_field_names = dir(nodetree_data)
fields = [getattr(nodetree_data, prop, None) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
# print("node tree", fields)
return str(fields)
def peel_value( value ):
try:
len( value )
return [ peel_value( x ) for x in value ]
except TypeError:
return value
def material_hash(material):
fields_to_ignore = fields_to_ignore_generic
fields_to_convert = {'diffuse_color': color, 'line_color': color, 'lineart': lineart, 'node_tree': node_tree} # TODO: perhaps use types rather than names
all_field_names = dir(material)
fields = [getattr(material, prop, None) if not prop in fields_to_convert.keys() else fields_to_convert[prop](getattr(material, prop)) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
type_of = [type(getattr(material, prop, None)) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
names = [prop for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]
tutu = [t == Color for t in type_of] # bpy.types.MaterialLineArt bpy.types.ShaderNodeTree
#print("fields", type_of)
"""for prop in [prop for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")]:
bla = getattr(material, prop, None)
if hasattr(bla, "rna_type"):
print("YOLO", prop, bla, peel_value(bla), "type", type(bla), bla.rna_type, bla.rna_type == bpy.types.FloatProperty, type(bla) == bpy.types.bpy_prop_collection)
print("types", type(bla) == bpy.types.bpy_prop_collection, type(bla) == bpy.types.FloatColorAttributeValue)"""
# print("oooooh", material, material.bl_rna.properties.items())
return str(fields)#str(hash(str(fields)))
# TODO: this is partially taken from export_materials utilities, perhaps we could avoid having to fetch things multiple times ?
def materials_hash(obj, cache):
# print("materials")
materials = []
for material_slot in obj.material_slots:
material = material_slot.material
cached_hash = cache['materials'].get(material.name, None)
if cached_hash:
# print("CACHHHHHED", cached_hash)
materials.append(cached_hash)
else:
mat = material_hash(material)
cache['materials'][material.name] = mat
materials.append(mat)
# print("NOT CACHHH", mat)
# materials = [material_hash(material_slot.material) if not material_slot.material.name in cache["materials"] else cache["materials"][material_slot.material.name] for material_slot in obj.material_slots]
return str(hash(str(materials)))
def custom_properties_hash(obj):
custom_properties = {}
for property_name in obj.keys():
if property_name not in '_RNA_UI' and property_name != 'components_meta':
custom_properties[property_name] = obj[property_name]
return str(hash(str(custom_properties)))
def serialize_scene():
cache = {"materials":{}}
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]
#loc, rot, scale = bpy.context.object.matrix_world.decompose()
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()
custom_properties = custom_properties_hash(object) if len(object.keys()) > 0 else None
animations = animation_hash(object)
mesh = mesh_hash(object) if object.type == 'MESH' else None
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]
materials = materials_hash(object, cache) if len(object.material_slots) > 0 else None
data[scene.name][object.name] = {
"name": object.name,
"transforms": transform,
"visibility": visibility,
"custom_properties": custom_properties,
"animations": animations,
"mesh": mesh,
"camera": camera,
"light": light,
"armature": armature,
"parent": parent,
"collections": collections,
"materials": materials
}
"""print("data", data)
print("")
print("")
print("data json", json.dumps(data))"""
return json.dumps(data)

View File

@ -0,0 +1,402 @@
bl_info = {
"name": "gltf_auto_export",
"author": "kaosigh",
"version": (0, 10, 0),
"blender": (3, 4, 0),
"location": "File > Import-Export",
"description": "glTF/glb auto-export",
"warning": "",
"wiki_url": "https://github.com/kaosat-dev/Blender_bevy_components_workflow",
"tracker_url": "https://github.com/kaosat-dev/Blender_bevy_components_workflow/issues/new",
"category": "Import-Export"
}
import bpy
from bpy.props import (BoolProperty,
IntProperty,
StringProperty,
EnumProperty,
CollectionProperty
)
# glTF extensions are named following a convention with known prefixes.
# See: https://github.com/KhronosGroup/glTF/tree/main/extensions#about-gltf-extensions
# also: https://github.com/KhronosGroup/glTF/blob/main/extensions/Prefixes.md
glTF_extension_name = "EXT_auto_export"
# Support for an extension is "required" if a typical glTF viewer cannot be expected
# to load a given model without understanding the contents of the extension.
# For example, a compression scheme or new image format (with no fallback included)
# would be "required", but physics metadata or app-specific settings could be optional.
extension_is_required = False
from io_scene_gltf2 import (GLTF_PT_export_main, GLTF_PT_export_include)
class ExampleExtensionProperties(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(
name=bl_info["name"],
description='Include this extension in the exported glTF file.',
default=True
)
auto_export_main_scene_name: StringProperty(
name='Main scene',
description='The name of the main scene/level/world to auto export',
default='Scene'
)
auto_export_output_folder: StringProperty(
name='Export folder (relative)',
description='The root folder for all exports(relative to current file) Defaults to current folder',
default=''
)
auto_export_library_scene_name: StringProperty(
name='Library scene',
description='The name of the library scene to auto export',
default='Library'
)
# scene components
auto_export_scene_settings: BoolProperty(
name='Export scene settings',
description='Export scene settings ie AmbientLighting, Bloom, AO etc',
default=False
)
# blueprint settings
auto_export_blueprints: BoolProperty(
name='Export Blueprints',
description='Replaces collection instances with an Empty with a BlueprintName custom property',
default=True
)
auto_export_blueprints_path: StringProperty(
name='Blueprints path',
description='path to export the blueprints to (relative to the Export folder)',
default='library'
)
auto_export_materials_library: BoolProperty(
name='Export materials library',
description='remove materials from blueprints and use the material library instead',
default=False
)
auto_export_materials_path: StringProperty(
name='Materials path',
description='path to export the materials libraries to (relative to the root folder)',
default='materials'
)
def register():
bpy.utils.register_class(ExampleExtensionProperties)
bpy.types.Scene.ExampleExtensionProperties = bpy.props.PointerProperty(type=ExampleExtensionProperties)
def register_panel():
# Register the panel on demand, we need to be sure to only register it once
# This is necessary because the panel is a child of the extensions panel,
# which may not be registered when we try to register this extension
try:
bpy.utils.register_class(GLTF_PT_UserExtensionPanel)
except Exception:
pass
# If the glTF exporter is disabled, we need to unregister the extension panel
# Just return a function to the exporter so it can unregister the panel
return unregister_panel
def unregister_panel():
# Since panel is registered on demand, it is possible it is not registered
try:
bpy.utils.unregister_class(GLTF_PT_UserExtensionPanel)
except Exception:
pass
def unregister():
unregister_panel()
bpy.utils.unregister_class(ExampleExtensionProperties)
del bpy.types.Scene.ExampleExtensionProperties
class GLTF_PT_UserExtensionPanel(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Enabled"
bl_parent_id = "GLTF_PT_export_user_extensions"
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):
props = bpy.context.scene.ExampleExtensionProperties
self.layout.prop(props, 'enabled')
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
props = bpy.context.scene.ExampleExtensionProperties
layout.active = props.enabled
props = bpy.context.scene.ExampleExtensionProperties
for bla in props.__annotations__:
layout.prop(props, bla)
class glTF2ExportUserExtension:
def __init__(self):
# We need to wait until we create the gltf2UserExtension to import the gltf2 modules
# Otherwise, it may fail because the gltf2 may not be loaded yet
from io_scene_gltf2.io.com.gltf2_io_extensions import Extension
self.Extension = Extension
self.properties = bpy.context.scene.ExampleExtensionProperties
def gather_node_hook(self, gltf2_object, blender_object, export_settings):
if self.properties.enabled:
if gltf2_object.extensions is None:
gltf2_object.extensions = {}
print("bla bla")
gltf2_object.extensions[glTF_extension_name] = self.Extension(
name=glTF_extension_name,
extension={"auto_export_blueprints": self.properties.auto_export_blueprints},
required=extension_is_required
)
def did_export_parameters_change(current_params, previous_params):
set1 = set(previous_params.items())
set2 = set(current_params.items())
difference = dict(set1 ^ set2)
changed_param_names = list(set(difference.keys())- set(AutoExportGltfPreferenceNames))
changed_parameters = len(changed_param_names) > 0
return changed_parameters
# original in export_blueprints => export_collections
# The part below is not necessary NORMALLY , but blender crashes in the "normal" case when using bpy.context.temp_override,
#if relevant we replace sub collections instances with placeholders too
# this is not needed if a collection/blueprint does not have sub blueprints or sub collections
collection_in_blueprint_hierarchy = collection_name in blueprint_hierarchy and len(blueprint_hierarchy[collection_name]) > 0
collection_has_child_collections = len(bpy.data.collections[collection_name].children) > 0
#if collection_in_blueprint_hierarchy or collection_has_child_collections:
"""else:
print("standard export")
# set active scene to be the library scene
original_scene = bpy.context.window.scene
bpy.context.window.scene = library_scene
with bpy.context.temp_override(scene=library_scene):
print("active scene", bpy.context.scene)
export_gltf(gltf_output_path, export_settings)
bpy.context.window.scene = original_scene"""
"""
blueprint_template = object['Template'] if 'Template' in object else False
if blueprint_template and parent_empty is None: # ONLY WORKS AT ROOT LEVEL
print("BLUEPRINT TEMPLATE", blueprint_template, destination_collection, parent_empty)
for object in source_collection.objects:
if object.type == 'EMPTY' and object.name.endswith("components"):
original_collection = bpy.data.collections[collection_name]
components_holder = object
print("WE CAN INJECT into", object, "data from", original_collection)
# now we look for components inside the collection
components = {}
for object in original_collection.objects:
if object.type == 'EMPTY' and object.name.endswith("components"):
for component_name in object.keys():
if component_name not in '_RNA_UI':
print( component_name , "-" , object[component_name] )
components[component_name] = object[component_name]
# copy template components into target object
for key in components:
print("copying ", key,"to", components_holder)
if not key in components_holder:
components_holder[key] = components[key]
"""
# potentially useful alternative
def duplicate_object2(object, original_name):
print("copy object", object)
with bpy.context.temp_override(object=object, active_object = object):
bpy.ops.object.duplicate(linked=False)
new_obj = bpy.context.active_object
print("new obj", new_obj, "bpy.context.view_layer", bpy.context.view_layer.objects)
for obj in bpy.context.view_layer.objects:
print("obj", obj)
bpy.context.view_layer.update()
new_obj.name = original_name
if object.animation_data:
print("OJECT ANIMATION")
new_obj.animation_data.action = object.animation_data.action.copy()
return new_obj
if active_operator:
# print("Operator", active_operator.bl_label, active_operator.bl_idname, "bla", bpy.context.window_manager.gltf_exporter_running)
if active_operator.bl_idname == "EXPORT_SCENE_OT_gltf" : #and not bpy.context.window_manager.gltf_exporter_running:
# we force saving params
active_operator.will_save_settings = True
if active_operator.bl_idname == "EXPORT_SCENES_OT_auto_gltf":
# we force saving params
active_operator.will_save_settings = True
"""
print("matching")
try:
bpy.app.timers.unregister(cls.gltf_exporter_handler)
except:pass
bpy.app.timers.register(cls.gltf_exporter_handler, first_interval=3)
# we backup any existing gltf export settings, if there where any
scene = bpy.context.scene
if "glTF2ExportSettings" in scene:
existing_setting = scene["glTF2ExportSettings"]
cls.existing_gltf_settings = existing_setting
bpy.context.window_manager.gltf_exporter_running = True
else:
if bpy.context.window_manager.gltf_exporter_running:
bpy.context.window_manager.gltf_exporter_running = False"""
"""@classmethod
def gltf_exporter_handler(cls):
# FOr some reason, the active operator here is always None, so using a workaround
# active_operator = bpy.context.active_operator
print("here", bpy.context.window_manager.gltf_exporter_running)
if bpy.context.window_manager.gltf_exporter_running:
try:
dummy_file_path = "/home/ckaos/projects/bevy/Blender_bevy_components_worklflow/testing/bevy_example/assets/dummy.glb"
import os
if os.path.exists(dummy_file_path):
print("dummy file exists, assuming it worked")
os.unlink(dummy_file_path)
# get the parameters
scene = bpy.context.scene
if "glTF2ExportSettings" in scene:
settings = scene["glTF2ExportSettings"]
formatted_settings = dict(settings)
export_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")
#check if params have changed
bpy.context.window_manager.gltf_settings_changed = sorted(json.loads(export_settings.as_string()).items()) != sorted(formatted_settings.items())
print("gltf NEW settings", formatted_settings, "OLD settings", export_settings, "CHANGED ?", bpy.context.window_manager.gltf_settings_changed)
# now write new settings
export_settings.clear()
export_settings.write(json.dumps(formatted_settings))
# now reset the original gltf_settings
if getattr(cls, "existing_gltf_settings", None) != None:
print("resetting original gltf settings")
scene["glTF2ExportSettings"] = cls.existing_gltf_settings
else:
print("no pre_existing settings")
if "glTF2ExportSettings" in scene:
del scene["glTF2ExportSettings"]
cls.existing_gltf_settings = None
except:pass
bpy.context.window_manager.gltf_exporter_running = False
return None
else:
try:
bpy.app.timers.unregister(cls.gltf_exporter_handler)
except:pass
return None
return 1"""
def invoke_override(self, context, event):
settings = context.scene.get(self.scene_key)
self.will_save_settings = False
if settings:
try:
for (k, v) in settings.items():
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]
import sys
preferences = bpy.context.preferences
for addon_name in preferences.addons.keys():
try:
if hasattr(sys.modules[addon_name], 'glTF2ExportUserExtension') or hasattr(sys.modules[addon_name], 'glTF2ExportUserExtensions'):
pass #exporter_extension_panel_unregister_functors.append(sys.modules[addon_name].register_panel())
except Exception:
pass
# self.has_active_exporter_extensions = len(exporter_extension_panel_unregister_functors) > 0
print("ovverride")
wm = context.window_manager
wm.fileselect_add(self)
return {'RUNNING_MODAL'}
from io_scene_gltf2 import (ExportGLTF2, GLTF_PT_export_main, GLTF_PT_export_include)
from io_scene_gltf2 import (ExportGLTF2, GLTF_PT_export_main,ExportGLTF2_Base, GLTF_PT_export_include)
import io_scene_gltf2 as gltf_exporter_original
#import io_scene_gltf2.GLTF_PT_export_data_scene as GLTF_PT_export_data_scene_original
"""
class GLTF_PT_export_data(gltf_exporter_original.GLTF_PT_export_data):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Data"
bl_parent_id = "GLTF_PT_auto_export_gltf"
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"
class GLTF_PT_export_data_scene(gltf_exporter_original.GLTF_PT_export_data_scene):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Scene Graph"
bl_parent_id = "GLTF_PT_export_data"
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"
def draw(self, context):
return super().draw(context)"""

View File

@ -0,0 +1,29 @@
import bpy
# checks if an object is dynamic
# TODO: for efficiency, it might make sense to write this flag semi automatically at the root level of the object so we can skip the inner loop
# TODO: we need to recompute these on blueprint changes too
# even better, keep a list of dynamic objects per scene , updated only when needed ?
def is_object_dynamic(object):
is_dynamic = object['Dynamic'] if 'Dynamic' in object else False
# only look for data in the original collection if it is not alread marked as dynamic at instance level
if not is_dynamic and object.type == 'EMPTY' and hasattr(object, 'instance_collection') and object.instance_collection != None :
#print("collection", object.instance_collection, "object", object.name)
# get the name of the collection this is an instance of
collection_name = object.instance_collection.name
original_collection = bpy.data.collections[collection_name]
# scan original collection, look for a 'Dynamic' flag
for object in original_collection.objects:
#print(" inner", object)
if object.type == 'EMPTY' and object.name.endswith("components"):
for component_name in object.keys():
#print(" compo", component_name)
if component_name == 'Dynamic':
is_dynamic = True
break
return is_dynamic
def is_object_static(object):
return not is_object_dynamic(object)

View File

@ -0,0 +1,64 @@
import bpy
from ..helpers.object_makers import make_empty
# TODO: replace this with placing scene level custom properties once support for that has been added to bevy_gltf
def upsert_scene_components(main_scenes):
for scene in main_scenes:
lighting_components_name = f"lighting_components_{scene.name}"
lighting_components = bpy.data.objects.get(lighting_components_name, None)
if not lighting_components:
root_collection = scene.collection
lighting_components = make_empty('lighting_components_'+scene.name, [0,0,0], [0,0,0], [0,0,0], root_collection)
if scene.world is not None:
lighting_components['BlenderBackgroundShader'] = ambient_color_to_component(scene.world)
lighting_components['BlenderShadowSettings'] = scene_shadows_to_component(scene)
if scene.eevee.use_bloom:
lighting_components['BloomSettings'] = scene_bloom_to_component(scene)
elif 'BloomSettings' in lighting_components:
del lighting_components['BloomSettings']
if scene.eevee.use_gtao:
lighting_components['SSAOSettings'] = scene_ao_to_component(scene)
elif 'SSAOSettings' in lighting_components:
del lighting_components['SSAOSettings']
def remove_scene_components(main_scenes):
for scene in main_scenes:
lighting_components_name = f"lighting_components_{scene.name}"
lighting_components = bpy.data.objects.get(lighting_components_name, None)
if lighting_components:
bpy.data.objects.remove(lighting_components, do_unlink=True)
def ambient_color_to_component(world):
color = None
strength = None
try:
color = world.node_tree.nodes['Background'].inputs[0].default_value
strength = world.node_tree.nodes['Background'].inputs[1].default_value
except Exception as ex:
print("failed to parse ambient color: Only background is supported")
if color is not None and strength is not None:
colorRgba = f"Rgba(red: {color[0]}, green: {color[1]}, blue: {color[2]}, alpha: {color[3]})"
component = f"( color: {colorRgba}, strength: {strength})"
return component
return None
def scene_shadows_to_component(scene):
cascade_size = scene.eevee.shadow_cascade_size
component = f"(cascade_size: {cascade_size})"
return component
def scene_bloom_to_component(scene):
component = f"BloomSettings(intensity: {scene.eevee.bloom_intensity})"
return component
def scene_ao_to_component(scene):
ssao = scene.eevee.use_gtao
component= "SSAOSettings()"
return component

View File

@ -0,0 +1,127 @@
import os
import bpy
from pathlib import Path
from ..helpers.generate_and_export import generate_and_export
from ..helpers.helpers_collections import (traverse_tree)
from ..auto_export.export_gltf import (export_gltf, generate_gltf_export_preferences)
from ..helpers.object_makers import make_cube
# get materials per object, and injects the materialInfo component
def get_materials(object):
material_slots = object.material_slots
used_materials_names = []
#materials_per_object = {}
current_project_name = Path(bpy.context.blend_data.filepath).stem
for m in material_slots:
material = m.material
# print(" slot", m, "material", material)
used_materials_names.append(material.name)
# TODO:, also respect slots & export multiple materials if applicable !
object['MaterialInfo'] = '(name: "'+material.name+'", source: "'+current_project_name + '")'
return used_materials_names
def clear_material_info(collection_names, library_scenes):
for scene in library_scenes:
root_collection = scene.collection
for cur_collection in traverse_tree(root_collection):
if cur_collection.name in collection_names:
for object in cur_collection.all_objects:
if 'MaterialInfo' in dict(object): # FIXME: hasattr does not work ????
del object["MaterialInfo"]
def get_all_materials(collection_names, library_scenes):
#print("collecton", layerColl, "otot", layerColl.all_objects) #all_objects
used_material_names = []
for scene in library_scenes:
root_collection = scene.collection
for cur_collection in traverse_tree(root_collection):
if cur_collection.name in collection_names:
for object in cur_collection.all_objects:
used_material_names = used_material_names + get_materials(object)
# we only want unique names
used_material_names = list(set(used_material_names))
return used_material_names
# creates a new object with the applied material, for the material library
def make_material_object(name, location=[0,0,0], rotation=[0,0,0], scale=[1,1,1], material=None, collection=None):
#original_active_object = bpy.context.active_object
#bpy.ops.mesh.primitive_cube_add(size=0.1, location=location)
object = make_cube(name, location=location, rotation=rotation, scale=scale, collection=collection)
if material:
if object.data.materials:
# assign to 1st material slot
object.data.materials[0] = material
else:
# no slots
object.data.materials.append(material)
return object
# generates a materials scene:
def generate_materials_scene_content(root_collection, used_material_names):
for index, material_name in enumerate(used_material_names):
material = bpy.data.materials[material_name]
make_material_object("Material_"+material_name, [index * 0.2,0,0], material=material, collection=root_collection)
return {}
def clear_materials_scene(temp_scene):
root_collection = temp_scene.collection
scene_objects = [o for o in root_collection.objects]
for object in scene_objects:
#print("removing ", object)
try:
mesh = bpy.data.meshes[object.name+"_Mesh"]
bpy.data.meshes.remove(mesh, do_unlink=True)
except Exception as error:
pass
#print("could not remove mesh", error)
try:
bpy.data.objects.remove(object, do_unlink=True)
except:pass
bpy.data.scenes.remove(temp_scene)
# exports the materials used inside the current project:
# the name of the output path is <materials_folder>/<name_of_your_blend_file>_materials_library.gltf/glb
def export_materials(collections, library_scenes, folder_path, addon_prefs):
gltf_export_preferences = generate_gltf_export_preferences(addon_prefs)
export_materials_path = getattr(addon_prefs,"export_materials_path")
export_root_folder = getattr(addon_prefs, "export_root_folder")
used_material_names = get_all_materials(collections, library_scenes)
current_project_name = Path(bpy.context.blend_data.filepath).stem
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
}
gltf_output_path = os.path.join(export_root_folder, export_materials_path, current_project_name + "_materials_library")
print(" exporting Materials to", gltf_output_path, ".gltf/glb")
generate_and_export(
addon_prefs,
temp_scene_name="__materials_scene",
export_settings=export_settings,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: generate_materials_scene_content(temp_collection, used_material_names),
tempScene_cleaner= lambda temp_scene, params: clear_materials_scene(temp_scene=temp_scene)
)
def cleanup_materials(collections, library_scenes):
# remove temporary components
clear_material_info(collections, library_scenes)

View File

@ -0,0 +1,323 @@
from typing import Set
import bpy
######################################################
## ui logic & co
# side panel that opens auto_export specific gltf settings & the auto export settings themselves
class GLTF_PT_auto_export_SidePanel(bpy.types.Panel):
bl_idname = "GLTF_PT_auto_export_SidePanel"
bl_label = "Auto export"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Auto Export"
bl_context = "objectmode"
bl_parent_id = "BLENVY_PT_SidePanel"
@classmethod
def poll(cls, context):
return context.window_manager.blenvy.mode == 'SETTINGS'
"""def draw_header(self, context):
layout = self.layout
layout.label(text="Auto export ")"""
def draw(self, context):
layout = self.layout
layout.label(text="MAKE SURE TO KEEP 'REMEMBER EXPORT SETTINGS' TOGGLED !!")
op = layout.operator("EXPORT_SCENE_OT_gltf", text='Gltf Settings')#'glTF 2.0 (.glb/.gltf)')
#op.export_format = 'GLTF_SEPARATE'
op.use_selection=True
op.will_save_settings=True
op.use_visible=True # Export visible and hidden objects. See Object/Batch Export to skip.
op.use_renderable=True
op.use_active_collection = True
op.use_active_collection_with_nested=True
op.use_active_scene = True
op.filepath="____dummy____"
op.gltf_export_id = "gltf_auto_export" # we specify that we are in a special case
op = layout.operator("EXPORT_SCENES_OT_auto_gltf", text="Auto Export Settings")
op.auto_export = True
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}')
# main ui in the file => export
class GLTF_PT_auto_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_SCENES_OT_auto_gltf"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
class GLTF_PT_auto_export_root(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Auto export"
bl_parent_id = "GLTF_PT_auto_export_main"
#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"
def draw_header(self, context):
sfile = context.space_data
operator = sfile.active_operator
self.layout.prop(operator, "auto_export", 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.auto_export
layout.prop(operator, 'will_save_settings')
class GLTF_PT_auto_export_general(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "General"
bl_parent_id = "GLTF_PT_auto_export_root"
@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
layout.prop(operator, "export_output_folder")
layout.prop(operator, "export_scene_settings")
class GLTF_PT_auto_export_change_detection(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Change detection"
bl_parent_id = "GLTF_PT_auto_export_root"
@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_header(self, context):
layout = self.layout
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, "export_change_detection", 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.auto_export
layout.prop(operator, "export_change_detection")
class GLTF_PT_auto_export_scenes(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Scenes"
bl_parent_id = "GLTF_PT_auto_export_root"
@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_header(self, context):
layout = self.layout
sfile = context.space_data
operator = sfile.active_operator
#layout.label(text="export scenes")#layout.prop(operator, "export_blueprints", 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
# scene selectors
row = layout.row()
col = row.column(align=True)
col.separator()
layout.active = operator.auto_export
source = operator
rows = 2
# main/level scenes
row = layout.row()
row.label(text="main scenes")
row.prop(context.window_manager, "main_scene", text='')
row = layout.row()
row.template_list("SCENE_UL_GLTF_auto_export", "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.window_manager.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
row = layout.row()
row.label(text="library scenes")
row.prop(context.window_manager, "library_scene", text='')
row = layout.row()
row.template_list("SCENE_UL_GLTF_auto_export", "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.window_manager.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'
bl_label = "Blueprints"
bl_parent_id = "GLTF_PT_auto_export_root"
@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_header(self, context):
layout = self.layout
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, "export_blueprints", 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.auto_export and operator.export_blueprints
# collections/blueprints
layout.prop(operator, "export_blueprints_path")
layout.prop(operator, "collection_instances_combine_mode")
layout.prop(operator, "export_marked_assets")
layout.prop(operator, "export_separate_dynamic_and_static_objects")
layout.separator()
# materials
layout.prop(operator, "export_materials_library")
layout.prop(operator, "export_materials_path")
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.
# 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)

View File

@ -0,0 +1,175 @@
import bpy
from bpy.types import Operator
class ASSETS_LIST_OT_actions(Operator):
"""Add / remove etc assets"""
bl_idname = "asset_list.list_action"
bl_label = "Asset Actions"
bl_description = "Move items up and down, add and remove"
bl_options = {'REGISTER'}
class SCENES_LIST_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", ""))) # type: ignore
scene_type: bpy.props.StringProperty()#TODO: replace with enum
def invoke(self, context, event):
source = context.active_operator
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)
current_index = getattr(source, target_index)
try:
item = target[idx]
except IndexError:
pass
else:
if self.action == 'DOWN' and idx < len(target) - 1:
target.move(idx, idx + 1)
setattr(source, target_index, current_index +1 )
info = 'Item "%s" moved to position %d' % (item.name, current_index + 1)
self.report({'INFO'}, info)
elif self.action == 'UP' and idx >= 1:
target.move(idx, idx - 1)
setattr(source, target_index, current_index -1 )
info = 'Item "%s" moved to position %d' % (item.name, current_index + 1)
self.report({'INFO'}, info)
elif self.action == 'REMOVE':
info = 'Item "%s" removed from list' % (target[idx].name)
setattr(source, target_index, current_index -1 )
target.remove(idx)
self.report({'INFO'}, info)
if self.action == 'ADD':
new_scene_name = None
if self.scene_type == "level":
if context.window_manager.main_scene:
new_scene_name = context.window_manager.main_scene.name
else:
if context.window_manager.library_scene:
new_scene_name = context.window_manager.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.window_manager.main_scene = None
else:
context.window_manager.library_scene = None
#name = f"Rule {idx +1}"
#target.append({"name": name})
setattr(source, target_index, len(target) - 1)
#source[target_index] = len(target) - 1
info = '"%s" added to list' % (item.name)
self.report({'INFO'}, info)
return {"FINISHED"}
import os
from bpy_extras.io_utils import ImportHelper
class OT_OpenFolderbrowser(Operator, ImportHelper):
"""Browse for registry json file"""
bl_idname = "generic.open_folderbrowser"
bl_label = "Select folder"
# Define this to tell 'fileselect_add' that we want a directoy
directory: bpy.props.StringProperty(
name="Outdir Path",
description="selected folder"
# subtype='DIR_PATH' is not needed to specify the selection mode.
# But this will be anyway a directory path.
) # type: ignore
# Filters folders
filter_folder: bpy.props.BoolProperty(
default=True,
options={"HIDDEN"}
) # type: ignore
target_property: bpy.props.StringProperty(
name="target_property",
options={'HIDDEN'}
) # type: ignore
def execute(self, context):
"""Do something with the selected file(s)."""
operator = context.active_operator
new_path = self.directory
target_path_name = self.target_property
# path to the current blend file
blend_file_path = bpy.data.filepath
# Get the folder
blend_file_folder_path = os.path.dirname(blend_file_path)
print("blend_file_folder_path", blend_file_folder_path)
print("new_path", self.directory, self.target_property, operator)
path_names = ['export_output_folder', 'export_blueprints_path', 'export_levels_path', 'export_materials_path']
export_root_folder = operator.export_root_folder
#export_root_path_absolute = os.path.join(blend_file_folder_path, export_root_folder)
if target_path_name == 'export_root_folder':
print("changing root new_path")
# we need to change all other relative paths before setting the new absolute path
for path_name in path_names:
# get absolute path
relative_path = getattr(operator, path_name, None)
if relative_path is not None:
absolute_path = os.path.join(export_root_folder, relative_path)
print("absolute path for", path_name, absolute_path)
relative_path = os.path.relpath(absolute_path, new_path)
setattr(operator, path_name, relative_path)
# store the root path as relative to the current blend file
setattr(operator, target_path_name, new_path)
else:
relative_path = os.path.relpath(new_path, export_root_folder)
setattr(operator, target_path_name, relative_path)
#filename, extension = os.path.splitext(self.filepath)
return {'FINISHED'}
def draw_folder_browser(layout, label, value, target_property):
row = layout.row()
row.label(text=label)
'''box = row.box()
box.scale_y = 0.5
box.label(text=value)'''
col = row.column()
col.enabled = False
col.prop(bpy.context.active_operator, target_property, text="")
folder_selector = row.operator(OT_OpenFolderbrowser.bl_idname, icon="FILE_FOLDER", text="")
folder_selector.target_property = target_property #"export_root_folder"

13
tools/blenvy/settings.py Normal file
View File

@ -0,0 +1,13 @@
import json
import bpy
def upsert_settings(name, data):
stored_settings = bpy.data.texts[name] if name in bpy.data.texts else bpy.data.texts.new(name)
stored_settings.clear()
stored_settings.write(json.dumps(data))
def load_settings(name):
stored_settings = bpy.data.texts[name] if name in bpy.data.texts else None
if stored_settings != None:
return json.loads(stored_settings.as_string())
return None

View File

View File

@ -0,0 +1,217 @@
import random
import string
import uuid
from bpy_types import PropertyGroup
def random_bool():
return bool(random.getrandbits(1))
def rand_int():
return random.randint(0, 100)
def rand_float():
return random.random()
def random_word(length):
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(length))
def random_vec(length, type,):
value = []
for i in range(0, length):
if type == 'float':
value.append(rand_float())
if type == 'int':
value.append(rand_int())
return value
type_mappings = {
"bool": random_bool,
"u8": rand_int,
"u16": rand_int,
"u32": rand_int,
"u64": rand_int,
"u128": rand_int,
"u64": rand_int,
"usize": rand_int,
"i8": rand_int,
"i16": rand_int,
"i32": rand_int,
"i64": rand_int,
"i128": rand_int,
"isize": rand_int,
'f32': rand_float,
'f64': rand_float,
"glam::Vec2": lambda : random_vec(2, 'float'),
"glam::DVec2": lambda : random_vec(2, 'float'),
"glam::UVec2": lambda : random_vec(2, 'int'),
'glam::Vec3': lambda : random_vec(3, 'float'),
"glam::Vec3A": lambda : random_vec(3, 'float'),
"glam::UVec3": lambda : random_vec(3, 'int'),
"glam::Vec4": lambda : random_vec(4, 'float'),
"glam::DVec4": lambda : random_vec(4, 'float'),
"glam::UVec4": lambda : random_vec(4, 'int'),
"glam::Quat": lambda : random_vec(4, 'float'),
'bevy_render::color::Color': lambda : random_vec(4, 'float'),
'alloc::string::String': lambda : random_word(8),
'alloc::borrow::Cow<str>': lambda : random_word(8),
'bevy_ecs::entity::Entity': lambda: 0, #4294967295, #
'bevy_utils::Uuid': lambda: '"'+str( uuid.UUID("73b3b118-7d01-4778-8bcc-4e79055f5d22") )+'"'
}
#
def is_def_value_type(definition, registry):
if definition == None:
return True
value_types_defaults = registry.value_types_defaults
long_name = definition["long_name"]
is_value_type = long_name in value_types_defaults
return is_value_type
# see https://docs.python.org/3/library/random.html
def component_values_shuffler(seed=1, property_group=None, definition=None, registry=None, parent=None):
if parent == None:
random.seed(seed)
value_types_defaults = registry.value_types_defaults
component_name = definition["short_name"]
type_info = definition["typeInfo"] if "typeInfo" in definition else None
type_def = definition["type"] if "type" in definition else None
properties = definition["properties"] if "properties" in definition else {}
prefixItems = definition["prefixItems"] if "prefixItems" in definition else []
has_properties = len(properties.keys()) > 0
has_prefixItems = len(prefixItems) > 0
is_enum = type_info == "Enum"
is_list = type_info == "List"
long_name = definition["long_name"]
#is_value_type = type_def in value_types_defaults or long_name in value_types_defaults
is_value_type = long_name in value_types_defaults
if is_value_type:
fieldValue = type_mappings[long_name]()
return fieldValue
elif type_info == "Struct":
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["properties"][field_name]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = component_values_shuffler(seed, child_property_group, item_definition, registry, parent=component_name)
else:
value = '""'
is_item_value_type = is_def_value_type(item_definition, registry)
if is_item_value_type:
#print("setting attr", field_name , "for", component_name, "to", value, "value type", is_item_value_type)
setattr(property_group , field_name, value)
elif type_info == "Tuple":
#print("tup")
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = component_values_shuffler(seed, child_property_group, item_definition, registry, parent=component_name)
else:
value = '""'
is_item_value_type = is_def_value_type(item_definition, registry)
if is_item_value_type:
#print("setting attr", field_name , "for", component_name, "to", value, "value type", is_item_value_type)
setattr(property_group , field_name, value)
elif type_info == "TupleStruct":
#print("tupstruct")
for index, field_name in enumerate(property_group.field_names):
item_long_name = definition["prefixItems"][index]["type"]["$ref"].replace("#/$defs/", "")
item_definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
value = getattr(property_group, field_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
if item_definition != None:
value = component_values_shuffler(seed, child_property_group, item_definition, registry, parent=component_name)
else:
value = '""'
is_item_value_type = is_def_value_type(item_definition, registry)
if is_item_value_type:
setattr(property_group , field_name, value)
elif type_info == "Enum":
available_variants = definition["oneOf"] if type_def != "object" else list(map(lambda x: x["long_name"], definition["oneOf"]))
selected = random.choice(available_variants)
# set selected variant
setattr(property_group , "selection", selected)
if type_def == "object":
selection_index = property_group.field_names.index("variant_"+selected)
variant_name = property_group.field_names[selection_index]
variant_definition = definition["oneOf"][selection_index-1]
if "prefixItems" in variant_definition:
value = getattr(property_group, variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
value = component_values_shuffler(seed, child_property_group, variant_definition, registry, parent=component_name)
value = selected + str(value,)
elif "properties" in variant_definition:
value = getattr(property_group, variant_name)
is_property_group = isinstance(value, PropertyGroup)
child_property_group = value if is_property_group else None
value = component_values_shuffler(seed, child_property_group, variant_definition, registry, parent=component_name)
value = selected + str(value,)
else:
value = selected # here the value of the enum is just the name of the variant
else:
value = selected
elif type_info == "List":
item_list = getattr(property_group, "list")
item_list.clear()
item_long_name = getattr(property_group, "long_name")
number_of_list_items_to_add = random.randint(1, 2)
for i in range(0, number_of_list_items_to_add):
new_entry = item_list.add()
item_long_name = getattr(new_entry, "long_name") # we get the REAL type name
definition = registry.type_infos[item_long_name] if item_long_name in registry.type_infos else None
if definition != None:
component_values_shuffler(seed, new_entry, definition, registry, parent=component_name)
else:
pass
else:
print("something else")
fieldValue = type_mappings[long_name]() if long_name in type_mappings else 'None'
return fieldValue
#return value

Some files were not shown because too many files have changed in this diff Show More