use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point2, Scalar, Tolerance};
use axiolid_curve::Curve2;
use axiolid_profile::{CircleProfile, EllipseProfile, Profile, RectangleProfile};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Rings {
pub outer: Vec<Point2>,
pub holes: Vec<Vec<Point2>>,
}
pub fn profile_rings(
profile: &Profile,
chord_error: Scalar,
tolerance: Tolerance,
) -> GeomResult<Rings> {
match profile {
Profile::Rectangle(r) if r.outer_radius.is_some() || r.inner_radius.is_some() => {
let contour = crate::section_lower::rectangle_contour(r)?;
contour_rings(&contour, chord_error, tolerance)
}
Profile::Rectangle(r) => rectangle_rings(r, chord_error, tolerance),
Profile::Section(section) => {
let contour = crate::section_lower::section_contour(section)?;
contour_rings(&contour, chord_error, tolerance)
}
Profile::Circle(c) => circle_rings(c, chord_error),
Profile::Ellipse(e) => ellipse_rings(e, chord_error),
Profile::Contour(c) => contour_rings(c, chord_error, tolerance),
Profile::Derived { basis, transform } => {
let mut rings = profile_rings(basis, chord_error, tolerance)?;
apply2(&mut rings.outer, transform);
for hole in &mut rings.holes {
apply2(hole, transform);
}
if transform.matrix2.determinant() < 0.0 {
rings.outer.reverse();
for hole in &mut rings.holes {
hole.reverse();
}
}
Ok(rings)
}
Profile::CenterLine(cl) => {
crate::center_line::center_line_rings(cl, chord_error, tolerance, contour_points)
}
other => Err(GeomError::Unsupported {
backend: crate::BACKEND_ID,
operation: axiolid_contracts::Operation::ProfileTriangulation,
})
.inspect_err(|_| {
let _ = other;
}),
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProfileDeviation {
Bounded(Scalar),
Unbounded(&'static str),
}
impl ProfileDeviation {
#[must_use]
pub fn max(self, other: Self) -> Self {
match (self, other) {
(Self::Unbounded(reason), _) | (_, Self::Unbounded(reason)) => Self::Unbounded(reason),
(Self::Bounded(a), Self::Bounded(b)) => Self::Bounded(a.max(b)),
}
}
fn scaled(self, factor: Scalar) -> Self {
match self {
Self::Bounded(d) => Self::Bounded(d * factor),
unbounded => unbounded,
}
}
fn plus(self, extra: Scalar) -> Self {
match self {
Self::Bounded(d) => Self::Bounded(d + extra),
unbounded => unbounded,
}
}
}
pub fn profile_deviation(
profile: &Profile,
chord_error: Scalar,
tolerance: Tolerance,
) -> GeomResult<ProfileDeviation> {
Ok(match profile {
Profile::Rectangle(r) if r.outer_radius.is_some() || r.inner_radius.is_some() => {
contour_deviation(
&crate::section_lower::rectangle_contour(r)?,
chord_error,
tolerance,
)?
}
Profile::Rectangle(r) => {
rectangle_rings(r, chord_error, tolerance)?;
match r.thickness {
Some(t) if tolerance.eq(r.x / 2.0 - t, 0.0) || tolerance.eq(r.y / 2.0 - t, 0.0) => {
ProfileDeviation::Unbounded("hollow rectangle whose hole collapses")
}
_ => ProfileDeviation::Bounded(0.0),
}
}
Profile::Section(section) => contour_deviation(
&crate::section_lower::section_contour(section)?,
chord_error,
tolerance,
)?,
Profile::Circle(c) => {
circle_rings(c, chord_error)?;
ProfileDeviation::Bounded(chord_error)
}
Profile::Ellipse(e) => {
ellipse_rings(e, chord_error)?;
ProfileDeviation::Bounded(chord_error)
}
Profile::Contour(c) => contour_deviation(c, chord_error, tolerance)?,
Profile::Derived { basis, transform } => {
profile_deviation(basis, chord_error, tolerance)?.scaled(stretch2(transform))
}
Profile::CenterLine(cl) => {
profile_rings(profile, chord_error, tolerance)?;
let mut merged: Scalar = 0.0;
contour_points_measured(&cl.path, chord_error, tolerance, &mut merged)?;
let straight = cl
.path
.segments
.iter()
.all(|segment| matches!(segment.curve, Curve2::Line(_) | Curve2::Polyline(_)));
if straight && merged == 0.0 {
ProfileDeviation::Bounded(0.0)
} else {
ProfileDeviation::Unbounded("centre-line profile along a curved or merged path")
}
}
_ => {
profile_rings(profile, chord_error, tolerance)?;
ProfileDeviation::Unbounded("profile family")
}
})
}
fn stretch2(transform: &axiolid_core::Transform2) -> Scalar {
let m = transform.matrix2;
let f = m.x_axis.length_squared() + m.y_axis.length_squared();
let d = m.determinant();
let disc = (f * f - 4.0 * d * d).max(0.0).sqrt();
(0.5 * (f + disc)).sqrt() * (1.0 + 1e-12)
}
fn contour_deviation(
c: &axiolid_profile::ContourProfile,
chord_error: Scalar,
tolerance: Tolerance,
) -> GeomResult<ProfileDeviation> {
let mut deviation = ProfileDeviation::Bounded(0.0);
for contour in core::iter::once(&c.outer).chain(&c.holes) {
let mut merged: Scalar = 0.0;
contour_points_measured(contour, chord_error, tolerance, &mut merged)?;
let mut chords = ProfileDeviation::Bounded(0.0);
for segment in &contour.segments {
chords = chords.max(segment_deviation(&segment.curve, chord_error));
}
deviation = deviation.max(chords.plus(merged));
}
Ok(deviation)
}
fn segment_deviation(curve: &Curve2, chord_error: Scalar) -> ProfileDeviation {
match curve {
Curve2::Line(_) | Curve2::Polyline(_) => ProfileDeviation::Bounded(0.0),
other if axiolid_reference::bound::certifies_flattening2(other) => {
ProfileDeviation::Bounded(chord_error)
}
Curve2::Intrinsic(_) => ProfileDeviation::Unbounded("intrinsic (clothoid) profile segment"),
Curve2::Chain(_) => {
ProfileDeviation::Unbounded("arc-length chain profile segment with an unbounded piece")
}
Curve2::BSpline(_) => {
ProfileDeviation::Unbounded("B-spline profile segment with a non-positive weight")
}
_ => ProfileDeviation::Unbounded("profile segment family"),
}
}
fn contour_rings(
c: &axiolid_profile::ContourProfile,
chord_error: Scalar,
tolerance: Tolerance,
) -> GeomResult<Rings> {
let outer = contour_points(&c.outer, chord_error, tolerance)?;
let mut holes = Vec::with_capacity(c.holes.len());
for hole in &c.holes {
holes.push(contour_points(hole, chord_error, tolerance)?);
}
Ok(orient_rings(outer, holes))
}
fn rectangle_rings(
r: &RectangleProfile,
_chord_error: Scalar,
tolerance: Tolerance,
) -> GeomResult<Rings> {
if !(r.x.is_finite() && r.y.is_finite()) || r.x <= 0.0 || r.y <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"rectangle profile must have positive finite extents, got {} x {}",
r.x, r.y
)));
}
let (hx, hy) = (r.x / 2.0, r.y / 2.0);
let outer = vec![
Point2::new(-hx, -hy),
Point2::new(hx, -hy),
Point2::new(hx, hy),
Point2::new(-hx, hy),
];
let mut holes = Vec::new();
if let Some(t) = r.thickness {
if t <= 0.0 || 2.0 * t >= r.x || 2.0 * t >= r.y {
return Err(GeomError::InvalidInput(format!(
"hollow rectangle wall thickness {t} does not fit inside {} x {}",
r.x, r.y
)));
}
let (ix, iy) = (hx - t, hy - t);
if !tolerance.eq(ix, 0.0) && !tolerance.eq(iy, 0.0) {
holes.push(vec![
Point2::new(-ix, -iy),
Point2::new(-ix, iy),
Point2::new(ix, iy),
Point2::new(ix, -iy),
]);
}
}
Ok(Rings { outer, holes })
}
fn circle_rings(c: &CircleProfile, chord_error: Scalar) -> GeomResult<Rings> {
if !c.radius.is_finite() || c.radius <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"circle profile radius must be positive and finite, got {}",
c.radius
)));
}
let outer = flatten_circle(c.radius, chord_error)?;
let mut holes = Vec::new();
if let Some(t) = c.thickness {
if t <= 0.0 || t >= c.radius {
return Err(GeomError::InvalidInput(format!(
"annulus wall thickness {t} does not fit inside radius {}",
c.radius
)));
}
let inner = c.radius - t;
let mut ring = flatten_circle(inner, chord_error)?;
ring.reverse(); holes.push(ring);
}
Ok(Rings { outer, holes })
}
fn ellipse_rings(e: &EllipseProfile, chord_error: Scalar) -> GeomResult<Rings> {
if !e.semi_axis_x.is_finite() || e.semi_axis_x <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"ellipse semi-axis x must be positive and finite, got {}",
e.semi_axis_x
)));
}
if !e.semi_axis_y.is_finite() || e.semi_axis_y <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"ellipse semi-axis y must be positive and finite, got {}",
e.semi_axis_y
)));
}
use axiolid_core::{Frame2, Interval, Vec2};
use axiolid_curve::{Curve2, Ellipse2};
let curve = Curve2::Ellipse(Ellipse2 {
frame: Frame2 {
origin: Point2::new(0.0, 0.0),
x: Vec2::new(1.0, 0.0),
y: Vec2::new(0.0, 1.0),
},
semi_axis_x: e.semi_axis_x,
semi_axis_y: e.semi_axis_y,
});
let mut ring = axiolid_reference::curve::flatten2(
&curve,
Interval {
start: 0.0,
end: core::f64::consts::TAU,
},
chord_error,
MAX_SUBDIVISION_DEPTH,
)?;
ring.pop();
Ok(Rings {
outer: ring,
holes: Vec::new(),
})
}
fn flatten_circle(radius: Scalar, chord_error: Scalar) -> GeomResult<Vec<Point2>> {
use axiolid_core::{Frame2, Interval, Vec2};
use axiolid_curve::{Circle2, Curve2};
let curve = Curve2::Circle(Circle2 {
frame: Frame2 {
origin: Point2::new(0.0, 0.0),
x: Vec2::new(1.0, 0.0),
y: Vec2::new(0.0, 1.0),
},
radius,
});
let flatten = |start: Scalar| {
axiolid_reference::curve::flatten2(
&curve,
Interval {
start,
end: start + core::f64::consts::TAU,
},
chord_error,
MAX_SUBDIVISION_DEPTH,
)
};
let steps = flatten(0.0)?.len().saturating_sub(1).max(1);
let mut ring = flatten(core::f64::consts::PI / steps as Scalar)?;
ring.pop();
Ok(ring)
}
pub fn triangulate(rings: &Rings) -> GeomResult<(Vec<Point2>, Vec<[u32; 3]>)> {
triangulate_with(rings, PinchPolicy::Refuse)
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PinchPolicy {
Refuse,
Accept,
}
pub fn triangulate_with(
rings: &Rings,
pinches: PinchPolicy,
) -> GeomResult<(Vec<Point2>, Vec<[u32; 3]>)> {
if rings.outer.len() < 3 {
return Err(GeomError::InvalidInput(format!(
"profile outer ring needs at least 3 vertices, got {}",
rings.outer.len()
)));
}
for hole in &rings.holes {
if hole.len() < 3 {
return Err(GeomError::InvalidInput(format!(
"profile hole needs at least 3 vertices, got {}",
hole.len()
)));
}
}
crate::ring_triangulation::triangulate_rings(&rings.outer, &rings.holes, pinches)
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RingTouch {
pub ring: usize,
pub edge: usize,
pub vertex: usize,
}
pub fn ring_touches(rings: &Rings) -> GeomResult<Vec<RingTouch>> {
crate::ring_triangulation::ring_touches(&rings.outer, &rings.holes).map(|touches| {
touches
.into_iter()
.map(|(ring, edge, vertex)| RingTouch { ring, edge, vertex })
.collect()
})
}
fn apply2(ring: &mut [Point2], t: &axiolid_core::Transform2) {
for p in ring.iter_mut() {
*p = t.transform_point2(*p);
}
}
fn contour_points(
contour: &axiolid_profile::Contour,
chord_error: Scalar,
tolerance: Tolerance,
) -> GeomResult<Vec<Point2>> {
let mut merged = 0.0;
contour_points_measured(contour, chord_error, tolerance, &mut merged)
}
fn contour_points_measured(
contour: &axiolid_profile::Contour,
chord_error: Scalar,
tolerance: Tolerance,
merged: &mut Scalar,
) -> GeomResult<Vec<Point2>> {
let mut out: Vec<Point2> = Vec::new();
for segment in &contour.segments {
let mut pts = segment_points(segment, chord_error)?;
if !segment.same_sense {
pts.reverse();
}
for p in pts {
match out.last() {
Some(last) if near2(*last, p, tolerance.linear()) => {
*merged = merged.max((*last - p).length());
}
_ => out.push(p),
}
}
}
while out.len() > 1 && near2(out[0], *out.last().expect("non-empty"), tolerance.linear()) {
let dropped = out.pop().expect("non-empty");
*merged = merged.max((out[0] - dropped).length());
}
if out.len() < 3 {
return Err(GeomError::Degenerate(format!(
"contour flattened to {} points, need at least 3",
out.len()
)));
}
Ok(out)
}
fn near2(a: Point2, b: Point2, linear: Scalar) -> bool {
(a.x - b.x).abs() <= linear && (a.y - b.y).abs() <= linear
}
const MAX_SUBDIVISION_DEPTH: u32 = 24;
fn segment_points(
segment: &axiolid_profile::ProfileSegment,
chord_error: Scalar,
) -> GeomResult<Vec<Point2>> {
axiolid_reference::curve::flatten2(
&segment.curve,
segment.domain,
chord_error,
MAX_SUBDIVISION_DEPTH,
)
}
use axiolid_reference::signed_area2;
fn orient_rings(mut outer: Vec<Point2>, mut holes: Vec<Vec<Point2>>) -> Rings {
if signed_area2(&outer) < 0.0 {
outer.reverse();
}
for hole in &mut holes {
if signed_area2(hole) > 0.0 {
hole.reverse();
}
}
Rings { outer, holes }
}