diff --git a/tools/bevy_components/README.md b/tools/bevy_components/README.md index d871271a..623712ff 100644 --- a/tools/bevy_components/README.md +++ b/tools/bevy_components/README.md @@ -57,6 +57,14 @@ Before you can use the add-on you need to configure it ![configuration 3](./docs/configuration3.png) + #### registry file polling + + + * by default, the add-on will check for changes in your registry file every second, and refresh the UI accordingly + * you can set the polling frequency or turn it off if you do not want auto-refresh + + ![registry file polling](./docs/registry_polling.png) + ## Use @@ -173,8 +181,8 @@ It will add the component to the select object ![invalid component](./docs/invalid_components.png) - > important ! ```gltf_auto_export``` currently has no way of filtering out components, so you need to delete invalid components like these before exporting - this will be adress in the future + > see [here](#invalidunregistered-type-renaming--conversion) for ways to convert invalid / unregistered components to other types. + - if you are encountering this type of view: don't panic your component data is not gone ! It just means you need to reload the registry data by clicking on the relevant button @@ -182,18 +190,65 @@ It will add the component to the select object -## advanced configuration +## Advanced Tools - ### registry file polling +In this section you will find various additional more advanced tooling +### Invalid/unregistered type renaming / conversion - * by default, the add-on will check for changes in your registry file every second, and refresh the UI accordingly - * you can set the polling frequency or turn it off if you do not want auto-refresh +If you have components that are + * invalid : ie some error was diagnosed + * unregistered: a custom property is present on the object, but there is no matching type in the registry + +Here you will get an overview, of ALL invalid and unregistered components in your Blender project, so you can find them, rename/convert them, +or delete them, also in bulk + +![component rename overview](./docs/component_rename_overview2.png) + +* you can click on the button to select the object in your outliner (this also works across scenes, so you will be taken to the scene where the +given object is located) + +![update custom properties](./docs/component_rename_object_select.png) + + +#### Single object component renaming/ conversion + + - to rename/convert a single component for a single object: + + * go to the row of the object you want to convert the component of + * in the dropdown menu, choose the target component + * click on the button with the magic wand to convert the component + + ![single rename](./docs/component_rename_single.png) + + > the tool will attempt to automatically convert the source component, including the field names/values, if the target component has the same ones + If it fails to do the conversion, you will get an error message, and you will either have to change the custom property yourself, or you can simply + change the values in the UI, which will automatically generate the custom property value + + - to delete a single component for a single object: + + * go to the row of the object you want to remove the component from + * click on the button with the "x" to remove the component + + ![single delete](./docs/component_remove_single.png) + +#### Bulk component renaming/ conversion + + - use this method if you want to convert ALL components of a given type of ALL objects + + * click on this button to pick your source component + + ![bulk convert remove](./docs/component_rename_remove_bulk.png) + + * for conversion: in the dropdown menu, choose the target component & click apply to convert all matching components + * for deletion: clic on the "x" to remove all matching components + + ![bulk convert remove](./docs/component_rename_remove_bulk2.png) - ![registry file polling](./docs/registry_polling.png) + ### For conversion between custom properties & components & vice-versa - ### regenerate custom property values + #### regenerate custom property values - "update custom properties of current object" : will go over **all components** that you have defined for the **currently selected object**, and re-generate the @@ -212,7 +267,7 @@ It will add the component to the select object You should also re-export your gltf files , otherwise you might run into issues - ### regenerate UI values + #### regenerate component/ UI values - since v0.2, you have the option to regenerate (for the selected object or all objects, as above) to regenerate your UI values from the custom property values diff --git a/tools/bevy_components/__init__.py b/tools/bevy_components/__init__.py index 8f41247a..48effb9f 100644 --- a/tools/bevy_components/__init__.py +++ b/tools/bevy_components/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "bevy_components", "author": "kaosigh", - "version": (0, 4, 0), + "version": (0, 4, 1), "blender": (3, 4, 0), "location": "VIEW_3D", "description": "UI to help create Bevy blueprints and components", @@ -16,11 +16,11 @@ from .helpers import load_settings from .blueprints import CreateBlueprintOperator -from .components.operators import CopyComponentOperator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, AddComponentOperator, Toggle_ComponentVisibility +from .components.operators import CopyComponentOperator, Fix_Component_Operator, OT_rename_component, RemoveComponentFromAllObjectsOperator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, AddComponentOperator, RenameHelper, Toggle_ComponentVisibility from .registry.registry import ComponentsRegistry,MissingBevyType -from .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, ReloadRegistryOperator, OT_OpenFilebrowser) -from .registry.ui import (BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_MissingTypesPanel, MISSING_TYPES_UL_List) +from .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 .registry.ui import (BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_AdvancedToolsPanel, BEVY_COMPONENTS_PT_MissingTypesPanel, MISSING_TYPES_UL_List) from .components.metadata import (ComponentMetadata, ComponentsMeta, ensure_metadata_for_all_objects) from .propGroups.prop_groups import (generate_propertyGroups_for_components) @@ -87,6 +87,10 @@ def draw(self, context): CopyComponentOperator, PasteComponentOperator, RemoveComponentOperator, + RemoveComponentFromAllObjectsOperator, + Fix_Component_Operator, + OT_rename_component, + RenameHelper, GenerateComponent_From_custom_property_Operator, Toggle_ComponentVisibility, @@ -105,9 +109,13 @@ def draw(self, context): 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_MainPanel, BEVY_COMPONENTS_PT_ComponentsPanel, + BEVY_COMPONENTS_PT_AdvancedToolsPanel, BEVY_COMPONENTS_PT_Configuration, MISSING_TYPES_UL_List, BEVY_COMPONENTS_PT_MissingTypesPanel, diff --git a/tools/bevy_components/components/metadata.py b/tools/bevy_components/components/metadata.py index f97a3421..3ba7deb1 100644 --- a/tools/bevy_components/components/metadata.py +++ b/tools/bevy_components/components/metadata.py @@ -237,6 +237,23 @@ def apply_propertyGroup_values_to_object_customProperties(object): value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None) object[component_name] = 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 + print("yallah", component_name) + (_, propertyGroup) = upsert_component_in_object(object, component_name, registry) + component_definition = find_component_definition_from_short_name(component_name) + if component_definition != None: + print("merde") + 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["name"] == component_name, components_metadata), None) + if componentMeta: + print("here") + componentMeta.invalid = False + componentMeta.invalid_details = "" def apply_customProperty_values_to_object_propertyGroups(object): @@ -258,6 +275,8 @@ def apply_customProperty_values_to_object_propertyGroups(object): 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): diff --git a/tools/bevy_components/components/operators.py b/tools/bevy_components/components/operators.py index 13ddffc9..f640a98f 100644 --- a/tools/bevy_components/components/operators.py +++ b/tools/bevy_components/components/operators.py @@ -3,7 +3,7 @@ import bpy from bpy_types import Operator from bpy.props import (StringProperty) -from .metadata import add_component_to_object, add_metadata_to_components_without_metadata, apply_customProperty_values_to_object_propertyGroups, copy_propertyGroup_values_to_another_object, find_component_definition_from_short_name, remove_component_from_object +from .metadata import add_component_to_object, add_metadata_to_components_without_metadata, apply_customProperty_values_to_object_propertyGroups, apply_propertyGroup_values_to_object_customProperties_for_component, copy_propertyGroup_values_to_another_object, find_component_definition_from_short_name, remove_component_from_object class AddComponentOperator(Operator): """Add component to blueprint""" @@ -90,12 +90,10 @@ def execute(self, context): return {'FINISHED'} - - class RemoveComponentOperator(Operator): - """Delete component from blueprint""" + """Remove component from object""" bl_idname = "object.remove_bevy_component" - bl_label = "Delete component from blueprint Operator" + bl_label = "Remove component from object Operator" bl_options = {"UNDO"} component_name: StringProperty( @@ -103,11 +101,18 @@ class RemoveComponentOperator(Operator): description="component to delete", ) # type: ignore - def execute(self, context): - object = context.object + 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 self.component_name in object: remove_component_from_object(object, self.component_name) else: @@ -116,6 +121,154 @@ def execute(self, context): return {'FINISHED'} +class RemoveComponentFromAllObjectsOperator(Operator): + """Remove 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", + 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 self.component_name in object: + 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 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 object: + + # copy data to new component, remove the old one + try: + object[new_name] = object[original_name] + remove_component_from_object(object, original_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 + # get metadata + components_metadata = getattr(object, "components_meta", None) + if components_metadata: + components_metadata = components_metadata.components + component_meta = next(filter(lambda component: component["name"] == new_name, components_metadata), None) + if component_meta: + component_meta.invalid = True + component_meta.invalid_details = "unknow issue when renaming/transforming component, please remove it & add it back again" + + errors.append( "failed to copy old component value to new component: object: '" + object.name + "', error: " + str(error)) + + try: + # attempt conversion + long_name = registry.short_names_to_long_names[new_name] + component_definition = type_infos[long_name] + add_component_to_object(object, component_definition, object[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["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 components from custom property""" bl_idname = "object.generate_bevy_component_from_custom_property" @@ -143,6 +296,31 @@ def execute(self, context): return {'FINISHED'} +class Fix_Component_Operator(Operator): + """attempt to fix 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): """toggles components visibility""" bl_idname = "object.toggle_bevy_component_visibility" diff --git a/tools/bevy_components/components/ui.py b/tools/bevy_components/components/ui.py index 23e8fe38..4a356eb4 100644 --- a/tools/bevy_components/components/ui.py +++ b/tools/bevy_components/components/ui.py @@ -1,7 +1,9 @@ import json import bpy + +from ..registry.operators import COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT from .metadata import do_object_custom_properties_have_missing_metadata -from .operators import AddComponentOperator, CopyComponentOperator, RemoveComponentOperator, GenerateComponent_From_custom_property_Operator, PasteComponentOperator, Toggle_ComponentVisibility +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") @@ -193,6 +195,16 @@ def draw(self, context): 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() diff --git a/tools/bevy_components/docs/component_remove_single.png b/tools/bevy_components/docs/component_remove_single.png new file mode 100644 index 00000000..30814d6f Binary files /dev/null and b/tools/bevy_components/docs/component_remove_single.png differ diff --git a/tools/bevy_components/docs/component_rename_object_select.png b/tools/bevy_components/docs/component_rename_object_select.png new file mode 100644 index 00000000..ea88d3a3 Binary files /dev/null and b/tools/bevy_components/docs/component_rename_object_select.png differ diff --git a/tools/bevy_components/docs/component_rename_overview2.png b/tools/bevy_components/docs/component_rename_overview2.png new file mode 100644 index 00000000..bc994b4a Binary files /dev/null and b/tools/bevy_components/docs/component_rename_overview2.png differ diff --git a/tools/bevy_components/docs/component_rename_remove_bulk.png b/tools/bevy_components/docs/component_rename_remove_bulk.png new file mode 100644 index 00000000..e4b91b27 Binary files /dev/null and b/tools/bevy_components/docs/component_rename_remove_bulk.png differ diff --git a/tools/bevy_components/docs/component_rename_remove_bulk2.png b/tools/bevy_components/docs/component_rename_remove_bulk2.png new file mode 100644 index 00000000..76de0805 Binary files /dev/null and b/tools/bevy_components/docs/component_rename_remove_bulk2.png differ diff --git a/tools/bevy_components/docs/component_rename_single.png b/tools/bevy_components/docs/component_rename_single.png new file mode 100644 index 00000000..0c86b3ac Binary files /dev/null and b/tools/bevy_components/docs/component_rename_single.png differ diff --git a/tools/bevy_components/docs/other_options.png b/tools/bevy_components/docs/other_options.png index 9eba317d..6356d083 100644 Binary files a/tools/bevy_components/docs/other_options.png and b/tools/bevy_components/docs/other_options.png differ diff --git a/tools/bevy_components/propGroups/conversions_to_prop_group.py b/tools/bevy_components/propGroups/conversions_to_prop_group.py index 6eec325b..84d10e2b 100644 --- a/tools/bevy_components/propGroups/conversions_to_prop_group.py +++ b/tools/bevy_components/propGroups/conversions_to_prop_group.py @@ -224,8 +224,9 @@ def property_group_value_from_custom_property_value(property_group, definition, else: - pass - #print("struct with zero fields") + 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) diff --git a/tools/bevy_components/registry/operators.py b/tools/bevy_components/registry/operators.py index 8abee81f..8d844131 100644 --- a/tools/bevy_components/registry/operators.py +++ b/tools/bevy_components/registry/operators.py @@ -42,11 +42,26 @@ class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL(Operator): 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() - for object in bpy.data.objects: + 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'} @@ -56,10 +71,23 @@ class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT(Operator): 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'} @@ -69,44 +97,79 @@ class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT(Operator): 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: 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") + 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 = [] - for object in bpy.data.objects: + 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): @@ -133,3 +196,41 @@ def execute(self, context): 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'} + \ No newline at end of file diff --git a/tools/bevy_components/registry/registry.py b/tools/bevy_components/registry/registry.py index 1299130c..20eb14a4 100644 --- a/tools/bevy_components/registry/registry.py +++ b/tools/bevy_components/registry/registry.py @@ -241,9 +241,6 @@ def unregister(cls): del bpy.types.WindowManager.components_registry - - - def load_schema(self): print("load schema", self) # cleanup previous data if any @@ -352,9 +349,9 @@ def generate_propGroup_name(self, nesting, shortName): return propGroupName def get_propertyGroupName_from_shortName(self, shortName): - return self.short_names_to_propgroup_names.get(shortName, None) + ########### """ object[component_definition.name] = 0.5 diff --git a/tools/bevy_components/registry/ui.py b/tools/bevy_components/registry/ui.py index 18ff21a6..9df1ab09 100644 --- a/tools/bevy_components/registry/ui.py +++ b/tools/bevy_components/registry/ui.py @@ -1,9 +1,15 @@ +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, ReloadRegistryOperator, + OT_OpenFilebrowser, + OT_select_component_name_to_replace, + OT_select_object, ReloadRegistryOperator, COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT) @@ -21,8 +27,7 @@ class BEVY_COMPONENTS_PT_Configuration(bpy.types.Panel): def draw(self, context): layout = self.layout registry = 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 + row = layout.row() col = row.column() @@ -43,31 +48,212 @@ def draw(self, context): 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 = "BEVY_COMPONENTS_PT_MainPanel" + bl_options = {'DEFAULT_CLOSED'} + bl_description = "advanced tooling" + + + 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]['short_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): + short_name = component_meta.name + if component_meta.invalid: + self.draw_invalid_or_unregistered(layout, "Invalid", short_name, object) + + if not object.name in objects_with_invalid_components: + objects_with_invalid_components.append(object.name) + + if not short_name in invalid_component_names: + invalid_component_names.append(short_name) + + + comp_names.append(short_name) + + for custom_property in object.keys(): + if custom_property != 'components_meta' 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 short_name in invalid_component_names: + invalid_component_names.append(custom_property) + 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() - 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 + 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() - 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 + 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() - 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 + 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() - 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 + 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): diff --git a/tools/bevy_components/tests/test_rename_components.py b/tools/bevy_components/tests/test_rename_components.py new file mode 100644 index 00000000..13efed6d --- /dev/null +++ b/tools/bevy_components/tests/test_rename_components.py @@ -0,0 +1,159 @@ +import json +import re +import bpy +import pprint +import pytest + +from .setup_data import setup_data + +# small helpers +def get_component_metadata(object, component_name): + target_components_metadata = object.components_meta.components + component_meta = next(filter(lambda component: component["name"] == component_name, target_components_metadata), None) + return component_meta + +def get_component_propGroup(registry, component_name, component_meta): + # component_type = registry.short_names_to_long_names[component_name] + # add_component_operator = bpy.ops.object.add_bevy_component + property_group_name = registry.get_propertyGroupName_from_shortName(component_name) + propertyGroup = getattr(component_meta, property_group_name, None) + return propertyGroup + + +def test_rename_component_single_unit_struct(setup_data): + registry = bpy.context.window_manager.components_registry + registry.schemaPath = setup_data["schema_path"] + bpy.ops.object.reload_registry() + + rename_component_operator = bpy.ops.object.rename_bevy_component + object = bpy.context.object + + + source_component_name = "SomeOldUnitStruct" + target_component_name = "UnitTest" + object[source_component_name] = '()' + + rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps([object.name])) + + is_old_component_in_object = source_component_name in object + is_new_component_in_object = target_component_name in object + assert is_old_component_in_object == False + assert is_new_component_in_object == True + assert object[target_component_name] == '()' + assert get_component_propGroup(registry, target_component_name, get_component_metadata(object, target_component_name)) != None + + +def test_rename_component_single_complex_struct(setup_data): + registry = bpy.context.window_manager.components_registry + registry.schemaPath = setup_data["schema_path"] + bpy.ops.object.reload_registry() + + rename_component_operator = bpy.ops.object.rename_bevy_component + object = bpy.context.object + + + source_component_name = "ProxyCollider" + target_component_name = "Collider" + object[source_component_name] = 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)' + + rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps([object.name])) + + is_old_component_in_object = source_component_name in object + is_new_component_in_object = target_component_name in object + assert is_old_component_in_object == False + assert is_new_component_in_object == True + assert object[target_component_name] == 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)' + assert get_component_propGroup(registry, target_component_name, get_component_metadata(object, target_component_name)) != None + + +def test_rename_component_bulk(setup_data): + registry = bpy.context.window_manager.components_registry + registry.schemaPath = setup_data["schema_path"] + bpy.ops.object.reload_registry() + + rename_component_operator = bpy.ops.object.rename_bevy_component + + source_component_name = "SomeOldUnitStruct" + target_component_name = "UnitTest" + objects_names = [] + for object in bpy.data.objects: + object[source_component_name] = '()' + objects_names.append(object.name) + + # bulk rename + rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps(objects_names)) + + for object in bpy.data.objects: + is_old_component_in_object = source_component_name in object + is_new_component_in_object = target_component_name in object + assert is_old_component_in_object == False + assert is_new_component_in_object == True + assert object[target_component_name] == '()' + assert get_component_propGroup(registry, target_component_name, get_component_metadata(object, target_component_name)) != None + +def test_rename_component_single_error_handling(setup_data): + registry = bpy.context.window_manager.components_registry + registry.schemaPath = setup_data["schema_path"] + bpy.ops.object.reload_registry() + + rename_component_operator = bpy.ops.object.rename_bevy_component + object = bpy.context.object + + + source_component_name = "SomeOldUnitStruct" + target_component_name = "UnitTest" + object[source_component_name] = 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)' + + expected_error = f'Error: Failed to rename component: Errors:["wrong custom property values to generate target component: object: \'{object.name}\', error: input string too big for a unit struct"]\n' + expected_error = re.escape(expected_error) + with pytest.raises(Exception, match=expected_error): + rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps([object.name])) + + target_component_metadata = get_component_metadata(object, target_component_name) + + is_old_component_in_object = source_component_name in object + is_new_component_in_object = target_component_name in object + assert is_old_component_in_object == False + assert is_new_component_in_object == True + assert object[target_component_name] == 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)' + assert get_component_propGroup(registry, target_component_name, target_component_metadata) != None + assert target_component_metadata.invalid == True + + assert target_component_metadata.invalid_details == 'wrong custom property value, overwrite them by changing the values in the ui or change them & regenerate' + +def test_rename_component_single_error_handling_clean_errors(setup_data): + registry = bpy.context.window_manager.components_registry + registry.schemaPath = setup_data["schema_path"] + bpy.ops.object.reload_registry() + + rename_component_operator = bpy.ops.object.rename_bevy_component + object = bpy.context.object + + + source_component_name = "SomeOldUnitStruct" + target_component_name = "UnitTest" + object[source_component_name] = 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)' + + expected_error = f'Error: Failed to rename component: Errors:["wrong custom property values to generate target component: object: \'{object.name}\', error: input string too big for a unit struct"]\n' + expected_error = re.escape(expected_error) + with pytest.raises(Exception, match=expected_error): + rename_component_operator(original_name=source_component_name, new_name=target_component_name, target_objects=json.dumps([object.name])) + + target_component_metadata = get_component_metadata(object, target_component_name) + + is_old_component_in_object = source_component_name in object + is_new_component_in_object = target_component_name in object + assert is_old_component_in_object == False + assert is_new_component_in_object == True + assert object[target_component_name] == 'Capsule(Vec3(x:1.0, y:2.0, z:0.0), Vec3(x:0.0, y:0.0, z:0.0), 3.0)' + assert get_component_propGroup(registry, target_component_name, target_component_metadata) != None + assert target_component_metadata.invalid == True + + assert target_component_metadata.invalid_details == 'wrong custom property value, overwrite them by changing the values in the ui or change them & regenerate' + + # if we fix the custom property value & regen the ui, it should be all good + regen_component_operator = bpy.ops.object.refresh_ui_from_custom_properties_current + object[target_component_name] = '' + regen_component_operator() + + assert target_component_metadata.invalid == False