110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
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"
|
|
)
|