Source code for yapcad.geometry_checks

"""Validation helpers for yapCAD geometry."""

from __future__ import annotations

from collections import Counter
from dataclasses import dataclass
from typing import Iterable, List, Sequence

from yapcad.geom import dist, epsilon, point
from yapcad.geometry_utils import triangle_normal


[docs] def is_closed_polygon(points: Sequence[Sequence[float]], tol: float = epsilon) -> bool: """Return ``True`` if a polyline is closed within ``tol``.""" if not points: return False first = points[0] last = points[-1] if len(first) < 3 or len(last) < 3: return False return dist(first, last) <= tol
[docs] def faces_oriented(surface: Sequence) -> "CheckResult": from yapcad.geom3d import issurface if not issurface(surface): raise ValueError('faces_oriented expects a surface') verts = surface[1] faces = surface[3] reference = None inconsistent = [] for idx, face in enumerate(faces): if len(face) != 3: continue v0 = verts[face[0]] v1 = verts[face[1]] v2 = verts[face[2]] normal = triangle_normal((v0[0], v0[1], v0[2]), (v1[0], v1[1], v1[2]), (v2[0], v2[1], v2[2])) if normal is None: continue if reference is None: reference = normal continue dot = reference[0] * normal[0] + reference[1] * normal[1] + reference[2] * normal[2] if dot < -epsilon: inconsistent.append(idx) if reference is None: return CheckResult(True, ['no non-degenerate faces found']) if inconsistent: return CheckResult(False, [f'inconsistent face orientation indices: {inconsistent}']) return CheckResult(True, [])
[docs] def surface_watertight(surface: Sequence) -> "CheckResult": from yapcad.geom3d import issurface if not issurface(surface): raise ValueError('surface_watertight expects a surface') faces = surface[3] edges = Counter() for face in faces: if len(face) != 3: continue a, b, c = face edges[_edge_key(a, b)] += 1 edges[_edge_key(b, c)] += 1 edges[_edge_key(c, a)] += 1 boundary = [edge for edge, count in edges.items() if count == 1] invalid = [edge for edge, count in edges.items() if count > 2] warnings: List[str] = [] ok = True if boundary: ok = False warnings.append(f'{len(boundary)} boundary edges detected') if invalid: ok = False warnings.append(f'edges with multiplicity >2: {invalid}') return CheckResult(ok, warnings)
def _edge_key(a: int, b: int) -> tuple[int, int]: return (a, b) if a < b else (b, a)
[docs] @dataclass class CheckResult: ok: bool warnings: List[str] def __bool__(self) -> bool: return self.ok
__all__ = [ 'CheckResult', 'is_closed_polygon', 'faces_oriented', 'surface_watertight', ]