"""
Topology Cleanup Tools
MCP tools for cleaning up mesh topology issues.
"""
from mcp.server.fastmcp import Context
import logging
logger = logging.getLogger("BlenderMCPServer")
def tris_to_quads(
ctx: Context,
blender_connection,
object_name: str = None,
angle_threshold: float = 40.0
) -> str:
"""
Convert triangle pairs to quads.
Parameters:
- object_name: Name of object (default: active object)
- angle_threshold: Maximum angle between face normals for merging (degrees)
Returns converted count and remaining triangles.
"""
try:
params = {"angle_threshold": angle_threshold}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("tris_to_quads", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Tris to Quads: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Angle threshold: {result.get('angle_threshold', 40)}°\n"
output += f"Triangles before: {result.get('tris_before', 0)}\n"
output += f"Triangles after: {result.get('tris_after', 0)}\n"
output += f"Quad pairs created: {result.get('converted', 0)}\n"
if result.get('tris_after', 0) > 0:
output += f"\nRemaining triangles may need manual attention.\n"
else:
output += f"\nAll triangles converted to quads!\n"
return output
except Exception as e:
logger.error(f"Error converting tris to quads: {str(e)}")
return f"Error converting tris to quads: {str(e)}"
def align_vertex_to_loop(
ctx: Context,
blender_connection,
object_name: str = None,
vertex_index: int = 0,
axis: str = "Y",
target_coord: float = 0.0
) -> str:
"""
Align a vertex coordinate to match edge loop position.
Parameters:
- object_name: Name of object (default: active object)
- vertex_index: Index of vertex to align
- axis: Axis to align ("X", "Y", or "Z")
- target_coord: Target coordinate value
Returns the new vertex position.
"""
try:
params = {
"vertex_index": vertex_index,
"axis": axis,
"target_coord": target_coord
}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("align_vertex_to_loop", params)
if "error" in result:
return f"Error: {result['error']}"
old_pos = result.get('old_position', {})
new_pos = result.get('new_position', {})
output = f"Vertex Alignment: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Vertex {result.get('vertex_index', 0)}:\n"
output += f" Old position: ({old_pos.get('x', 0):.4f}, {old_pos.get('y', 0):.4f}, {old_pos.get('z', 0):.4f})\n"
output += f" New position: ({new_pos.get('x', 0):.4f}, {new_pos.get('y', 0):.4f}, {new_pos.get('z', 0):.4f})\n"
output += f" Aligned {result.get('axis', 'Y')} to {target_coord}\n"
return output
except Exception as e:
logger.error(f"Error aligning vertex: {str(e)}")
return f"Error aligning vertex: {str(e)}"
def dissolve_edge_loop_by_selection(
ctx: Context,
blender_connection,
object_name: str = None
) -> str:
"""
Dissolve currently selected edge loop.
The edge loop must be pre-selected in Edit Mode.
Use select_edge_loop first to select the loop.
Parameters:
- object_name: Name of object (default: active object)
Returns success status and edge count.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("dissolve_edge_loop_by_selection", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Edge Loop Dissolution: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Edges dissolved: {result.get('edges_dissolved', 0)}\n"
output += f"Faces before: {result.get('faces_before', 0)}\n"
output += f"Faces after: {result.get('faces_after', 0)}\n"
if result.get('ngons_created', 0) > 0:
output += f"\nWARNING: {result.get('ngons_created')} n-gons were created!\n"
output += "Consider using tris_to_quads or manual fixes.\n"
else:
output += "\nEdge loop dissolved cleanly with no n-gons.\n"
return output
except Exception as e:
logger.error(f"Error dissolving edge loop: {str(e)}")
return f"Error dissolving edge loop: {str(e)}"
def reduce_vertex_valence(
ctx: Context,
blender_connection,
object_name: str = None,
vertex_index: int = 0,
target_valence: int = 4
) -> str:
"""
Attempt to reduce vertex valence by collapsing nearby edges.
Note: This is a complex operation that may not always succeed.
It's often better to manually fix high-valence vertices.
Parameters:
- object_name: Name of object (default: active object)
- vertex_index: Index of vertex to reduce
- target_valence: Target valence (minimum 3)
Returns success status.
"""
try:
params = {
"vertex_index": vertex_index,
"target_valence": target_valence
}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("reduce_vertex_valence", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Vertex Valence Reduction: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Original vertex: {result.get('original_vertex_index', 0)}\n"
output += f"Original valence: {result.get('original_valence', 0)}\n"
output += f"Target valence: {result.get('target_valence', 4)}\n"
if result.get('message'):
output += f"\n{result.get('message')}\n"
else:
output += f"Edges collapsed: {result.get('edges_collapsed', 0)}\n"
if result.get('note'):
output += f"\nNote: {result.get('note')}\n"
return output
except Exception as e:
logger.error(f"Error reducing vertex valence: {str(e)}")
return f"Error reducing vertex valence: {str(e)}"
def fix_ngons(
ctx: Context,
blender_connection,
object_name: str = None,
method: str = "triangulate",
select_all: bool = True
) -> str:
"""
Convert n-gon faces (5+ vertices) to quads or triangles.
Parameters:
- object_name: Name of object (default: active object)
- method: Conversion method - 'triangulate', 'poke', 'fan' (default: 'triangulate')
- select_all: Process all n-gons in mesh (default: true)
Returns count of converted n-gons.
"""
try:
valid_methods = ['triangulate', 'poke', 'fan']
if method not in valid_methods:
return f"Error: Invalid method '{method}'. Must be one of {valid_methods}."
params = {
"method": method,
"select_all": select_all
}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("fix_ngons", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Fix N-gons: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Method: {method}\n"
output += f"N-gons before: {result.get('ngons_before', 0)}\n"
output += f"N-gons after: {result.get('ngons_after', 0)}\n"
output += f"N-gons fixed: {result.get('ngons_fixed', 0)}\n"
if result.get('message'):
output += f"\n{result.get('message')}\n"
elif result.get('ngons_after', 0) == 0:
output += "\nAll n-gons successfully converted!\n"
else:
output += f"\n{result.get('ngons_after')} n-gons remain.\n"
return output
except Exception as e:
logger.error(f"Error fixing n-gons: {str(e)}")
return f"Error fixing n-gons: {str(e)}"