Added sanity check baseline made by claude.
Also changed the location to a panel in VIEW_3D instead of menu.
This commit is contained in:
1 parent
71b1a61b5c
commit
4617c47ad0
33 files changed
+2771
-52
No files matched your search
@@ -0,0 +1,109 @@
|
||||
import bmesh
|
||||
import numpy as np
|
||||
|
||||
from ... import config
|
||||
from ...core import Check, Issue, contexts
|
||||
from ...core.mesh import loop_polygon_index, polygon_arrays, polygon_normals, vertex_coords
|
||||
|
||||
|
||||
class NgonCheck(Check):
|
||||
id = "ngons"
|
||||
label = "N-gons"
|
||||
description = "Meshes contain only triangles and quads (no face with more than 4 sides)."
|
||||
category = "Geometry"
|
||||
contexts = contexts.ASSET
|
||||
order = 10
|
||||
|
||||
def run(self, ctx):
|
||||
for ob in ctx.objects("MESH"):
|
||||
_, totals, _ = polygon_arrays(ob.data)
|
||||
bad = np.flatnonzero(totals > config.MAX_FACE_SIDES)
|
||||
if bad.size:
|
||||
yield Issue.for_id(
|
||||
f"{ob.name}: {bad.size} face(s) with more than {config.MAX_FACE_SIDES} sides",
|
||||
ob,
|
||||
elements=bad.tolist(),
|
||||
element_type="FACE",
|
||||
)
|
||||
|
||||
|
||||
def concave_polygons(mesh, tolerance):
|
||||
"""Indices of polygons with at least one reflex corner."""
|
||||
if not len(mesh.polygons):
|
||||
return np.empty(0, dtype=np.int32)
|
||||
starts, totals, loop_verts = polygon_arrays(mesh)
|
||||
co = vertex_coords(mesh)
|
||||
normals = polygon_normals(mesh)
|
||||
|
||||
poly_of_loop = loop_polygon_index(totals)
|
||||
start = starts[poly_of_loop]
|
||||
total = totals[poly_of_loop]
|
||||
local = np.arange(len(loop_verts)) - start
|
||||
prev_loop = start + (local - 1) % total
|
||||
next_loop = start + (local + 1) % total
|
||||
|
||||
point = co[loop_verts]
|
||||
incoming = point - co[loop_verts[prev_loop]]
|
||||
outgoing = co[loop_verts[next_loop]] - point
|
||||
turn = np.einsum("ij,ij->i", np.cross(incoming, outgoing), normals[poly_of_loop])
|
||||
scale = np.linalg.norm(incoming, axis=1) * np.linalg.norm(outgoing, axis=1) + 1e-12
|
||||
reflex = (turn / scale < -tolerance) & (total > 3)
|
||||
return np.unique(poly_of_loop[reflex])
|
||||
|
||||
|
||||
class ConcaveFaceCheck(Check):
|
||||
id = "concave_faces"
|
||||
label = "Concave faces"
|
||||
description = "Quads and n-gons are convex (no corner angle above 180 degrees)."
|
||||
category = "Geometry"
|
||||
contexts = contexts.ASSET
|
||||
order = 20
|
||||
|
||||
def run(self, ctx):
|
||||
for ob in ctx.objects("MESH"):
|
||||
bad = concave_polygons(ob.data, config.CONCAVE_TOLERANCE)
|
||||
if bad.size:
|
||||
yield Issue.for_id(
|
||||
f"{ob.name}: {bad.size} concave face(s)",
|
||||
ob,
|
||||
elements=bad.tolist(),
|
||||
element_type="FACE",
|
||||
)
|
||||
|
||||
|
||||
class NonManifoldCheck(Check):
|
||||
id = "non_manifold"
|
||||
label = "Non-manifold geometry"
|
||||
description = (
|
||||
"No wire edges, edges shared by more than two faces, inconsistent normals, "
|
||||
"bow-tie vertices or (unless allowed in config) open borders."
|
||||
)
|
||||
category = "Geometry"
|
||||
contexts = contexts.ASSET
|
||||
order = 30
|
||||
|
||||
def run(self, ctx):
|
||||
allow_boundary = config.NON_MANIFOLD_ALLOW_BOUNDARY
|
||||
for ob in ctx.objects("MESH"):
|
||||
bm = bmesh.new()
|
||||
try:
|
||||
bm.from_mesh(ob.data)
|
||||
edges = [
|
||||
e.index for e in bm.edges
|
||||
if not e.is_contiguous and not (allow_boundary and e.is_boundary)
|
||||
]
|
||||
verts = [
|
||||
v.index for v in bm.verts
|
||||
if not v.is_manifold and not (allow_boundary and v.is_boundary)
|
||||
and not any(not e.is_contiguous for e in v.link_edges)
|
||||
]
|
||||
finally:
|
||||
bm.free()
|
||||
if edges:
|
||||
yield Issue.for_id(
|
||||
f"{ob.name}: {len(edges)} non-manifold edge(s)", ob, elements=edges, element_type="EDGE"
|
||||
)
|
||||
if verts:
|
||||
yield Issue.for_id(
|
||||
f"{ob.name}: {len(verts)} non-manifold vertex(es)", ob, elements=verts, element_type="VERT"
|
||||
)
|
||||
Reference in new issue
Block a user