Plugin Architecture
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.
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.
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:
- First access: Mesh is converted and structures are built
- Subsequent access: Cached mesh is returned immediately
- On geometry change: Mesh is marked dirty (not rebuilt yet)
- Next access after change: Mesh is rebuilt lazily
