feat(bevy_components): qol improvements (#164)

* closes #163 
* closes #153 
* closes #154 
* feat(bevy_components): added tools for diagnostics/ finding & replacing invalid & unregistered components
   * added ui for listing invalid & unregistered components
   * added boilerplate & functionality for component renaming/replacing
   * injection of invalid status & message in case the conversion did not work well
   * added deletion of components individual & bulk
   * added handling of wrong string for unit structs : allows detection of more wrong values for components
   * added progress bars for bulk operators
   * added docs for new features
   * added tests
   * added small "attempt to fix" button for unit struct uis in case they are invalid
* feat(bevy_components): added progress indicators for from/to custom properties
* various other minor ui tweaks for workflow improvement
This commit is contained in:
Mark Moissette 2024-03-07 16:29:04 +01:00 committed by GitHub
parent 3b7b5f28bd
commit 1353e14802
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 759 additions and 43 deletions

View File

@ -57,6 +57,14 @@ Before you can use the add-on you need to configure it
![configuration 3](./docs/configuration3.png) ![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 ## Use
@ -173,8 +181,8 @@ It will add the component to the select object
![invalid component](./docs/invalid_components.png) ![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 > see [here](#invalidunregistered-type-renaming--conversion) for ways to convert invalid / unregistered components to other types.
this will be adress in the future
- 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 - 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
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)
* by default, the add-on will check for changes in your registry file every second, and refresh the UI accordingly #### Single object component renaming/ conversion
* you can set the polling frequency or turn it off if you do not want auto-refresh
![registry file polling](./docs/registry_polling.png) - 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)
### regenerate custom property values ### For conversion between custom properties & components & vice-versa
#### 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 - "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 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 - 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

View File

@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "bevy_components", "name": "bevy_components",
"author": "kaosigh", "author": "kaosigh",
"version": (0, 4, 0), "version": (0, 4, 1),
"blender": (3, 4, 0), "blender": (3, 4, 0),
"location": "VIEW_3D", "location": "VIEW_3D",
"description": "UI to help create Bevy blueprints and components", "description": "UI to help create Bevy blueprints and components",
@ -16,11 +16,11 @@ from bpy.props import (StringProperty)
from .helpers import load_settings from .helpers import load_settings
from .blueprints import CreateBlueprintOperator 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.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.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_MissingTypesPanel, MISSING_TYPES_UL_List) 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 .components.metadata import (ComponentMetadata, ComponentsMeta, ensure_metadata_for_all_objects)
from .propGroups.prop_groups import (generate_propertyGroups_for_components) from .propGroups.prop_groups import (generate_propertyGroups_for_components)
@ -87,6 +87,10 @@ classes = [
CopyComponentOperator, CopyComponentOperator,
PasteComponentOperator, PasteComponentOperator,
RemoveComponentOperator, RemoveComponentOperator,
RemoveComponentFromAllObjectsOperator,
Fix_Component_Operator,
OT_rename_component,
RenameHelper,
GenerateComponent_From_custom_property_Operator, GenerateComponent_From_custom_property_Operator,
Toggle_ComponentVisibility, Toggle_ComponentVisibility,
@ -106,8 +110,12 @@ classes = [
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT,
OT_select_object,
OT_select_component_name_to_replace,
BEVY_COMPONENTS_PT_MainPanel, BEVY_COMPONENTS_PT_MainPanel,
BEVY_COMPONENTS_PT_ComponentsPanel, BEVY_COMPONENTS_PT_ComponentsPanel,
BEVY_COMPONENTS_PT_AdvancedToolsPanel,
BEVY_COMPONENTS_PT_Configuration, BEVY_COMPONENTS_PT_Configuration,
MISSING_TYPES_UL_List, MISSING_TYPES_UL_List,
BEVY_COMPONENTS_PT_MissingTypesPanel, BEVY_COMPONENTS_PT_MissingTypesPanel,

View File

@ -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) value = property_group_value_to_custom_property_value(propertyGroup, component_definition, registry, None)
object[component_name] = value 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): 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) 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) property_group_value_from_custom_property_value(propertyGroup, component_definition, registry, customProperty_value)
del object["__disable__update"] 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 # 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): def remove_component_from_object(object, component_name):

View File

@ -3,7 +3,7 @@ import json
import bpy import bpy
from bpy_types import Operator from bpy_types import Operator
from bpy.props import (StringProperty) 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): class AddComponentOperator(Operator):
"""Add component to blueprint""" """Add component to blueprint"""
@ -90,12 +90,10 @@ class PasteComponentOperator(Operator):
return {'FINISHED'} return {'FINISHED'}
class RemoveComponentOperator(Operator): class RemoveComponentOperator(Operator):
"""Delete component from blueprint""" """Remove component from object"""
bl_idname = "object.remove_bevy_component" bl_idname = "object.remove_bevy_component"
bl_label = "Delete component from blueprint Operator" bl_label = "Remove component from object Operator"
bl_options = {"UNDO"} bl_options = {"UNDO"}
component_name: StringProperty( component_name: StringProperty(
@ -103,11 +101,18 @@ class RemoveComponentOperator(Operator):
description="component to delete", description="component to delete",
) # type: ignore ) # type: ignore
object_name: StringProperty(
name="object name",
description="object whose component to delete",
default=""
) # type: ignore
def execute(self, context): def execute(self, context):
if self.object_name == "":
object = context.object object = context.object
else:
object = bpy.data.objects[self.object_name]
print("removing component ", self.component_name, "from object '"+object.name+"'") print("removing component ", self.component_name, "from object '"+object.name+"'")
if object is not None and self.component_name in object: if object is not None and self.component_name in object:
remove_component_from_object(object, self.component_name) remove_component_from_object(object, self.component_name)
else: else:
@ -116,6 +121,154 @@ class RemoveComponentOperator(Operator):
return {'FINISHED'} 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): class GenerateComponent_From_custom_property_Operator(Operator):
"""generate components from custom property""" """generate components from custom property"""
bl_idname = "object.generate_bevy_component_from_custom_property" bl_idname = "object.generate_bevy_component_from_custom_property"
@ -143,6 +296,31 @@ class GenerateComponent_From_custom_property_Operator(Operator):
return {'FINISHED'} 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): class Toggle_ComponentVisibility(Operator):
"""toggles components visibility""" """toggles components visibility"""
bl_idname = "object.toggle_bevy_component_visibility" bl_idname = "object.toggle_bevy_component_visibility"

View File

@ -1,7 +1,9 @@
import json import json
import bpy import bpy
from ..registry.operators import COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT
from .metadata import do_object_custom_properties_have_missing_metadata 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): def draw_propertyGroup( propertyGroup, layout, nesting =[], rootName=None):
is_enum = getattr(propertyGroup, "with_enum") is_enum = getattr(propertyGroup, "with_enum")
@ -193,6 +195,16 @@ class BEVY_COMPONENTS_PT_ComponentsPanel(bpy.types.Panel):
row.label(text=error_message) row.label(text=error_message)
# "footer" with additional controls # "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 = row.operator(RemoveComponentOperator.bl_idname, text="", icon="X")
op.component_name = component_name op.component_name = component_name
row.separator() row.separator()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -224,8 +224,9 @@ def property_group_value_from_custom_property_value(property_group, definition,
else: else:
pass if len(value) > 2: #a unit struct should be two chars long :()
#print("struct with zero fields") #print("struct with zero fields")
raise Exception("input string too big for a unit struct")
elif type_info == "Tuple": elif type_info == "Tuple":
custom_property_values = parse_tuplestruct_string(value, start_nesting=1 if len(nesting) == 1 else 1) custom_property_values = parse_tuplestruct_string(value, start_nesting=1 if len(nesting) == 1 else 1)

View File

@ -42,11 +42,26 @@ class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_ALL(Operator):
bl_label = "Apply Registry to all objects" bl_label = "Apply Registry to all objects"
bl_options = {"UNDO"} 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): def execute(self, context):
print("apply registry to all") print("apply registry to all")
#context.window_manager.components_registry.load_schema() #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) 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'} return {'FINISHED'}
@ -56,10 +71,23 @@ class COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT(Operator):
bl_label = "Apply Registry to current object" bl_label = "Apply Registry to current object"
bl_options = {"UNDO"} 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): def execute(self, context):
print("apply registry to current object") print("apply registry to current object")
object = context.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) apply_propertyGroup_values_to_object_customProperties(object)
context.window_manager.custom_properties_from_components_progress = -1.0
return {'FINISHED'} 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_label = "Apply custom_properties to current object"
bl_options = {"UNDO"} 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): def execute(self, context):
print("apply custom properties to current object") print("apply custom properties to current object")
object = context.object object = context.object
error = False error = False
try: try:
apply_customProperty_values_to_object_propertyGroups(object) 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: except Exception as error:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure
error = True error = True
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Error:" + str(error)) self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Error:" + str(error))
if not error: if not error:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for selected object") 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'} return {'FINISHED'}
class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL(Operator): class COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL(Operator):
"""Update UI values from custom properties to ALL object""" """Update UI values from custom properties to ALL object"""
bl_idname = "object.refresh_ui_from_custom_properties_all" bl_idname = "object.refresh_ui_from_custom_properties_all"
bl_label = "Apply custom_properties to all objects" bl_label = "Apply custom_properties to all objects"
bl_options = {"UNDO"} 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): def execute(self, context):
print("apply custom properties to all object") print("apply custom properties to all object")
bpy.context.window_manager.components_registry.disable_all_object_updates = True bpy.context.window_manager.components_registry.disable_all_object_updates = True
errors = [] errors = []
for object in bpy.data.objects: total = len(bpy.data.objects)
for index, object in enumerate(bpy.data.objects):
try: try:
apply_customProperty_values_to_object_propertyGroups(object) apply_customProperty_values_to_object_propertyGroups(object)
except Exception as error: except Exception as error:
del object["__disable__update"] # make sure custom properties are updateable afterwards, even in the case of failure 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)) 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: if len(errors) > 0:
self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Errors:" + str(errors)) self.report({'ERROR'}, "Failed to update propertyGroup values from custom property: Errors:" + str(errors))
else: else:
self.report({'INFO'}, "Sucessfully generated UI values for custom properties for all objects") self.report({'INFO'}, "Sucessfully generated UI values for custom properties for all objects")
bpy.context.window_manager.components_registry.disable_all_object_updates = False bpy.context.window_manager.components_registry.disable_all_object_updates = False
context.window_manager.components_from_custom_properties_progress_all = -1.0
return {'FINISHED'} return {'FINISHED'}
class OT_OpenFilebrowser(Operator, ImportHelper): class OT_OpenFilebrowser(Operator, ImportHelper):
@ -133,3 +196,41 @@ class OT_OpenFilebrowser(Operator, ImportHelper):
return {'FINISHED'} 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

@ -241,9 +241,6 @@ class ComponentsRegistry(PropertyGroup):
del bpy.types.WindowManager.components_registry del bpy.types.WindowManager.components_registry
def load_schema(self): def load_schema(self):
print("load schema", self) print("load schema", self)
# cleanup previous data if any # cleanup previous data if any
@ -352,9 +349,9 @@ class ComponentsRegistry(PropertyGroup):
return propGroupName return propGroupName
def get_propertyGroupName_from_shortName(self, shortName): def get_propertyGroupName_from_shortName(self, shortName):
return self.short_names_to_propgroup_names.get(shortName, None) return self.short_names_to_propgroup_names.get(shortName, None)
###########
""" """
object[component_definition.name] = 0.5 object[component_definition.name] = 0.5

View File

@ -1,9 +1,15 @@
import json
import bpy import bpy
from bpy_types import (UIList) from bpy_types import (UIList)
from bpy.props import (StringProperty)
from ..components.operators import OT_rename_component, RemoveComponentFromAllObjectsOperator, RemoveComponentOperator
from .operators import( from .operators import(
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL, COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_ALL,
COMPONENTS_OT_REFRESH_PROPGROUPS_FROM_CUSTOM_PROPERTIES_CURRENT, 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_ALL,
COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT) COMPONENTS_OT_REFRESH_CUSTOM_PROPERTIES_CURRENT)
@ -21,8 +27,7 @@ class BEVY_COMPONENTS_PT_Configuration(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
registry = context.window_manager.components_registry 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() row = layout.row()
col = row.column() col = row.column()
@ -43,31 +48,212 @@ class BEVY_COMPONENTS_PT_Configuration(bpy.types.Panel):
layout.separator() layout.separator()
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 = 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.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.alert = True
##
row = layout.row() 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.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 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() layout.separator()
row = layout.row() 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.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 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 = 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.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 row.alert = True
components_from_custom_properties_progress_current = context.window_manager.components_from_custom_properties_progress
row = layout.row() 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.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 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() layout.separator()
row = layout.row() 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.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 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): class BEVY_COMPONENTS_PT_MissingTypesPanel(bpy.types.Panel):

View File

@ -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