"""
Blender Selection Handlers
This module contains handlers for mesh selection operations in Blender.
Includes selecting non-manifold geometry and other selection utilities.
"""
import bpy
import bmesh
def select_non_manifold(wire, boundaries, multiple_faces, non_contiguous):
"""Select non-manifold mesh elements"""
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')
# Deselect all first
bpy.ops.mesh.select_all(action='DESELECT')
# Select non-manifold
bpy.ops.mesh.select_non_manifold(
extend=False,
use_wire=wire,
use_boundary=boundaries,
use_multi_face=multiple_faces,
use_non_contiguous=non_contiguous
)
# Count selected elements
bm = bmesh.from_edit_mesh(active_obj.data)
verts_selected = len([v for v in bm.verts if v.select])
edges_selected = len([e for e in bm.edges if e.select])
bm.free()
return {
"success": True,
"vertices_selected": verts_selected,
"edges_selected": edges_selected
}
except Exception as e:
return {"error": f"Failed to select non-manifold: {str(e)}"}
def select_by_valence(object_name=None, min_valence=5):
"""
Select vertices with valence (edge count) >= threshold.
Parameters:
- object_name: Name of object to select in (default: active object)
- min_valence: Minimum valence to select (default: 5)
Returns selection count.
"""
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')
# Deselect all first
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.select_mode(type='VERT')
# Get bmesh
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
# Select vertices with valence >= min_valence
selection_count = 0
for vert in bm.verts:
if len(vert.link_edges) >= min_valence:
vert.select = True
selection_count += 1
bmesh.update_edit_mesh(obj.data)
return {
"success": True,
"object_name": obj.name,
"min_valence": min_valence,
"selection_count": selection_count
}
except Exception as e:
return {"error": f"Failed to select by valence: {str(e)}"}
def select_triangles(object_name=None):
"""
Select all triangle faces (3 vertices) in the mesh.
Parameters:
- object_name: Name of object to select in (default: active object)
Returns selection count.
"""
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')
# Deselect all first
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.select_mode(type='FACE')
# Get bmesh
bm = bmesh.from_edit_mesh(obj.data)
bm.faces.ensure_lookup_table()
# Select triangle faces
selection_count = 0
for face in bm.faces:
if len(face.verts) == 3:
face.select = True
selection_count += 1
bmesh.update_edit_mesh(obj.data)
return {
"success": True,
"object_name": obj.name,
"selection_count": selection_count
}
except Exception as e:
return {"error": f"Failed to select triangles: {str(e)}"}
def select_ngons(object_name=None):
"""
Select all n-gon faces (5+ vertices) in the mesh.
Parameters:
- object_name: Name of object to select in (default: active object)
Returns selection count.
"""
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"}
# Ensure Object Mode first to sync mesh data
if bpy.context.mode == 'EDIT_MESH':
bpy.ops.object.mode_set(mode='OBJECT')
# Force mesh data update
obj.data.update()
# Enter Edit Mode
bpy.ops.object.mode_set(mode='EDIT')
# Deselect all first
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.select_mode(type='FACE')
# Get fresh bmesh after mode switch
bm = bmesh.from_edit_mesh(obj.data)
bm.faces.ensure_lookup_table()
# Select n-gon faces
selection_count = 0
for face in bm.faces:
if len(face.verts) > 4:
face.select = True
selection_count += 1
bmesh.update_edit_mesh(obj.data)
return {
"success": True,
"object_name": obj.name,
"selection_count": selection_count
}
except Exception as e:
return {"error": f"Failed to select n-gons: {str(e)}"}
def select_edge_loop(object_name=None, edge_index=0):
"""
Select the edge loop containing the specified edge.
Parameters:
- object_name: Name of object to select in (default: active object)
- edge_index: Index of edge to start loop selection from
Returns selection count.
"""
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
bm = bmesh.from_edit_mesh(obj.data)
bm.edges.ensure_lookup_table()
# Validate edge index
if edge_index < 0 or edge_index >= len(bm.edges):
return {"error": f"Edge index {edge_index} out of range (0-{len(bm.edges)-1})"}
# Deselect all first
for e in bm.edges:
e.select = False
for v in bm.verts:
v.select = False
for f in bm.faces:
f.select = False
bpy.ops.mesh.select_mode(type='EDGE')
# Walk the edge loop using bmesh
start_edge = bm.edges[edge_index]
loop_edges = _walk_edge_loop(start_edge)
# Select all edges in the loop
for edge in loop_edges:
edge.select = True
bmesh.update_edit_mesh(obj.data)
return {
"success": True,
"object_name": obj.name,
"start_edge": edge_index,
"selection_count": len(loop_edges)
}
except Exception as e:
return {"error": f"Failed to select edge loop: {str(e)}"}
def _walk_edge_loop(start_edge):
"""
Walk an edge loop starting from the given edge.
Returns a set of all edges in the loop.
"""
loop_edges = set()
loop_edges.add(start_edge)
# Walk in both directions from the starting edge
for vert in start_edge.verts:
_walk_edge_loop_direction(start_edge, vert, loop_edges)
return loop_edges
def _walk_edge_loop_direction(current_edge, current_vert, loop_edges):
"""
Walk edge loop in one direction from current_edge through current_vert.
An edge loop continues through a vertex if that vertex has exactly 4 edges (valence 4).
"""
visited_verts = {current_edge.other_vert(current_vert)}
while True:
# Get linked edges at current vertex
linked_edges = current_vert.link_edges
# For edge loop, we need valence 4 vertex
if len(linked_edges) != 4:
break
# Find the edge across from current_edge (opposite edge in the quad)
next_edge = None
for edge in linked_edges:
if edge == current_edge:
continue
if edge in loop_edges:
continue
# Check if this is the "across" edge (shares no faces with current_edge)
shared_faces = set(current_edge.link_faces) & set(edge.link_faces)
if len(shared_faces) == 0:
next_edge = edge
break
if next_edge is None:
break
# Add to loop
loop_edges.add(next_edge)
# Move to next vertex
next_vert = next_edge.other_vert(current_vert)
# Check for loop closure
if next_vert in visited_verts:
break
visited_verts.add(current_vert)
current_edge = next_edge
current_vert = next_vert
def select_by_criteria(object_name=None, criteria='valence', min_value=None, max_value=None):
"""
Select mesh elements by flexible criteria.
Parameters:
- object_name: Name of object (default: active object)
- criteria: Selection criteria type:
- 'valence': Select vertices by edge count
- 'face_sides': Select faces by vertex count
- 'boundary': Select boundary edges
- 'area': Select faces by area
- 'non_manifold': Select non-manifold geometry
- min_value: Minimum value for range criteria (optional)
- max_value: Maximum value for range criteria (optional)
Returns selection count.
"""
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')
# Deselect all first
bpy.ops.mesh.select_all(action='DESELECT')
# Get bmesh
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
selection_count = 0
if criteria == 'valence':
bpy.ops.mesh.select_mode(type='VERT')
min_v = min_value if min_value is not None else 0
max_v = max_value if max_value is not None else 999
for vert in bm.verts:
valence = len(vert.link_edges)
if min_v <= valence <= max_v:
vert.select = True
selection_count += 1
elif criteria == 'face_sides':
bpy.ops.mesh.select_mode(type='FACE')
min_s = min_value if min_value is not None else 0
max_s = max_value if max_value is not None else 999
for face in bm.faces:
sides = len(face.verts)
if min_s <= sides <= max_s:
face.select = True
selection_count += 1
elif criteria == 'boundary':
bpy.ops.mesh.select_mode(type='EDGE')
for edge in bm.edges:
if edge.is_boundary:
edge.select = True
selection_count += 1
elif criteria == 'area':
bpy.ops.mesh.select_mode(type='FACE')
min_a = min_value if min_value is not None else 0
max_a = max_value if max_value is not None else float('inf')
for face in bm.faces:
area = face.calc_area()
if min_a <= area <= max_a:
face.select = True
selection_count += 1
elif criteria == 'non_manifold':
bpy.ops.mesh.select_mode(type='EDGE')
for edge in bm.edges:
if not edge.is_manifold:
edge.select = True
selection_count += 1
else:
return {"error": f"Invalid criteria '{criteria}'. Valid: valence, face_sides, boundary, area, non_manifold"}
bmesh.update_edit_mesh(obj.data)
return {
"success": True,
"object_name": obj.name,
"criteria": criteria,
"selection_count": selection_count
}
except Exception as e:
return {"error": f"Failed to select by criteria: {str(e)}"}
def get_selected_elements(object_name=None, mode='VERT'):
"""
Get list of selected mesh elements with detailed data.
Parameters:
- object_name: Name of object (default: active object)
- mode: 'VERT' | 'EDGE' | 'FACE'
Returns: {count, elements: [{index, co/verts/area, ...}]}
"""
try:
# Validate mode
valid_modes = ['VERT', 'EDGE', 'FACE']
if mode not in valid_modes:
return {"error": f"Invalid mode '{mode}'. Must be one of: {valid_modes}"}
# 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 not already
if bpy.context.mode != 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
# Get bmesh
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
elements = []
if mode == 'VERT':
for v in bm.verts:
if v.select:
elements.append({
"index": v.index,
"co": list(v.co),
"valence": len(v.link_edges),
"normal": list(v.normal)
})
elif mode == 'EDGE':
for e in bm.edges:
if e.select:
elements.append({
"index": e.index,
"verts": [e.verts[0].index, e.verts[1].index],
"length": e.calc_length(),
"is_boundary": e.is_boundary,
"face_count": len(e.link_faces)
})
elif mode == 'FACE':
for f in bm.faces:
if f.select:
elements.append({
"index": f.index,
"verts": [v.index for v in f.verts],
"vert_count": len(f.verts),
"area": f.calc_area(),
"center": list(f.calc_center_median()),
"normal": list(f.normal)
})
return {
"success": True,
"object_name": obj.name,
"mode": mode,
"count": len(elements),
"elements": elements
}
except Exception as e:
return {"error": f"Failed to get selected elements: {str(e)}"}