"""
Mesh Analysis Handlers
Business logic for mesh topology analysis that runs inside Blender.
These functions use bpy and bmesh, and are called by the addon.py socket server.
"""
import bpy
import bmesh
import mathutils
import math
import traceback
def mesh_stats(active_only=True):
"""Analyze mesh statistics using bmesh for accurate analysis"""
try:
objects_to_analyze = []
if active_only:
# Analyze only the active object
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
objects_to_analyze = [active_obj]
else:
# Analyze all mesh objects in the scene
objects_to_analyze = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH']
if not objects_to_analyze:
return {"error": "No mesh objects found in the scene"}
# Initialize statistics
total_stats = {
"vertex_count": 0,
"edge_count": 0,
"face_count": 0,
"tri_count": 0,
"quad_count": 0,
"ngon_count": 0,
"non_manifold_edge_count": 0,
"sharp_edge_count": 0,
"total_surface_area": 0.0,
"total_volume": 0.0,
"objects_analyzed": []
}
for obj in objects_to_analyze:
# Create a bmesh from the object
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
# Count basic elements
vertex_count = len(bm.verts)
edge_count = len(bm.edges)
face_count = len(bm.faces)
# Count face types
tri_count = sum(1 for f in bm.faces if len(f.verts) == 3)
quad_count = sum(1 for f in bm.faces if len(f.verts) == 4)
ngon_count = sum(1 for f in bm.faces if len(f.verts) > 4)
# Count non-manifold edges
non_manifold_edge_count = sum(1 for e in bm.edges if not e.is_manifold)
# Count sharp edges (angle > 30 degrees)
sharp_angle_threshold = math.radians(30)
sharp_edge_count = 0
for edge in bm.edges:
if len(edge.link_faces) == 2:
angle = edge.calc_face_angle()
if abs(angle) > sharp_angle_threshold:
sharp_edge_count += 1
# Calculate surface area
surface_area = sum(f.calc_area() for f in bm.faces)
# Calculate volume (only if mesh is closed/manifold)
volume = 0.0
try:
# Check if mesh is closed (all edges are manifold)
is_closed = all(e.is_manifold for e in bm.edges)
if is_closed:
volume = bm.calc_volume()
except:
volume = 0.0
# Add to totals
total_stats["vertex_count"] += vertex_count
total_stats["edge_count"] += edge_count
total_stats["face_count"] += face_count
total_stats["tri_count"] += tri_count
total_stats["quad_count"] += quad_count
total_stats["ngon_count"] += ngon_count
total_stats["non_manifold_edge_count"] += non_manifold_edge_count
total_stats["sharp_edge_count"] += sharp_edge_count
total_stats["total_surface_area"] += surface_area
total_stats["total_volume"] += volume
total_stats["objects_analyzed"].append({
"name": obj.name,
"vertex_count": vertex_count,
"edge_count": edge_count,
"face_count": face_count,
"surface_area": surface_area,
"volume": volume
})
# Clean up
bm.free()
return {"success": True, "stats": total_stats}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to analyze mesh: {str(e)}"}
def detect_topology_issues(angle_sharp=30, distance_doubles=0.0001):
"""Detect and report topology issues in the active mesh"""
try:
active_obj = bpy.context.active_object
if not active_obj or active_obj.type != 'MESH':
return {"error": "No active mesh object selected"}
# Create a bmesh from the object
bm = bmesh.new()
bm.from_mesh(active_obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
issues = []
# Detect non-manifold edges
non_manifold_edges = [e.index for e in bm.edges if not e.is_manifold]
if non_manifold_edges:
issues.append({
"type": "non_manifold_edges",
"count": len(non_manifold_edges),
"indices": non_manifold_edges[:100], # Limit to first 100
"suggestion": "Use Mesh > Clean Up > Make Manifold or manually fix edge connections"
})
# Detect loose vertices (not connected to any edge)
loose_verts = [v.index for v in bm.verts if len(v.link_edges) == 0]
if loose_verts:
issues.append({
"type": "loose_vertices",
"count": len(loose_verts),
"indices": loose_verts[:100],
"suggestion": "Use Mesh > Clean Up > Delete Loose to remove disconnected vertices"
})
# Detect loose edges (not connected to any face)
loose_edges = [e.index for e in bm.edges if len(e.link_faces) == 0]
if loose_edges:
issues.append({
"type": "loose_edges",
"count": len(loose_edges),
"indices": loose_edges[:100],
"suggestion": "Use Mesh > Clean Up > Delete Loose to remove disconnected edges"
})
# Detect duplicate vertices
duplicate_verts = []
checked = set()
for v1 in bm.verts:
if v1.index in checked:
continue
for v2 in bm.verts:
if v1.index >= v2.index or v2.index in checked:
continue
distance = (v1.co - v2.co).length
if distance < distance_doubles:
duplicate_verts.append(v1.index)
checked.add(v1.index)
break
if duplicate_verts:
issues.append({
"type": "duplicate_vertices",
"count": len(duplicate_verts),
"indices": duplicate_verts[:100],
"suggestion": f"Use Mesh > Clean Up > Merge by Distance with distance {distance_doubles}"
})
# Detect faces with inverted normals
# First, ensure normals are calculated
bm.normal_update()
# Check for faces with normals pointing in opposite direction to majority
if len(bm.faces) > 0:
# Calculate average normal direction
avg_normal = mathutils.Vector((0, 0, 0))
for f in bm.faces:
avg_normal += f.normal
avg_normal.normalize()
# Find faces with normals pointing opposite
inverted_faces = []
for f in bm.faces:
dot_product = f.normal.dot(avg_normal)
if dot_product < -0.5: # More than 120 degrees difference
inverted_faces.append(f.index)
if inverted_faces:
issues.append({
"type": "inverted_normals",
"count": len(inverted_faces),
"indices": inverted_faces[:100],
"suggestion": "Use Mesh > Normals > Recalculate Outside (Shift+N) to fix face orientation"
})
# Clean up
bm.free()
total_issues = sum(issue["count"] for issue in issues)
return {
"success": True,
"issues": issues,
"total_issues_count": total_issues,
"object_name": active_obj.name
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to detect topology issues: {str(e)}"}