Blender_bevy_components_wor.../tools/bevy_components/registry/registry.py
Mark Moissette 1353e14802
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
2024-03-07 16:29:04 +01:00

369 lines
13 KiB
Python

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