"""
Blender Mesh Operations Handlers
This module contains handlers for various mesh editing operations in Blender.
Includes symmetrize, smooth, normals, merge, delete loose, dissolve, and knife project operations.
"""
import bpy
import bmesh
def symmetrize_mesh(direction, threshold):
"""Enforce mesh symmetry by mirroring one side"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Must be in Edit Mode
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.symmetrize(direction=direction, threshold=threshold)
# Count vertices for feedback
bm = bmesh.from_edit_mesh(active_obj.data)
vert_count = len(bm.verts)
bm.free()
return {
"success": True,
"vertices_created": vert_count
}
except Exception as e:
return {"error": f"Failed to symmetrize mesh: {str(e)}"}
def smooth_vertices(iterations, factor, axes):
"""Smooth vertices by averaging neighbor positions"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Must be in Edit Mode
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
# Convert axes string to boolean flags
use_x = 'X' in axes
use_y = 'Y' in axes
use_z = 'Z' in axes
# Apply smoothing
for _ in range(iterations):
bpy.ops.mesh.vertices_smooth(
factor=factor,
use_x=use_x,
use_y=use_y,
use_z=use_z
)
# Count affected vertices
bm = bmesh.from_edit_mesh(active_obj.data)
selected_verts = len([v for v in bm.verts if v.select])
bm.free()
return {
"success": True,
"vertices_affected": selected_verts
}
except Exception as e:
return {"error": f"Failed to smooth vertices: {str(e)}"}
def recalculate_normals(inside):
"""Recalculate face normals for consistent orientation"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Must be in Edit Mode
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.normals_make_consistent(inside=inside)
return {"success": True}
except Exception as e:
return {"error": f"Failed to recalculate normals: {str(e)}"}
def merge_by_distance(distance, unselected):
"""Merge vertices closer than specified distance"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Must be in Edit Mode
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
# Count vertices before
bm = bmesh.from_edit_mesh(active_obj.data)
verts_before = len(bm.verts)
bm.free()
# Merge by distance
if unselected:
bpy.ops.mesh.select_all(action='SELECT')
result = bpy.ops.mesh.remove_doubles(threshold=distance)
# Count vertices after
bm = bmesh.from_edit_mesh(active_obj.data)
verts_after = len(bm.verts)
bm.free()
return {
"success": True,
"vertices_removed": verts_before - verts_after
}
except Exception as e:
return {"error": f"Failed to merge by distance: {str(e)}"}
def delete_loose(delete_faces):
"""Delete loose vertices and edges"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Must be in Edit Mode
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.delete_loose(use_verts=True, use_edges=True, use_faces=delete_faces)
return {"success": True}
except Exception as e:
return {"error": f"Failed to delete loose geometry: {str(e)}"}
def dissolve_edge_loops():
"""Dissolve selected edge loops without leaving holes"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Must be in Edit Mode
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.dissolve_edges(use_verts=True, use_face_split=False)
return {"success": True}
except Exception as e:
return {"error": f"Failed to dissolve edge loops: {str(e)}"}
def dissolve_selected(object_name=None, mode='VERT', use_verts=True, use_face_split=False):
"""
Dissolve selected mesh elements without creating holes.
Parameters:
- object_name: Name of object (default: active object)
- mode: 'VERT' | 'EDGE' | 'FACE'
- use_verts: For edge dissolve, also dissolve verts (default: True)
- use_face_split: Split non-planar faces (default: False)
Returns count of dissolved elements.
"""
try:
# Get target object
if object_name:
obj = bpy.data.objects.get(object_name)
if not obj:
return {"error": f"Object '{object_name}' not found"}
bpy.context.view_layer.objects.active = obj
else:
obj = bpy.context.active_object
if not obj or obj.type != 'MESH':
return {"error": "No valid mesh object selected"}
# Enter Edit Mode
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
# Get bmesh to count selected before
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
# Count selected elements
if mode == 'VERT':
selected_count = sum(1 for v in bm.verts if v.select)
elif mode == 'EDGE':
selected_count = sum(1 for e in bm.edges if e.select)
elif mode == 'FACE':
selected_count = sum(1 for f in bm.faces if f.select)
else:
return {"error": f"Invalid mode '{mode}'. Must be VERT, EDGE, or FACE"}
if selected_count == 0:
return {"error": "No elements selected to dissolve"}
# Perform dissolve based on mode
if mode == 'VERT':
bpy.ops.mesh.dissolve_verts()
elif mode == 'EDGE':
bpy.ops.mesh.dissolve_edges(use_verts=use_verts, use_face_split=use_face_split)
elif mode == 'FACE':
bpy.ops.mesh.dissolve_faces(use_verts=use_verts)
# Update bmesh and count remaining
bmesh.update_edit_mesh(obj.data)
return {
"success": True,
"object_name": obj.name,
"mode": mode,
"dissolved_count": selected_count
}
except Exception as e:
return {"error": f"Failed to dissolve: {str(e)}"}
def knife_project(cutter_object, cut_through):
"""Project cutter object outline to cut the mesh"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Find cutter object
cutter_obj = bpy.data.objects.get(cutter_object)
if not cutter_obj:
return {"error": f"Cutter object '{cutter_object}' not found"}
# Must be in Edit Mode
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
# Select the cutter object
cutter_obj.select_set(True)
# Perform knife project
bpy.ops.mesh.knife_project(cut_through=cut_through)
return {"success": True}
except Exception as e:
return {"error": f"Failed to knife project: {str(e)}"}