"""
Topology Analysis Tools
Advanced topology analysis tools for retopology quality assessment.
Provides semantic metrics beyond simple polygon counting.
"""
from mcp.server.fastmcp import Context
import logging
logger = logging.getLogger("BlenderMCPServer")
def get_topology_quality(ctx: Context, blender_connection, object_name: str = None) -> str:
"""
Get detailed topology quality metrics for retopology evaluation.
Returns semantic metrics like quad percentage, pole distribution,
aspect ratios, and an overall quality score (0-100).
Parameters:
- object_name: Name of object to analyze (default: active object)
Returns a detailed quality report with actionable metrics.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("get_topology_quality", params)
if "error" in result:
return f"Error: {result['error']}"
# Format the quality report
output = f"Topology Quality Report: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
# Face composition
output += "FACE COMPOSITION:\n"
output += f" Total Faces: {result.get('total_faces', 0)}\n"
output += f" Quads: {result.get('quad_count', 0)} ({result.get('quad_percentage', 0):.1f}%)\n"
output += f" Triangles: {result.get('tri_count', 0)} ({result.get('tri_percentage', 0):.1f}%)\n"
output += f" N-gons: {result.get('ngon_count', 0)} ({result.get('ngon_percentage', 0):.1f}%)\n\n"
# Vertex valence
output += "VERTEX VALENCE:\n"
output += f" Total Vertices: {result.get('total_vertices', 0)}\n"
valence = result.get('valence_distribution', {})
output += f" Valence 3 (E-poles): {valence.get(3, 0)}\n"
output += f" Valence 4 (regular): {valence.get(4, 0)} ({result.get('valence_4_ratio', 0):.1f}%)\n"
output += f" Valence 5 (N-poles): {valence.get(5, 0)}\n"
output += f" Valence 6+: {valence.get('6+', 0)}\n"
output += f" Total Poles: {result.get('pole_count', 0)}\n\n"
# Aspect ratio
output += "FACE QUALITY:\n"
output += f" Avg Aspect Ratio: {result.get('avg_aspect_ratio', 1.0):.2f}\n"
output += f" Elongated Faces (>4:1): {result.get('bad_aspect_count', 0)}\n\n"
# Overall score
score = result.get('quality_score', 0)
if score >= 85:
grade = "A - Excellent"
elif score >= 70:
grade = "B - Good"
elif score >= 55:
grade = "C - Acceptable"
elif score >= 40:
grade = "D - Needs Work"
else:
grade = "F - Poor"
output += "OVERALL QUALITY:\n"
output += f" Score: {score:.1f}/100\n"
output += f" Grade: {grade}\n\n"
# Recommendations
output += "RECOMMENDATIONS:\n"
if result.get('ngon_percentage', 0) > 0:
output += " - Convert n-gons to quads (use tris_to_quads or manual)\n"
if result.get('quad_percentage', 0) < 85:
output += " - Increase quad ratio (target: 85%+)\n"
if result.get('pole_count', 0) > result.get('total_vertices', 1) * 0.2:
output += " - Reduce pole count by improving edge flow\n"
if result.get('avg_aspect_ratio', 1) > 3:
output += " - Improve face proportions with relax_vertices\n"
if score >= 85:
output += " - Mesh is ready for production!\n"
return output
except Exception as e:
logger.error(f"Error getting topology quality: {str(e)}")
return f"Error getting topology quality: {str(e)}"
def analyze_mesh_regions(ctx: Context, blender_connection, object_name: str = None) -> str:
"""
Analyze mesh by dividing into spatial regions for localized quality assessment.
Identifies problem areas in the mesh that need attention.
Parameters:
- object_name: Name of object to analyze (default: active object)
Returns a regional breakdown with quality metrics per area.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("analyze_mesh_regions", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Regional Analysis: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
regions = result.get('regions', [])
problem_zones = result.get('problem_zones', [])
output += f"Total Regions: {len(regions)}\n"
output += f"Problem Zones: {len(problem_zones)}\n\n"
for region in regions:
region_id = region.get('id', 0)
is_problem = region_id in problem_zones
marker = " [PROBLEM]" if is_problem else ""
output += f"Region {region_id}{marker}:\n"
output += f" Center: ({region['center'][0]:.2f}, {region['center'][1]:.2f}, {region['center'][2]:.2f})\n"
output += f" Faces: {region.get('face_count', 0)}\n"
output += f" Type: {region.get('dominant_type', 'unknown')}\n"
output += f" Quad %: {region.get('quad_percentage', 0):.1f}%\n"
output += f" Quality: {region.get('quality_score', 0):.1f}/100\n"
issues = region.get('issues', [])
if issues:
output += f" Issues: {', '.join(issues)}\n"
output += "\n"
if problem_zones:
output += "FOCUS AREAS:\n"
output += f" Regions needing attention: {problem_zones}\n"
output += " Use inspect_region tool for detailed view\n"
return output
except Exception as e:
logger.error(f"Error analyzing mesh regions: {str(e)}")
return f"Error analyzing mesh regions: {str(e)}"
def get_vertex_valence(
ctx: Context,
blender_connection,
object_name: str = None,
min_valence: int = 5
) -> str:
"""
Get vertices with valence (edge count) >= threshold.
Identifies potential topology problems like high-valence poles.
Parameters:
- object_name: Name of object to analyze (default: active object)
- min_valence: Minimum valence to include (default: 5)
Returns list of vertices with index, location, and valence.
"""
try:
params = {"min_valence": min_valence}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("get_vertex_valence", params)
if "error" in result:
return f"Error: {result['error']}"
vertices = result.get('vertices', [])
count = result.get('count', 0)
output = f"Vertex Valence Report: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Filter: valence >= {result.get('min_valence', 5)}\n"
output += f"Found: {count} vertices\n\n"
if count == 0:
output += "No vertices match the criteria. Mesh has clean topology!\n"
else:
output += "HIGH-VALENCE VERTICES:\n"
for vert in vertices[:20]: # Limit output
loc = vert.get('location', {})
output += f" Vertex {vert['index']}: valence {vert['valence']}"
output += f" at ({loc.get('x', 0):.3f}, {loc.get('y', 0):.3f}, {loc.get('z', 0):.3f})\n"
if count > 20:
output += f"\n ... and {count - 20} more vertices\n"
output += "\nUse focus_view_on_point to navigate to problem vertices.\n"
return output
except Exception as e:
logger.error(f"Error getting vertex valence: {str(e)}")
return f"Error getting vertex valence: {str(e)}"
def get_triangle_faces(
ctx: Context,
blender_connection,
object_name: str = None
) -> str:
"""
Get all triangle faces (3 vertices) in the mesh.
Identifies triangles that may need conversion to quads.
Parameters:
- object_name: Name of object to analyze (default: active object)
Returns list of triangles with index, center, and vertex indices.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("get_triangle_faces", params)
if "error" in result:
return f"Error: {result['error']}"
triangles = result.get('triangles', [])
count = result.get('count', 0)
output = f"Triangle Faces Report: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Found: {count} triangle faces\n\n"
if count == 0:
output += "No triangles found. Mesh is quad-only or has only n-gons.\n"
else:
output += "TRIANGLE FACES:\n"
for tri in triangles[:15]: # Limit output
center = tri.get('center', {})
output += f" Face {tri['index']}: vertices {tri['vertices']}"
output += f" center ({center.get('x', 0):.3f}, {center.get('y', 0):.3f}, {center.get('z', 0):.3f})\n"
if count > 15:
output += f"\n ... and {count - 15} more triangles\n"
output += "\nUse tris_to_quads to convert compatible triangle pairs.\n"
output += "Use select_triangles to select all triangles for manual editing.\n"
return output
except Exception as e:
logger.error(f"Error getting triangle faces: {str(e)}")
return f"Error getting triangle faces: {str(e)}"
def get_ngon_faces(
ctx: Context,
blender_connection,
object_name: str = None
) -> str:
"""
Get all n-gon faces (5+ vertices) in the mesh.
N-gons should be avoided in retopology for clean edge flow.
Parameters:
- object_name: Name of object to analyze (default: active object)
Returns list of n-gons with index, center, and vertex count.
"""
try:
params = {}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("get_ngon_faces", params)
if "error" in result:
return f"Error: {result['error']}"
ngons = result.get('ngons', [])
count = result.get('count', 0)
output = f"N-gon Faces Report: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Found: {count} n-gon faces (5+ vertices)\n\n"
if count == 0:
output += "No n-gons found. Mesh has clean quad/tri topology!\n"
else:
output += "N-GON FACES:\n"
for ngon in ngons[:15]: # Limit output
center = ngon.get('center', {})
output += f" Face {ngon['index']}: {ngon['vertex_count']} vertices"
output += f" center ({center.get('x', 0):.3f}, {center.get('y', 0):.3f}, {center.get('z', 0):.3f})\n"
if count > 15:
output += f"\n ... and {count - 15} more n-gons\n"
output += "\nN-gons should be manually dissolved or triangulated.\n"
output += "Use select_ngons to select all n-gons for editing.\n"
return output
except Exception as e:
logger.error(f"Error getting n-gon faces: {str(e)}")
return f"Error getting n-gon faces: {str(e)}"
def analyze_cylindrical_structure(
ctx: Context,
blender_connection,
object_name: str = None,
analyze_spacing: bool = False
) -> str:
"""
Analyze cylindrical mesh regions from selected edge loop.
Detects segment count, loop structure, and axis orientation
for cylindrical topology like limbs, pipes, or tubular forms.
Parameters:
- object_name: Name of object (default: active object)
- analyze_spacing: Include edge loop spacing analysis (default: false)
Returns segment count, axis info, and optimization suggestions.
"""
try:
params = {"analyze_spacing": analyze_spacing}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("analyze_cylindrical_structure", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Cylindrical Structure Analysis: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
if result.get('is_cylindrical'):
output += "STRUCTURE: Cylindrical detected\n\n"
else:
output += "STRUCTURE: Not clearly cylindrical (irregular radius)\n\n"
output += "LOOP PROPERTIES:\n"
output += f" Segment count: {result.get('segment_count', 0)}\n"
output += f" Loop vertices: {result.get('loop_vertex_count', 0)}\n"
output += f" Primary axis: {result.get('primary_axis', 'Unknown')}\n"
output += f" Average radius: {result.get('avg_radius', 0):.4f}\n"
center = result.get('center', [0, 0, 0])
output += f" Center: ({center[0]:.3f}, {center[1]:.3f}, {center[2]:.3f})\n\n"
output += "OPTIMIZATION:\n"
output += f" Suggested segments: {result.get('suggested_segments', 8)}\n"
current = result.get('segment_count', 0)
suggested = result.get('suggested_segments', 8)
if current != suggested:
output += f" Note: Consider adjusting from {current} to {suggested} segments\n"
else:
output += " Segment count is optimal!\n"
if analyze_spacing and 'edge_loop_count' in result:
output += f"\nSPACING ANALYSIS:\n"
output += f" Edge loops found: {result.get('edge_loop_count', 1)}\n"
output += f" Average spacing: {result.get('average_spacing', 0):.4f}\n"
return output
except Exception as e:
logger.error(f"Error analyzing cylindrical structure: {str(e)}")
return f"Error analyzing cylindrical structure: {str(e)}"
def find_support_loops(
ctx: Context,
blender_connection,
object_name: str = None,
min_angle: float = 30.0,
near_sharp: bool = True,
suggest: bool = False
) -> str:
"""
Find edge loops suitable for subdivision support.
Identifies edge loops near sharp edges or at significant angle
transitions that could serve as support loops for subdivision surfaces.
Parameters:
- object_name: Name of object (default: active object)
- min_angle: Minimum angle (degrees) to consider an edge sharp (default: 30)
- near_sharp: Find loops near marked sharp edges (default: true)
- suggest: Suggest where to add support loops (default: false)
Returns candidate loops with their locations and edge indices.
"""
try:
params = {
"min_angle": min_angle,
"near_sharp": near_sharp,
"suggest": suggest
}
if object_name:
params["object_name"] = object_name
result = blender_connection.send_command("find_support_loops", params)
if "error" in result:
return f"Error: {result['error']}"
output = f"Support Loop Analysis: {result.get('object_name', 'Unknown')}\n"
output += "=" * 50 + "\n\n"
output += f"Search criteria:\n"
output += f" Min angle: {result.get('min_angle', 30)}°\n"
output += f" Near sharp edges: {result.get('near_sharp', True)}\n\n"
# Sharp edges found
sharp_count = result.get('sharp_edge_count', 0)
output += f"Sharp edges found: {sharp_count}\n"
# Candidate edges
candidates = result.get('candidate_edges', [])
output += f"Candidate support edges: {len(candidates)}\n\n"
if candidates:
output += "CANDIDATE EDGES (by angle):\n"
for i, edge in enumerate(candidates[:20]):
output += f" Edge {edge.get('index', i)}: angle {edge.get('angle', 0):.1f}°\n"
if len(candidates) > 20:
output += f" ... and {len(candidates) - 20} more edges\n"
output += "\n"
# Support loop suggestions
suggestions = result.get('support_suggestions', [])
if suggest and suggestions:
output += "SUGGESTED SUPPORT LOOP LOCATIONS:\n"
for i, sug in enumerate(suggestions[:10]):
loc = sug.get('location', [0, 0, 0])
output += f" {i+1}. Near ({loc[0]:.3f}, {loc[1]:.3f}, {loc[2]:.3f})"
if 'reason' in sug:
output += f" - {sug['reason']}"
output += "\n"
output += "\n"
# Summary
if sharp_count == 0 and len(candidates) == 0:
output += "CONCLUSION: No support loops required.\n"
output += "Mesh appears smooth without hard edges.\n"
elif len(candidates) > 0:
output += "RECOMMENDATION:\n"
output += " Consider adding support loops near candidate edges\n"
output += " to maintain edge sharpness after subdivision.\n"
return output
except Exception as e:
logger.error(f"Error finding support loops: {str(e)}")
return f"Error finding support loops: {str(e)}"