Register Property Groups#
This document describes the property group registration system in the UVFlow addon_utils module, which provides a structured way to organize related properties and attach them to Blender types with automatic cleanup and type safety.
Overview#
The property group registration system allows you to:
Create structured property collections with related data
Attach to any Blender type (Scene, Object, Mesh, WindowManager, etc.)
Nest property groups for complex data structures
Automatic registration and cleanup when the addon is disabled
Type-safe access with helper methods and utilities
Property Group Types#
Child Property Groups#
Child property groups are meant to be nested within other property groups and provide reusable data structures:
from ..addon_utils import Register, Property
@Register.PROP_GROUP.CHILD
class ChildPropertyGroup:
some_bool: Property.BOOL(name="Enable Feature", default=True)
some_string: Property.STRING(name="Custom Name", default="Default")
some_float: Property.FLOAT(name="Scale Factor", default=1.0, min=0.1, max=10.0)
Features:
Can be nested in other property groups
Reusable across multiple parent groups
No direct attachment to Blender types
Root Property Groups#
Root property groups are directly attached to Blender types and serve as the main data containers:
WindowManager (Temporal) Properties#
For temporary UI state that doesn’t persist with the file:
@Register.PROP_GROUP.ROOT.WINDOW_MANAGER('uvflow') # or ROOT.TEMPORAL
class TempProps:
show_uvmap_section: Property.BOOL(name="Show UV Map Options", default=True)
show_unwrap_section: Property.BOOL(name="Show Unwrap Options", default=False)
show_straighten_section: Property.BOOL(name="Show Straighten Options", default=False)
show_pack_section: Property.BOOL(name="Show Pack Options", default=False)
@staticmethod
def get_data(context) -> 'TempProps':
return context.window_manager.uvflow
Scene Properties#
For global settings that persist with the blend file:
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class SceneProps:
uv_editor_enabled: Property.BOOL(name="Toggle UV Editor", default=False)
global_scale: Property.FLOAT(name="Global Scale", default=1.0, min=0.001, max=1000.0)
export_format: Property.ENUM(
name="Export Format",
items=[
('OBJ', 'OBJ', 'Wavefront OBJ format'),
('FBX', 'FBX', 'Autodesk FBX format'),
('GLTF', 'glTF', 'GL Transmission Format'),
],
default='OBJ'
)
@staticmethod
def get_data(context) -> 'SceneProps':
return context.scene.uvflow
Object Properties#
For object-specific data:
@Register.PROP_GROUP.ROOT.OBJECT('uvflow')
class ObjectProps:
custom_id: Property.STRING(name="Custom ID", default="")
is_processed: Property.BOOL(name="Is Processed", default=False)
processing_data: Property.VECTOR_3D(name="Processing Data", default=(0, 0, 0))
@staticmethod
def get_data(obj) -> 'ObjectProps':
return obj.uvflow
Mesh Properties#
For mesh-specific data with advanced functionality:
from bpy.types import Context, MeshUVLoopLayer, Mesh, Object
@Register.PROP_GROUP.ROOT.MESH('uvflow')
class MeshProps:
last_uv_layer_index: Property.INT(name="Last UV Layer Index", default=-1)
last_uv_layer_count: Property.INT(name="Last UV Layer Count", default=0)
last_uv_layer_name: Property.STRING(name="Last UV Layer Name", default="")
@classmethod
def ensure_last_uv_layer(cls, mesh: Mesh) -> None:
"""Ensure all UV layer tracking data is initialized"""
if cls.get_last_uv_layer_count(mesh) == 0:
cls.update_last_uv_layer_count(mesh)
if cls.get_last_uv_layer_index(mesh) == -1:
cls.update_last_uv_layer_index(mesh)
if cls.get_last_uv_layer_name(mesh) == '':
cls.update_last_uv_layer_name(mesh)
@staticmethod
def get_data(data: Object | Mesh | Context) -> 'MeshProps':
"""Flexible data access from different contexts"""
if isinstance(data, Mesh):
return data.uvflow
if isinstance(data, Context):
return data.object.data.uvflow
if isinstance(data, Object):
return data.data.uvflow
raise TypeError("Invalid data type")
@classmethod
def update_last_uv_layer_index(cls, mesh: Mesh, get_indices: bool = False):
"""Track UV layer index changes"""
me_uvflow = cls.get_data(mesh)
prev_index = me_uvflow.last_uv_layer_index
curr_index = mesh.uv_layers.active_index if mesh.uv_layers.active is not None else -1
if prev_index != curr_index:
me_uvflow.last_uv_layer_index = curr_index
if get_indices:
return prev_index != curr_index, prev_index, curr_index
return prev_index != curr_index
Advanced Property Group Features#
Nested Property Groups#
Combine child and root property groups for complex data structures:
@Register.PROP_GROUP.CHILD
class ExportSettings:
format: Property.ENUM(
items=[('OBJ', 'OBJ', ''), ('FBX', 'FBX', '')],
default='OBJ'
)
scale: Property.FLOAT(default=1.0, min=0.1, max=10.0)
apply_modifiers: Property.BOOL(default=True)
@Register.PROP_GROUP.CHILD
class ImportSettings:
auto_smooth: Property.BOOL(default=True)
merge_vertices: Property.BOOL(default=False)
merge_threshold: Property.FLOAT(default=0.001, min=0.0001, max=1.0)
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class ProjectSettings:
# Basic properties
project_name: Property.STRING(default="Untitled Project")
version: Property.INT(default=1, min=1)
# Nested property groups
export_settings: Property.POINTER(type=ExportSettings)
import_settings: Property.POINTER(type=ImportSettings)
@staticmethod
def get_data(context) -> 'ProjectSettings':
return context.scene.uvflow
Collection Properties#
Use collections for lists of structured data:
@Register.PROP_GROUP.CHILD
class CustomItem:
name: Property.STRING(default="Item")
value: Property.FLOAT(default=0.0)
enabled: Property.BOOL(default=True)
color: Property.COLOR_RGB(default=(1, 1, 1))
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class ItemManager:
# Collection of custom items
items: Property.COLLECTION(type=CustomItem)
# Index for UI list
active_item_index: Property.INT(default=0, min=0)
@property
def active_item(self) -> CustomItem:
"""Get the currently active item"""
if 0 <= self.active_item_index < len(self.items):
return self.items[self.active_item_index]
return None
def add_item(self, name: str = "New Item") -> CustomItem:
"""Add a new item to the collection"""
item = self.items.add()
item.name = name
return item
def remove_item(self, index: int) -> bool:
"""Remove an item from the collection"""
if 0 <= index < len(self.items):
self.items.remove(index)
if self.active_item_index >= len(self.items):
self.active_item_index = max(0, len(self.items) - 1)
return True
return False
Property Groups with Update Callbacks#
Add dynamic behavior with update functions:
def update_scale_factor(self, context):
"""Called when scale_factor changes"""
if self.auto_update:
print(f"Scale factor changed to: {self.scale_factor}")
# Trigger viewport updates, recalculations, etc.
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class DynamicSettings:
scale_factor: Property.FLOAT(
name="Scale Factor",
default=1.0,
min=0.1,
max=10.0,
update=update_scale_factor
)
auto_update: Property.BOOL(
name="Auto Update",
default=True,
description="Automatically update when properties change"
)
Property Group Access Patterns#
Static Access Methods#
Provide convenient access to property group data:
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class ToolSettings:
tool_mode: Property.ENUM(
items=[('BASIC', 'Basic', ''), ('ADVANCED', 'Advanced', '')],
default='BASIC'
)
@staticmethod
def get_data(context) -> 'ToolSettings':
return context.scene.uvflow
@classmethod
def is_advanced_mode(cls, context) -> bool:
return cls.get_data(context).tool_mode == 'ADVANCED'
@classmethod
def set_mode(cls, context, mode: str):
cls.get_data(context).tool_mode = mode
Flexible Data Access#
Handle multiple input types gracefully:
@Register.PROP_GROUP.ROOT.OBJECT('uvflow')
class ObjectData:
custom_name: Property.STRING(default="")
@staticmethod
def get_data(data) -> 'ObjectData':
"""Flexible access from different contexts"""
if hasattr(data, 'uvflow'):
return data.uvflow
elif hasattr(data, 'object') and data.object:
return data.object.uvflow
elif hasattr(data, 'active_object') and data.active_object:
return data.active_object.uvflow
raise TypeError(f"Cannot get ObjectData from {type(data)}")
Integration with UI System#
Property Group UI Panels#
Create panels that work with property groups:
@Register.UI.PANEL.VIEW3D
class SettingsPanel:
label = "Project Settings"
def draw_ui(self, context, layout):
settings = ProjectSettings.get_data(context)
# Basic properties
layout.prop(settings, "project_name")
layout.prop(settings, "version")
# Nested property groups
box = layout.box()
box.label(text="Export Settings:")
box.prop(settings.export_settings, "format")
box.prop(settings.export_settings, "scale")
box.prop(settings.export_settings, "apply_modifiers")
box = layout.box()
box.label(text="Import Settings:")
box.prop(settings.import_settings, "auto_smooth")
box.prop(settings.import_settings, "merge_vertices")
if settings.import_settings.merge_vertices:
box.prop(settings.import_settings, "merge_threshold")
Collection UI Lists#
Display collections in UI lists:
@Register.UI.PANEL.VIEW3D
class ItemManagerPanel:
label = "Item Manager"
def draw_ui(self, context, layout):
manager = ItemManager.get_data(context)
# UI List for items
row = layout.row()
row.template_list(
"UI_UL_list", "custom_items",
manager, "items",
manager, "active_item_index"
)
# Add/Remove buttons
col = row.column(align=True)
col.operator("uvflow.add_item", icon='ADD', text="")
col.operator("uvflow.remove_item", icon='REMOVE', text="")
# Active item properties
if manager.active_item:
box = layout.box()
box.label(text="Item Properties:")
box.prop(manager.active_item, "name")
box.prop(manager.active_item, "value")
box.prop(manager.active_item, "enabled")
box.prop(manager.active_item, "color")
Integration with Operators#
Property Group Operators#
Create operators that work with property groups:
@Register.OPS.GENERIC
class AddItemOperator:
def action(self, context):
manager = ItemManager.get_data(context)
item = manager.add_item("New Item")
manager.active_item_index = len(manager.items) - 1
self.report({'INFO'}, f"Added item: {item.name}")
@Register.OPS.GENERIC
class RemoveItemOperator:
def action(self, context):
manager = ItemManager.get_data(context)
if manager.active_item:
name = manager.active_item.name
if manager.remove_item(manager.active_item_index):
self.report({'INFO'}, f"Removed item: {name}")
else:
self.report({'WARNING'}, "No item to remove")
@Register.OPS.INVOKE_PROPS
class ConfigureSettingsOperator:
# Override with property group values
scale: Property.FLOAT(default=1.0)
def action(self, context):
settings = ToolSettings.get_data(context)
# Use property group data or operator properties
scale = self.scale if self.scale != 1.0 else settings.scale_factor
# Process with the scale value
Best Practices#
1. Use Descriptive Names and Structure#
# Good: Clear hierarchy and naming
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class UVFlowSettings:
unwrap_settings: Property.POINTER(type=UnwrapSettings)
pack_settings: Property.POINTER(type=PackSettings)
display_settings: Property.POINTER(type=DisplaySettings)
# Avoid: Flat structure with unclear names
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class Settings:
val1: Property.FLOAT()
val2: Property.BOOL()
val3: Property.STRING()
2. Provide Access Helpers#
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class SmartSettings:
@staticmethod
def get_data(context) -> 'SmartSettings':
return context.scene.uvflow
@classmethod
def reset_to_defaults(cls, context):
"""Reset all settings to default values"""
settings = cls.get_data(context)
# Reset logic here
@classmethod
def copy_from_object(cls, context, source_obj):
"""Copy settings from another object"""
# Copy logic here
3. Handle Missing Data Gracefully#
@Register.PROP_GROUP.ROOT.MESH('uvflow')
class SafeMeshProps:
@staticmethod
def get_data(data, safe: bool = True):
try:
if isinstance(data, Mesh):
return data.uvflow
elif hasattr(data, 'data') and data.data:
return data.data.uvflow
elif hasattr(data, 'object') and data.object and data.object.data:
return data.object.data.uvflow
except (AttributeError, TypeError):
if safe:
return None
raise
if safe:
return None
raise TypeError("Cannot access mesh properties")
4. Use Appropriate Root Types#
# Persistent project settings → Scene
@Register.PROP_GROUP.ROOT.SCENE('project')
class ProjectSettings:
pass
# Temporary UI state → WindowManager
@Register.PROP_GROUP.ROOT.TEMPORAL('ui_state')
class UIState:
pass
# Object-specific data → Object
@Register.PROP_GROUP.ROOT.OBJECT('custom_data')
class ObjectCustomData:
pass
# Mesh-specific data → Mesh
@Register.PROP_GROUP.ROOT.MESH('mesh_data')
class MeshCustomData:
pass
Automatic Cleanup#
The property group registration system automatically handles:
Blender registration/unregistration
Property cleanup when addon is disabled
Memory management for nested structures
Collection cleanup for dynamic lists
No manual cleanup is required in most cases.
Common Patterns#
Settings Manager Pattern#
@Register.PROP_GROUP.CHILD
class ToolSettings:
strength: Property.FLOAT(default=1.0, min=0.0, max=2.0)
mode: Property.ENUM(items=[('A', 'Mode A', ''), ('B', 'Mode B', '')], default='A')
@Register.PROP_GROUP.ROOT.SCENE('uvflow')
class GlobalSettings:
tool_settings: Property.POINTER(type=ToolSettings)
@classmethod
def get_tool_settings(cls, context) -> ToolSettings:
return cls.get_data(context).tool_settings
State Tracking Pattern#
@Register.PROP_GROUP.ROOT.MESH('uvflow')
class MeshState:
last_modified: Property.FLOAT(default=0.0)
is_dirty: Property.BOOL(default=False)
@classmethod
def mark_dirty(cls, mesh: Mesh):
import time
state = cls.get_data(mesh)
state.is_dirty = True
state.last_modified = time.time()
@classmethod
def mark_clean(cls, mesh: Mesh):
state = cls.get_data(mesh)
state.is_dirty = False
This property group registration system provides a robust foundation for organizing complex addon data with automatic management and type safety.