Register an Operator#
This document describes the comprehensive operator registration system in the UVFlow addon_utils module, which provides multiple operator types and registration methods for different use cases.
Overview#
The operator registration system automatically handles:
Automatic ID generation from class names
Label creation from class names or custom labels
Property integration with the Property system
UI drawing through
draw_ui
methodsBlender registration and cleanup
Basic Operator Types#
Generic Operator#
The most common operator type for simple actions:
from ..addon_utils import Register, Property
@Register.OPS.GENERIC
class TestOperator:
def action(self, context):
print(f"{context.active_object.name}: Hello, I'm the active object! :-)")
# No need to return anything! The system handles it automatically
Features:
Simple execution model
Automatic ID:
uvflow.test_operator
Automatic label: “Test Operator”
No return required in
action()
method
Operator with Properties and Invoke Dialog#
For operators that need user input before execution:
from ..addon_utils import Register, Property
@Register.OPS.INVOKE_PROPS
class TestOperatorInvokeProps:
test_filepath: Property.FILEPATH()
test_color: Property.COLOR_RGB(default=(1, 0, 1))
test_bool: Property.BOOL()
def action(self, context):
print("Hello!", self.test_filepath, self.test_color, self.test_bool)
# No need to return anything! Keep it simple!
Features:
Automatically shows property dialog when invoked
Properties are accessible as
self.property_name
Perfect for file operations, settings, etc.
Advanced Operator Types#
Modal Operators#
For operators that need continuous user interaction with enhanced features:
from ..addon_utils import Register, OpsReturn
@Register.OPS.MODAL.GENERIC
class InteractiveOperator:
# Enable built-in raycasting
use_raycast_info = True
raycast_type = 'SCENE' # 'SCENE', 'OBJECT', or 'BVHTREE'
def modal_start(self, context, event):
"""Called when modal operation starts"""
print("Modal operation started")
print(f"Mouse position: {self.mouse.current}")
# Access raycast info if enabled
if self.use_raycast_info and self.raycast_info.hit:
print(f"Hit location: {self.raycast_info.location}")
def modal_update(self, context, event, mouse):
"""Main modal logic - called for most events"""
if event.type == 'ESC':
return OpsReturn.CANCEL
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
print(f"Clicked at: {mouse.current}")
if self.raycast_info.hit:
print(f"Hit object: {self.raycast_info.object}")
return OpsReturn.FINISH
# Return None to continue modal operation
return None
def modal__mousemove(self, context, mouse):
"""Called specifically for mouse movement"""
print(f"Mouse moved to: {mouse.current}, delta: {mouse.delta}")
def modal__timer(self, context, event, mouse):
"""Called for timer events (if timer is started)"""
# Useful for continuous updates
return None
def modal_finish(self, context, event):
"""Called when modal completes successfully"""
print("Modal operation completed successfully")
def modal_cancel(self, context, event):
"""Called when modal is cancelled"""
print("Modal operation was cancelled")
def modal_exit(self, context, event):
"""Called when modal exits (success or cancel)"""
print("Modal operation ended")
Enhanced Modal Features:
Built-in Raycasting#
@Register.OPS.MODAL.GENERIC
class RaycastOperator:
use_raycast_info = True
raycast_type = 'SCENE' # Options: 'SCENE', 'OBJECT', 'BVHTREE'
def modal_update(self, context, event, mouse):
if self.raycast_info.hit:
print(f"Hit: {self.raycast_info.object.name}")
print(f"Location: {self.raycast_info.location}")
print(f"Normal: {self.raycast_info.normal}")
print(f"Face Index: {self.raycast_info.face_index}")
return None
Mouse Utilities#
@Register.OPS.MODAL.GENERIC
class MouseTrackingOperator:
def modal_update(self, context, event, mouse):
# Access comprehensive mouse data
print(f"Current: {mouse.current}") # Current position
print(f"Previous: {mouse.prev}") # Previous position
print(f"Start: {mouse.start}") # Initial position
print(f"Offset: {mouse.offset}") # From start to current
print(f"Delta: {mouse.delta}") # Frame-to-frame movement
print(f"Direction: {mouse.dir}") # Movement direction (-1, 0, 1)
# Local coordinates (if rect provided)
if hasattr(mouse, 'local'):
print(f"Local: {mouse.local}") # Local space coordinates
print(f"Local Rel: {mouse.local_rel}") # Relative [0-1] coordinates
return None
Modal Timer Control#
@Register.OPS.MODAL.GENERIC
class TimerModalOperator:
def modal_start(self, context, event):
# Start a timer for continuous updates
self.start_modal_timer(context, time_step=0.1) # 10 FPS
def modal__timer(self, context, event, mouse):
"""Called every timer interval"""
# Continuous update logic here
print("Timer tick")
return None
def modal_exit(self, context, event):
# Timer is automatically stopped, but you can stop it manually
self.stop_modal_timer(context)
Operator Calling Methods#
Class-based Operator Calling#
Use the operator class directly for cleaner code:
@Register.OPS.GENERIC
class HelperOperator:
my_prop: Property.STRING(default="helper")
def action(self, context):
print(f"Helper called with: {self.my_prop}")
@Register.OPS.GENERIC
class MainOperator:
def action(self, context):
# Method 1: Direct execution
HelperOperator.run()
# Method 2: With properties
HelperOperator.run(my_prop="custom_value")
# Method 3: Invoke with dialog (for INVOKE_PROPS operators)
HelperOperator.run_invoke(my_prop="dialog_value")
Traditional bpy.ops Calling#
Still available for compatibility:
@Register.OPS.GENERIC
class TraditionalCaller:
def action(self, context):
# Call another operator
bpy.ops.uvflow.my_operator_1('INVOKE_DEFAULT')
# Call with properties
bpy.ops.uvflow.my_operator_2('INVOKE_DEFAULT', my_prop='custom_value')
Return Values and Control Flow#
Action Method Returns#
@Register.OPS.GENERIC
class SimpleOperator:
def action(self, context):
# Simple case - no return needed
print("Operation completed")
# System automatically returns {'FINISHED'}
@Register.OPS.GENERIC
class CustomReturnOperator:
def execute(self, context):
"""Override execute for custom return handling"""
try:
self.action(context)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def action(self, context):
# Your logic here
if some_condition:
raise Exception("Something went wrong")
Modal Return Values#
from ..addon_utils import OpsReturn
@Register.OPS.MODAL.GENERIC
class ModalReturnOperator:
def modal_update(self, context, event, mouse):
if event.type == 'ESC':
return OpsReturn.CANCEL
elif event.type == 'ENTER':
return OpsReturn.FINISH
elif event.type == 'TAB':
return OpsReturn.PASS # Pass through to other handlers
# Return None to continue modal operation
return None
Available Return Values:
OpsReturn.FINISH
- Complete successfullyOpsReturn.CANCEL
- Cancel operationOpsReturn.PASS
- Pass event to other handlersOpsReturn.RUN
- Continue modal operationNone
- Continue modal operation (same as RUN)
Advanced Modal Examples#
Interactive Drawing Tool#
@Register.OPS.MODAL.GENERIC
class DrawingTool:
use_raycast_info = True
raycast_type = 'SCENE'
def modal_start(self, context, event):
self.points = []
self.drawing = False
def modal_update(self, context, event, mouse):
if event.type == 'LEFTMOUSE':
if event.value == 'PRESS':
self.drawing = True
if self.raycast_info.hit:
self.points.append(self.raycast_info.location.copy())
elif event.value == 'RELEASE':
self.drawing = False
elif event.type == 'RIGHTMOUSE' and event.value == 'PRESS':
return OpsReturn.FINISH
elif event.type == 'ESC':
return OpsReturn.CANCEL
return None
def modal__mousemove(self, context, mouse):
if self.drawing and self.raycast_info.hit:
self.points.append(self.raycast_info.location.copy())
def modal_finish(self, context, event):
print(f"Created path with {len(self.points)} points")
# Create mesh from points, etc.
Object Transformation Tool#
@Register.OPS.MODAL.GENERIC
class TransformTool:
use_raycast_info = True
raycast_type = 'OBJECT'
def modal_start(self, context, event):
self.target_object = context.active_object
if not self.target_object:
return OpsReturn.CANCEL
self.start_location = self.target_object.location.copy()
self.start_mouse = self.mouse.current.copy()
def modal_update(self, context, event, mouse):
if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
return OpsReturn.FINISH
elif event.type == 'ESC':
# Restore original position
self.target_object.location = self.start_location
return OpsReturn.CANCEL
return None
def modal__mousemove(self, context, mouse):
# Transform object based on mouse movement
offset = mouse.offset
self.target_object.location.x = self.start_location.x + offset.x * 0.01
self.target_object.location.y = self.start_location.y + offset.y * 0.01
# Update viewport
context.area.tag_redraw()
Performance Monitor Tool#
@Register.OPS.MODAL.GENERIC
class PerformanceMonitor:
def modal_start(self, context, event):
self.frame_count = 0
self.start_time = time.time()
self.start_modal_timer(context, 1.0) # Update every second
def modal__timer(self, context, event, mouse):
current_time = time.time()
elapsed = current_time - self.start_time
if elapsed >= 1.0:
fps = self.frame_count / elapsed
print(f"FPS: {fps:.2f}")
# Reset counters
self.frame_count = 0
self.start_time = current_time
return None
def modal__mousemove(self, context, mouse):
self.frame_count += 1
def modal_update(self, context, event, mouse):
if event.type == 'ESC':
return OpsReturn.CANCEL
return None
Property Integration#
All Property Types#
@Register.OPS.INVOKE_PROPS
class ComprehensivePropertiesOperator:
# Basic types
string_prop: Property.STRING(default="Default text")
int_prop: Property.INT(default=5, min=0, max=100)
float_prop: Property.FLOAT(default=1.0, min=0.0, max=10.0)
bool_prop: Property.BOOL(default=True)
# Advanced types
filepath_prop: Property.FILEPATH()
dirpath_prop: Property.DIRPATH()
color_prop: Property.COLOR_RGB(default=(1, 0, 0))
vector_prop: Property.VECTOR_3D(default=(0, 0, 1))
# Enum
enum_prop: Property.ENUM(
items=[
('OPTION1', 'Option 1', 'First option'),
('OPTION2', 'Option 2', 'Second option'),
],
default='OPTION1'
)
def action(self, context):
print(f"String: {self.string_prop}")
print(f"Int: {self.int_prop}")
print(f"Float: {self.float_prop}")
print(f"Bool: {self.bool_prop}")
print(f"File: {self.filepath_prop}")
print(f"Color: {self.color_prop}")
print(f"Vector: {self.vector_prop}")
print(f"Enum: {self.enum_prop}")
Automatic ID and Label Generation#
ID Generation Rules#
Class names are automatically converted to operator IDs:
TestOperator
→uvflow.test_operator
UVUnwrapAdvanced
→uvflow.uv_unwrap_advanced
MyCustomTool
→uvflow.my_custom_tool
Label Generation Rules#
Labels are automatically created from class names:
TestOperator
→ “Test Operator”UVUnwrapAdvanced
→ “UV Unwrap Advanced”MyCustomTool
→ “My Custom Tool”
Error Handling and Return Values#
Standard Return Values#
@Register.OPS.GENERIC
class ProperReturnOperator:
def execute(self, context):
"""Override execute for custom error handling"""
try:
self.action(context)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Operation failed: {str(e)}")
return {'CANCELLED'}
def action(self, context):
# Your operation logic
if some_error_condition:
raise Exception("Something went wrong")
Integration with UI System#
Drawing in Layouts#
Operators can be drawn in UI layouts using the draw_in_layout
method:
# In a panel or menu
MyOperator.draw_in_layout(layout, text="Custom Text", icon='MODIFIER')
MyOperator.draw_in_layout(layout, op_props={'my_prop': 'custom_value'})
Best Practices#
1. Use Descriptive Class Names#
# Good
@Register.OPS.GENERIC
class UVUnwrapWithSeams:
pass
# Avoid
@Register.OPS.GENERIC
class Op1:
pass
2. Handle Context Properly#
@Register.OPS.GENERIC
class ContextAwareOperator:
def action(self, context):
if not context.active_object:
self.report({'ERROR'}, "No active object")
return # No need to return specific values
if context.active_object.type != 'MESH':
self.report({'ERROR'}, "Active object must be a mesh")
return
# Proceed with operation
3. Use Modal Features Effectively#
@Register.OPS.MODAL.GENERIC
class EffectiveModalOperator:
use_raycast_info = True
raycast_type = 'SCENE'
def modal_start(self, context, event):
# Initialize modal state
self.operation_data = {}
def modal_update(self, context, event, mouse):
# Handle main events
if event.type == 'ESC':
return OpsReturn.CANCEL
return None
def modal__mousemove(self, context, mouse):
# Handle mouse movement efficiently
if mouse.enough_movement: # Built-in movement threshold
# Update based on mouse movement
pass
def modal_finish(self, context, event):
# Clean up and finalize
print("Operation completed successfully")
4. Use Class-based Operator Calling#
@Register.OPS.GENERIC
class ModernOperator:
def action(self, context):
# Modern approach - cleaner and more maintainable
SomeHelperOperator.run(param="value")
# Instead of:
# bpy.ops.uvflow.some_helper_operator('INVOKE_DEFAULT', param="value")
Automatic Cleanup#
The operator registration system automatically handles:
Blender registration/unregistration
Property cleanup
UI integration cleanup
Modal state cleanup
Timer cleanup (for modal operators)
No manual cleanup is required in most cases.
This operator registration system provides a powerful, flexible foundation for creating all types of Blender operators with minimal boilerplate code and maximum functionality, while providing nice scalability as developers can create new operator types with ease and add them to the system.