AgentStack
SKILL verified MIT Self-run

Blender Impl Operators

skill-impertio-studio-blender-bonsai-ifcopenshell-sverchok-claude-skill-package-blender-impl-operators · by Impertio-Studio

>

No reviews yet
0 installs
21 views
0.0% view→install

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

✓ Passed

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

Are you the author of Blender Impl Operators? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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() requires window as 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

  1. 'UNDO' requires 'REGISTER' — without 'REGISTER', 'UNDO' has NO effect
  2. Returning {'CANCELLED'} NEVER creates an undo step
  3. Modal operators with {'UNDO'} push ONE undo step when {'FINISHED'} is returned — all intermediate modifications are bundled
  4. If execute() modifies data then returns {'CANCELLED'}, those changes persist WITHOUT being undoable — ALWAYS restore state before cancelling
  5. UNDO_GROUPED consolidates 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 check context.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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.