Blender | PY

Plugin Architecture

Build add-ons with modifier-like boolean and curves operations.

We provide a complete example add-on in python/examples/bpy-plugin/ demonstrating how to build Blender plugins with trueform. This page explains the modifier-like architecture pattern.

See Installation for pre-built downloads and building from source.

Overview

The plugin uses a modifier-like architecture where:

  • Settings are stored on result objects, not scene-level properties
  • Multiple boolean/curves operations can exist simultaneously
  • Each result tracks its source objects and updates independently
  • Live updates happen per-result when sources change

This mirrors how Blender's native modifiers work—select the result object to see and edit its settings.

Performance Note: While trueform operations are highly optimized, transferring geometry to Blender via foreach_set accounts for roughly 65% of total boolean operation time. This is a Blender API limitation.

Architecture Pattern

Object-Level Properties

Store modifier data on result objects using a PropertyGroup:

class TrueformBooleanModifier(bpy.types.PropertyGroup):
    source_a: bpy.props.PointerProperty(
        name="Source A",
        type=bpy.types.Object,
        poll=lambda s, o: o.type == 'MESH',
        update=_on_modifier_update
    )
    source_b: bpy.props.PointerProperty(
        name="Source B",
        type=bpy.types.Object,
        poll=lambda s, o: o.type == 'MESH',
        update=_on_modifier_update
    )
    operation: bpy.props.EnumProperty(
        name="Operation",
        items=[
            ('DIFFERENCE', "Difference", "A - B"),
            ('UNION', "Union", "A ∪ B"),
            ('INTERSECTION', "Intersection", "A ∩ B"),
        ],
        update=_on_modifier_update
    )
    live: bpy.props.BoolProperty(
        name="Live",
        default=True,
        update=_on_live_toggle
    )

# Register on Object, not Scene
bpy.types.Object.trueform_boolean = bpy.props.PointerProperty(
    type=TrueformBooleanModifier
)

Scene-Level Properties (Creation UI)

Use scene-level properties only for the creation interface:

class TrueformBooleanCreateProps(bpy.types.PropertyGroup):
    target_a: bpy.props.PointerProperty(
        name="Mesh A",
        type=bpy.types.Object,
        poll=lambda s, o: o.type == 'MESH'
    )
    target_b: bpy.props.PointerProperty(
        name="Mesh B",
        type=bpy.types.Object,
        poll=lambda s, o: o.type == 'MESH'
    )
    operation: bpy.props.EnumProperty(...)

bpy.types.Scene.trueform_boolean_create = bpy.props.PointerProperty(
    type=TrueformBooleanCreateProps
)

Dual-Mode Panel

The panel switches between creation mode and modifier mode based on the active object.

Detection Function

def _is_trueform_result(obj) -> bool:
    """Check if object is a Trueform boolean result."""
    if not obj or obj.type != 'MESH':
        return False
    mod = getattr(obj, 'trueform_boolean', None)
    return mod is not None and mod.source_a is not None

Panel Implementation

class VIEW3D_PT_trueform_boolean(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Trueform'
    bl_label = "Boolean"

    def draw(self, context):
        layout = self.layout
        obj = context.active_object

        if obj and _is_trueform_result(obj):
            # MODIFIER MODE: show result's settings
            mod = obj.trueform_boolean

            if not mod.source_a or not mod.source_b:
                layout.label(text="Source missing!", icon='ERROR')

            col = layout.column(align=True)
            col.prop(mod, "source_a")
            col.prop(mod, "source_b")

            layout.prop(mod, "operation", expand=True)
            layout.prop(mod, "live")

            row = layout.row(align=True)
            row.operator("mesh.trueform_boolean_refresh", icon='FILE_REFRESH')
            row.operator("mesh.trueform_boolean_remove", icon='X', text="Remove")
        else:
            # CREATE MODE: show creation interface
            props = context.scene.trueform_boolean_create

            col = layout.column(align=True)
            col.prop(props, "target_a")
            col.prop(props, "target_b")

            layout.prop(props, "operation", expand=True)
            layout.operator("mesh.trueform_boolean_create", icon='MOD_BOOLEAN')

Live Update System

Finding Live Results

def _get_live_results():
    """Get all result objects with live=True."""
    results = []
    for obj in bpy.data.objects:
        if obj.type != 'MESH':
            continue
        mod = getattr(obj, 'trueform_boolean', None)
        if mod and mod.live and mod.source_a and mod.source_b:
            results.append(obj)
    return results

Handler Registration

Only register handlers when live results exist:

def _ensure_handlers():
    """Add handlers if any live results exist."""
    if _get_live_results():
        if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
            bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update)
        if _on_frame_change not in bpy.app.handlers.frame_change_post:
            bpy.app.handlers.frame_change_post.append(_on_frame_change)
    else:
        _remove_handlers()

def _remove_handlers():
    """Remove handlers."""
    if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update)
    if _on_frame_change in bpy.app.handlers.frame_change_post:
        bpy.app.handlers.frame_change_post.remove(_on_frame_change)

Efficient Update Detection

Check if source objects changed before recomputing:

def _on_depsgraph_update(scene, depsgraph):
    """Handle depsgraph updates - update live results when sources change."""
    live_results = _get_live_results()
    if not live_results:
        return

    for result_obj in live_results:
        mod = result_obj.trueform_boolean
        targets = {mod.source_a, mod.source_b}
        target_data = {t.data for t in targets if t and t.data}

        for upd in depsgraph.updates:
            upd_id = getattr(upd.id, 'original', upd.id)
            if upd_id in targets:
                _update_result(result_obj)
                break
            if hasattr(upd.id, 'data') and upd.id.data in target_data:
                _update_result(result_obj)
                break

In-Place Updates

Use update_blender() to update geometry without recreating the object:

def _update_result(result_obj):
    """Update a single result object from its sources."""
    mod = result_obj.trueform_boolean
    if not mod or not mod.source_a or not mod.source_b:
        return

    tf, tfb = core.get_tf_libs()

    op_map = {
        'DIFFERENCE': tfb.scene.boolean_difference_mesh,
        'UNION': tfb.scene.boolean_union_mesh,
        'INTERSECTION': tfb.scene.boolean_intersection_mesh
    }
    result_mesh = op_map[mod.operation](mod.source_a, mod.source_b)
    tfb.convert.update_blender(result_mesh, result_obj)

This preserves object name, materials, display settings, and custom properties.

Operators

Create Operator

class MESH_OT_trueform_boolean_create(bpy.types.Operator):
    """Create a new Trueform boolean from two meshes"""
    bl_idname = "mesh.trueform_boolean_create"
    bl_label = "Create Boolean"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.trueform_boolean_create

        if not props.target_a or not props.target_b:
            self.report({'WARNING'}, "Select two meshes")
            return {'CANCELLED'}

        if props.target_a == props.target_b:
            self.report({'WARNING'}, "Select two different meshes")
            return {'CANCELLED'}

        tf, tfb = core.get_tf_libs()

        # Compute initial result
        op_map = {
            'DIFFERENCE': tfb.scene.boolean_difference_mesh,
            'UNION': tfb.scene.boolean_union_mesh,
            'INTERSECTION': tfb.scene.boolean_intersection_mesh
        }
        result_mesh = op_map[props.operation](props.target_a, props.target_b)
        result_obj = tfb.convert.to_blender(result_mesh, name=f"TF_{props.target_a.name}")

        # Store modifier data on result
        mod = result_obj.trueform_boolean
        mod.source_a = props.target_a
        mod.source_b = props.target_b
        mod.operation = props.operation
        mod.live = True

        # Register handlers
        _ensure_handlers()

        # Select result
        bpy.ops.object.select_all(action='DESELECT')
        result_obj.select_set(True)
        context.view_layer.objects.active = result_obj

        return {'FINISHED'}

Refresh Operator

class MESH_OT_trueform_boolean_refresh(bpy.types.Operator):
    """Force refresh the boolean result"""
    bl_idname = "mesh.trueform_boolean_refresh"
    bl_label = "Refresh"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.active_object and _is_trueform_result(context.active_object)

    def execute(self, context):
        _update_result(context.active_object)
        return {'FINISHED'}

Remove Operator

class MESH_OT_trueform_boolean_remove(bpy.types.Operator):
    """Remove Trueform boolean modifier, keeping result as static mesh"""
    bl_idname = "mesh.trueform_boolean_remove"
    bl_label = "Remove Modifier"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.active_object and _is_trueform_result(context.active_object)

    def execute(self, context):
        mod = context.active_object.trueform_boolean

        # Clear modifier data
        mod.source_a = None
        mod.source_b = None
        mod.live = False

        # Update handlers
        _ensure_handlers()

        return {'FINISHED'}

Intersection Curves Tool

The same pattern applies to intersection curves:

class TrueformCurvesModifier(bpy.types.PropertyGroup):
    source_a: bpy.props.PointerProperty(...)
    source_b: bpy.props.PointerProperty(...)
    live: bpy.props.BoolProperty(default=True, ...)

bpy.types.Object.trueform_curves = bpy.props.PointerProperty(
    type=TrueformCurvesModifier
)

The update function uses update_curves() for efficient in-place updates:

def _update_curves_result(result_obj):
    mod = result_obj.trueform_curves
    tf, tfb = core.get_tf_libs()

    mesh_a = tfb.scene.get(mod.source_a)
    mesh_b = tfb.scene.get(mod.source_b)
    paths, points = tf.intersection_curves(mesh_a, mesh_b)

    if paths:
        tfb.convert.update_curves(paths, points, result_obj)
    else:
        result_obj.data.splines.clear()

Performance

The scene module's caching minimizes redundant computation:

  1. First access: Mesh is converted and structures are built
  2. Subsequent access: Cached mesh is returned immediately
  3. On geometry change: Mesh is marked dirty (not rebuilt yet)
  4. Next access after change: Mesh is rebuilt lazily
See Scene for API details and Convert for standalone script usage.