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:

  1. Create structured property collections with related data

  2. Attach to any Blender type (Scene, Object, Mesh, WindowManager, etc.)

  3. Nest property groups for complex data structures

  4. Automatic registration and cleanup when the addon is disabled

  5. 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.