Install
$ agentstack add skill-impertio-studio-blender-bonsai-ifcopenshell-sverchok-claude-skill-package-blender-impl-operators ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
blender-impl-operators
Quick Reference
Critical Warnings
ALWAYS clean up timers in BOTH cancel() AND the {'FINISHED'} path. Leaked timers fire indefinitely and reference deleted operator instances.
ALWAYS call wm.modal_handler_add(self) BEFORE returning {'RUNNING_MODAL'} from invoke(). Returning {'RUNNING_MODAL'} without a handler registration means events go nowhere.
ALWAYS handle ESC and RIGHTMOUSE in modal() to allow user cancellation. Without an escape path, the user is locked in the modal state.
ALWAYS set bl_options = {'REGISTER', 'UNDO'} for operators that modify scene data. 'UNDO' alone has NO effect — 'REGISTER' is required for undo to work.
NEVER modify bpy.data from a background thread. Use bpy.app.timers.register() with a queue.Queue to marshal work to the main thread.
NEVER expect viewport redraws during a synchronous execute(). The UI is frozen until execute() returns. Use a modal operator with timer for progressive visual updates.
NEVER use threading.Timer for deferred Blender operations. Use bpy.app.timers.register() instead.
Implementation Decision Tree
Need to implement a complex Blender operator?
│
├─ Long-running operation that must show progress?
│ └─ Use modal operator with timer (Section 1)
│ └─ Add wm.progress_begin/update/end (Section 5)
│ └─ Process items incrementally per TIMER event
│
├─ Need user to select a file?
│ └─ Use ImportHelper/ExportHelper mixins (Section 2)
│ └─ Or manual fileselect_add for custom file dialogs
│
├─ Need to process many objects in batch?
│ ├─ Fast operation ( 1 second)?
│ └─ Use modal timer + progress reporting (Section 1 + 5)
│
├─ Need multi-step user interaction?
│ ├─ Sequential clicks/points in viewport?
│ │ └─ Use state machine in modal() (Section 6)
│ └─ Parameter dialog before execution?
│ └─ Use invoke_props_dialog (Section 6)
│
├─ Need deferred execution outside an operator?
│ └─ Use bpy.app.timers.register() (Section 7)
│ └─ Return None to run once, return float to repeat
│
└─ Need undo support?
├─ Single operation?
│ └─ bl_options = {'REGISTER', 'UNDO'} (Section 3)
└─ Repeated rapid calls (e.g., timer-driven)?
└─ bl_options = {'REGISTER', 'UNDO_GROUPED'} (Section 3)
Version Compatibility Matrix
| Feature | Blender 3.x | Blender 4.0+ | Blender 5.x | |---------|-------------|--------------|-------------| | wm.event_timer_add(t, window=w) | Keyword arg | Keyword arg | Keyword arg | | bpy.app.timers.register() | Available | Available | Available | | context.temp_override() | Available from 3.2 | REQUIRED | REQUIRED | | Dict context override | Deprecated (3.2+) | REMOVED | REMOVED | | UILayout.progress() | Not available | Available (4.0+) | Available | | ImportHelper/ExportHelper | Stable | Stable | Stable | | UNDO_GROUPED bl_option | Available | Available | Available |
Section 1: Modal Operators with Timer Callbacks
When to Use
Use modal operators with timers when an operation takes more than ~0.5 seconds and must:
- Show progress to the user
- Allow cancellation mid-operation
- Keep Blender's UI responsive during processing
Implementation Pattern
import bpy
class MYCAT_OT_long_process(bpy.types.Operator):
"""Process objects over multiple frames with progress"""
bl_idname = "mycat.long_process"
bl_label = "Long Process"
bl_options = {'REGISTER', 'UNDO'}
_timer = None
_items: list = None
_index: int = 0
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def invoke(self, context, event):
self._items = list(context.selected_objects)
self._index = 0
wm = context.window_manager
wm.progress_begin(0, len(self._items))
self._timer = wm.event_timer_add(0.01, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}:
self._cleanup(context)
self.report({'INFO'}, "Processing cancelled")
return {'CANCELLED'}
if event.type == 'TIMER':
if self._index >= len(self._items):
self._cleanup(context)
self.report({'INFO'}, f"Processed {len(self._items)} items")
return {'FINISHED'}
obj = self._items[self._index]
self._process_item(context, obj)
self._index += 1
context.window_manager.progress_update(self._index)
return {'PASS_THROUGH'}
def _process_item(self, context, obj):
# Per-item work here
pass
def _cleanup(self, context):
wm = context.window_manager
wm.progress_end()
if self._timer is not None:
wm.event_timer_remove(self._timer)
self._timer = None
def cancel(self, context):
self._cleanup(context)
Rules
- Timer interval is a minimum — actual intervals may be longer due to Blender processing
event_timer_add()requireswindowas keyword:event_timer_add(0.1, window=context.window)- Return
{'PASS_THROUGH'}from TIMER events to let keyboard shortcuts work - Return
{'RUNNING_MODAL'}from TIMER events to block all other input - ALWAYS create a shared
_cleanup()method called from both FINISHED and CANCELLED paths
Section 2: File Browser Integration
Using ImportHelper / ExportHelper
import bpy
import os
from bpy_extras.io_utils import ImportHelper, ExportHelper
class MYCAT_OT_import_data(bpy.types.Operator, ImportHelper):
"""Import data from file"""
bl_idname = "mycat.import_data"
bl_label = "Import Data"
bl_options = {'REGISTER', 'UNDO'}
# ImportHelper provides: filepath, invoke()
filter_glob: bpy.props.StringProperty(
default="*.csv;*.json",
options={'HIDDEN'},
)
# Custom options shown in file browser sidebar
skip_header: bpy.props.BoolProperty(name="Skip Header", default=True)
def execute(self, context):
if not os.path.isfile(self.filepath):
self.report({'ERROR'}, f"File not found: {self.filepath}")
return {'CANCELLED'}
# Process self.filepath here
self.report({'INFO'}, f"Imported: {self.filepath}")
return {'FINISHED'}
ExportHelper Requirements
class MYCAT_OT_export_data(bpy.types.Operator, ExportHelper):
bl_idname = "mycat.export_data"
bl_label = "Export Data"
filename_ext = ".csv" # REQUIRED — ExportHelper.check() uses this
filter_glob: bpy.props.StringProperty(default="*.csv", options={'HIDDEN'})
def execute(self, context):
with open(self.filepath, 'w') as f:
f.write("data")
return {'FINISHED'}
Manual File Browser (without mixins)
Use filepath: StringProperty(subtype='FILE_PATH') or directory: StringProperty(subtype='DIR_PATH'), call context.window_manager.fileselect_add(self) in invoke(), return {'RUNNING_MODAL'}.
Menu Registration
Register import operators to bpy.types.TOPBAR_MT_file_import.append(menu_func). Register export operators to bpy.types.TOPBAR_MT_file_export.append(menu_func). ALWAYS remove in unregister() to prevent duplicate entries on addon reload.
Section 3: Undo/Redo Support
bl_options for Undo
| Option | Effect | |--------|--------| | {'REGISTER', 'UNDO'} | One undo step on {'FINISHED'}. ALWAYS use for data-modifying operators. | | {'REGISTER', 'UNDO_GROUPED'} | Consecutive calls of the same operator produce one undo step. Use for repeated rapid calls. | | {'REGISTER'} alone | NO undo step. Use only for read-only / reporting operators. |
Rules
'UNDO'requires'REGISTER'— without'REGISTER','UNDO'has NO effect- Returning
{'CANCELLED'}NEVER creates an undo step - Modal operators with
{'UNDO'}push ONE undo step when{'FINISHED'}is returned — all intermediate modifications are bundled - If
execute()modifies data then returns{'CANCELLED'}, those changes persist WITHOUT being undoable — ALWAYS restore state before cancelling UNDO_GROUPEDconsolidates consecutive calls of the SAME operator into one undo step — use for timer-driven modifications
Cancel-Safe Pattern
def execute(self, context):
obj = context.active_object
original_location = obj.location.copy()
try:
self._do_work(obj)
except Exception as e:
obj.location = original_location # Restore on failure
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
return {'FINISHED'}
Section 4: Batch Processing Operators
Synchronous Batch (fast operations)
class MYCAT_OT_batch_rename(bpy.types.Operator):
bl_idname = "mycat.batch_rename"
bl_label = "Batch Rename"
bl_options = {'REGISTER', 'UNDO'}
prefix: bpy.props.StringProperty(name="Prefix", default="obj_")
def execute(self, context):
objects = list(context.selected_objects) # Copy — NEVER iterate and modify
for i, obj in enumerate(objects):
obj.name = f"{self.prefix}{i:03d}"
self.report({'INFO'}, f"Renamed {len(objects)} objects")
return {'FINISHED'}
Batch with Context Override (Blender 4.0+)
def batch_apply_modifiers(objects):
"""Apply all modifiers on multiple objects."""
for obj in objects:
with bpy.context.temp_override(active_object=obj, object=obj):
for mod in obj.modifiers[:]: # Copy list — modifiers removed during iteration
try:
bpy.ops.object.modifier_apply(modifier=mod.name)
except RuntimeError as e:
print(f"Cannot apply {mod.name} on {obj.name}: {e}")
Rules
- ALWAYS copy collections before iterating if the loop modifies them:
list(context.selected_objects) - ALWAYS copy modifier lists before applying:
obj.modifiers[:] - Use
context.temp_override()when calling operators that checkcontext.active_object - For batch operations on 100+ objects, use the modal timer pattern (Section 1) for responsiveness
Section 5: Progress Reporting
WindowManager Progress API
wm = context.window_manager
wm.progress_begin(0, total_count) # Initialize range
wm.progress_update(current_index) # Update cursor indicator
wm.progress_end() # Finish — ALWAYS call, even on error
Header Text as Progress (alternative)
# In modal() during processing:
context.area.header_text_set(f"Processing: {i}/{total} ({i/total*100:.0f}%)")
# On completion or cancel: ALWAYS restore:
context.area.header_text_set(None)
UILayout.progress() (Blender 4.0+)
# In Panel.draw() or Operator.draw() only:
layout.progress(factor=0.66, type='BAR', text="66%")
Known Issue
wm.progress_end() does not immediately reset the cursor — it stays as a busy indicator until the user moves the cursor. This is a confirmed Blender bug, not a coding error.
Section 6: Multi-Step Workflows
Parameter Dialog Before Execution
class MYCAT_OT_configured_action(bpy.types.Operator):
bl_idname = "mycat.configured_action"
bl_label = "Configured Action"
bl_options = {'REGISTER', 'UNDO'}
count: bpy.props.IntProperty(name="Count", default=5, min=1, max=100)
mode: bpy.props.EnumProperty(
name="Mode",
items=[('ADD', "Add", ""), ('REPLACE', "Replace", "")],
)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=300)
def check(self, context):
return True # Redraw dialog when properties change
def draw(self, context):
layout = self.layout
layout.prop(self, "mode", expand=True)
layout.prop(self, "count")
def execute(self, context):
# Runs when user clicks OK
return {'FINISHED'}
State Machine Modal Operator
class MYCAT_OT_two_point_tool(bpy.types.Operator):
bl_idname = "mycat.two_point_tool"
bl_label = "Two Point Tool"
bl_options = {'REGISTER', 'UNDO'}
_state: str = 'PICK_FIRST'
_first_point: tuple = None
def invoke(self, context, event):
self._state = 'PICK_FIRST'
self._first_point = None
context.window_manager.modal_handler_add(self)
context.area.header_text_set("Click first point (ESC to cancel)")
return {'RUNNING_MODAL'}
def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}:
context.area.header_text_set(None)
return {'CANCELLED'}
if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
if self._state == 'PICK_FIRST':
self._first_point = (event.mouse_region_x, event.mouse_region_y)
self._state = 'PICK_SECOND'
context.area.header_text_set("Click second point (ESC to cancel)")
return {'RUNNING_MODAL'}
elif self._state == 'PICK_SECOND':
second = (event.mouse_region_x, event.mouse_region_y)
self._execute_action(context, self._first_point, second)
context.area.header_text_set(None)
return {'FINISHED'}
return {'PASS_THROUGH'}
def _execute_action(self, context, p1, p2):
pass # Implementation here
Invocation Methods Reference
| Method | Dialog | Calls execute() | Use Case | |--------|--------|-----------------|----------| | invoke_props_dialog(self, width) | Property form + OK | On OK click | Parameter gathering | | invoke_props_popup(self, event) | Property popup | Immediately + on change | Interactive adjustment | | invoke_confirm(self, event) | OK/Cancel only | On OK click | Destructive actions | | invoke_popup(self, width) | Custom draw only | NEVER | Info display | | invoke_search_popup(self) | Searchable enum | On selection | Enum with many items |
Section 7: Deferred Execution with bpy.app.timers
When to Use (instead of modal operators)
- Fire-and-forget deferred work (no UI interaction needed)
- Thread-safe bridge from background threads to main thread
- Persistent recurring tasks that survive file loads
API
bpy.app.timers.register(function, first_interval=0, persistent=False)
bpy.app.timers.unregister(function)
bpy.app.timers.is_registered(function) -> bool
Return Value Semantics
| Return | Effect | |--------|--------| | None | Timer unregistered — function never called again | | float | Function called again after that many seconds |
Thread-Safe Queue Pattern
import bpy
import queue
import threading
_queue = queue.Queue()
def _process_queue():
while not _queue.empty():
fn = _queue.get()
fn()
return 1.0 # Check every second
def run_on_main_thread(fn):
"""Schedule a function to run on Blender's main thread."""
_queue.put(fn)
# Register in addon register():
bpy.app.timers.register(_process_queue, persistent=True)
bpy.app.timers vs wm.eventtimeradd
| | bpy.app.timers | wm.event_timer_add | |--|-------------------|----------------------| | Requires modal operator | No | Yes | | Tied to specific window | No | Yes | | Survives file load | Yes (persistent=True) | No | | User-cancellable | No (must unregister) | Yes (ESC in modal) | | Use case | Background tasks, thread bridge | Interactive modal tools |
Reference Links
- [references/methods.md](references/methods.md) — Complete API signatures for modal, timer, progress, and file browser APIs
- [references/examples.md](references/examples.md) — Full working examples for all implementation patterns
- [references/anti-patterns.md](references/anti-patterns.md) — Implementation mistakes with explanations and fixes
Official Sources
- [Operator](https://
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Impertio-Studio
- Source: Impertio-Studio/Blender-Bonsai-ifcOpenshell-Sverchok-Claude-Skill-Package
- License: MIT
- Homepage: https://github.com/OpenAEC-Foundation
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.