Source code for yapcad.geometry_utils

"""Common geometric helpers shared across exporters and validators."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable, Sequence, Tuple

from yapcad.geom import cross, epsilon, mag

Vec3 = Tuple[float, float, float]


[docs] @dataclass(frozen=True) class Triangle: """Immutable triangle representation in XYZ space.""" normal: Vec3 v0: Vec3 v1: Vec3 v2: Vec3
[docs] def to_vec3(point_like: Sequence[float]) -> Vec3: """Return the XYZ components of a yapCAD point/vector as a tuple.""" if len(point_like) < 3: raise ValueError("value must have at least three components") return float(point_like[0]), float(point_like[1]), float(point_like[2])
[docs] def to_point(vec: Sequence[float]) -> Tuple[float, float, float, float]: """Lift an XYZ tuple into yapCAD homogeneous point form.""" if len(vec) < 3: raise ValueError("vector must have three components") return float(vec[0]), float(vec[1]), float(vec[2]), 1.0
[docs] def to_vector(vec: Sequence[float]) -> Tuple[float, float, float, float]: """Lift an XYZ tuple into yapCAD homogeneous vector form (w=0).""" if len(vec) < 3: raise ValueError("vector must have three components") return float(vec[0]), float(vec[1]), float(vec[2]), 0.0
[docs] def triangle_normal(v0: Vec3, v1: Vec3, v2: Vec3) -> Vec3 | None: """Return the unit normal of a triangle or ``None`` if degenerate.""" ax, ay, az = v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2] bx, by, bz = v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2] n = cross([ax, ay, az, 1.0], [bx, by, bz, 1.0]) length = mag(n) if length <= epsilon: return None return (n[0] / length, n[1] / length, n[2] / length)
[docs] def triangle_area(v0: Vec3, v1: Vec3, v2: Vec3) -> float: """Return the area of a triangle.""" ax, ay, az = v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2] bx, by, bz = v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2] n = cross([ax, ay, az, 1.0], [bx, by, bz, 1.0]) return 0.5 * mag(n)
[docs] def triangle_is_degenerate(v0: Vec3, v1: Vec3, v2: Vec3, tol: float = epsilon) -> bool: """Return ``True`` if a triangle collapses under the given tolerance.""" return triangle_area(v0, v1, v2) <= tol
[docs] def orient_triangle(v0: Vec3, v1: Vec3, v2: Vec3, preferred_normal: Vec3) -> Tuple[Vec3, Vec3, Vec3]: """Ensure triangle winding aligns with ``preferred_normal``.""" current = triangle_normal(v0, v1, v2) if current is None: return v0, v1, v2 dot = current[0] * preferred_normal[0] + current[1] * preferred_normal[1] + current[2] * preferred_normal[2] if dot < 0: return v0, v2, v1 return v0, v1, v2
[docs] def triangle_centroid(v0: Vec3, v1: Vec3, v2: Vec3) -> Vec3: """Return the centroid of a triangle.""" return ( (v0[0] + v1[0] + v2[0]) / 3.0, (v0[1] + v1[1] + v2[1]) / 3.0, (v0[2] + v1[2] + v2[2]) / 3.0, )
[docs] def triangles_from_mesh(mesh: Iterable[Tuple[Vec3, Vec3, Vec3, Vec3]]) -> Iterable[Triangle]: """Convert ``mesh_view`` output into ``Triangle`` instances.""" for normal, v0, v1, v2 in mesh: yield Triangle(normal=normal, v0=v0, v1=v1, v2=v2)
__all__ = [ "Triangle", "Vec3", "to_vec3", "to_point", "to_vector", "triangle_normal", "triangle_area", "triangle_is_degenerate", "orient_triangle", "triangle_centroid", "triangles_from_mesh", ]