Skip to main content

axiolid_overlay/
arc.rs

1//! Arc-capable planar boundaries (ADR 0050, step 1).
2//!
3//! # Why a second ring type
4//!
5//! [`Ring`](crate::Ring) is a point list, so every edge between
6//! consecutive points is a straight segment. A cylinder cross-section is a
7//! circle, and approximating it by segments would destroy the exactness
8//! that the exact boolean path exists to provide.
9//!
10//! [`ArcRing`] instead stores a bulge per edge, the DXF convention:
11//! `bulge = tan(theta / 4)` where `theta` is the signed included angle of
12//! the arc from one vertex to the next. `bulge == 0` is exactly a straight
13//! segment, so a polygonal boundary is representable without a special
14//! case, and a circle needs only two vertices.
15//!
16//! The sign carries direction: a positive bulge turns counter-clockwise
17//! (bulging left of the chord), a negative one clockwise. That is what
18//! makes a bite and a bump distinguishable on the same chord.
19//!
20//! # What this module does NOT do
21//!
22//! No boolean, no backend, no conversion to any third-party type. This is
23//! the neutral contract and its validation only; ADR 0050 keeps the
24//! adapter in a later step so that library types never reach this API.
25
26use axiolid_core::{Point2, Tolerance};
27use axiolid_guarantees::Sign;
28
29use crate::OverlayError;
30
31/// One boundary vertex and the bulge of the edge leaving it.
32///
33/// The bulge describes the edge from this vertex to the NEXT one, so a
34/// ring of `n` vertices has exactly `n` edges and needs no separate edge
35/// list. Storing it per departing vertex keeps insertion and reversal
36/// local operations rather than re-indexing an edge array.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct ArcVertex {
39    /// Vertex position.
40    pub point: Point2,
41    /// `tan(theta / 4)` for the edge leaving `point`; `0` means straight.
42    pub bulge: f64,
43}
44
45impl ArcVertex {
46    /// A vertex whose departing edge is a straight segment.
47    pub const fn straight(point: Point2) -> Self {
48        Self { point, bulge: 0.0 }
49    }
50
51    /// A vertex whose departing edge bulges by `bulge`.
52    pub const fn bulged(point: Point2, bulge: f64) -> Self {
53        Self { point, bulge }
54    }
55
56    /// True when the departing edge is straight.
57    ///
58    /// Exact comparison is deliberate: a bulge is either exactly zero, in
59    /// which case the edge IS a segment, or it is an arc whose radius is
60    /// whatever the value implies. Treating tiny bulges as straight would
61    /// silently discard curvature the caller asked for.
62    pub fn is_straight(&self) -> bool {
63        self.bulge == 0.0
64    }
65}
66
67/// A closed boundary whose edges may be arcs.
68///
69/// Always closed: the edge leaving the last vertex returns to the first.
70/// An explicitly repeated closing vertex is therefore a zero-length edge
71/// and is refused by [`validate_arc_ring`].
72#[derive(Debug, Clone, PartialEq)]
73pub struct ArcRing {
74    /// Boundary vertices in order.
75    pub vertices: Vec<ArcVertex>,
76}
77
78impl ArcRing {
79    /// Build a ring from vertices.
80    pub fn new(vertices: Vec<ArcVertex>) -> Self {
81        Self { vertices }
82    }
83
84    /// Build a straight-edged ring, the polygonal case.
85    pub fn from_points(points: &[Point2]) -> Self {
86        Self {
87            vertices: points.iter().copied().map(ArcVertex::straight).collect(),
88        }
89    }
90
91    /// A full circle as two semicircular edges.
92    ///
93    /// Two vertices is the minimum honest encoding of a circle: one vertex
94    /// would make the chord degenerate and the centre ambiguous, which is
95    /// why [`validate_arc_ring`] requires at least two.
96    pub fn circle(centre: Point2, radius: f64) -> Self {
97        let left = Point2::new(centre.x - radius, centre.y);
98        let right = Point2::new(centre.x + radius, centre.y);
99        Self {
100            vertices: vec![ArcVertex::bulged(left, 1.0), ArcVertex::bulged(right, 1.0)],
101        }
102    }
103
104    /// Number of edges, which equals the number of vertices.
105    pub fn edge_count(&self) -> usize {
106        self.vertices.len()
107    }
108
109    /// True when every edge is straight.
110    pub fn is_polygonal(&self) -> bool {
111        self.vertices.iter().all(ArcVertex::is_straight)
112    }
113}
114
115/// Signed area enclosed by an arc ring.
116///
117/// Positive is counter-clockwise. The value is the polygon area over the
118/// vertices plus, for each arc edge, the signed area of its circular
119/// segment:
120///
121/// ```text
122/// theta = 4 * atan(bulge)                 signed included angle
123/// R     = chord * (1 + bulge^2) / (4 |bulge|)
124/// extra = R^2 * (theta - sin theta) / 2   signed by theta
125/// ```
126///
127/// Keeping `theta` signed is what makes a reversed ring negate exactly: a
128/// clockwise circle must return `-pi r^2`, not `+pi r^2`. An unsigned
129/// segment term gets the disc right and the reversed disc wrong.
130///
131/// The polygon area is summed as triangles fanned from the first vertex,
132/// so its rounding scales with the ring's extent, not with its distance
133/// from the origin (#274).
134pub fn arc_ring_area(ring: &ArcRing) -> f64 {
135    let count = ring.vertices.len();
136    if count < 2 {
137        return 0.0;
138    }
139    let origin = ring.vertices[0].point;
140    let mut area = 0.0;
141    for index in 1..count - 1 {
142        let from = ring.vertices[index].point - origin;
143        let to = ring.vertices[index + 1].point - origin;
144        area += from.perp_dot(to);
145    }
146    area *= 0.5;
147    for index in 0..count {
148        let from = ring.vertices[index];
149        if from.is_straight() {
150            continue;
151        }
152        let to = ring.vertices[(index + 1) % count];
153        let chord = (to.point - from.point).length();
154        if chord == 0.0 {
155            continue;
156        }
157        let bulge = from.bulge;
158        let theta = 4.0 * bulge.atan();
159        let radius = chord * (1.0 + bulge * bulge) / (4.0 * bulge.abs());
160        area += 0.5 * radius * radius * (theta - theta.sin());
161    }
162    area
163}
164
165/// Arc radius implied by an edge, or `None` for a straight edge.
166pub fn arc_edge_radius(from: ArcVertex, to: ArcVertex) -> Option<f64> {
167    if from.is_straight() {
168        return None;
169    }
170    let chord = (to.point - from.point).length();
171    if chord == 0.0 {
172        return None;
173    }
174    let bulge = from.bulge;
175    Some(chord * (1.0 + bulge * bulge) / (4.0 * bulge.abs()))
176}
177
178/// Validate an arc ring against the same contract as [`crate::Ring`],
179/// plus the arc-specific degeneracies.
180///
181/// Refused, each with a reason the caller can act on:
182///
183/// - fewer than two vertices: a circle needs two, a polygon needs three,
184///   and one vertex cannot describe either
185/// - a polygonal ring with fewer than three vertices: two straight edges
186///   enclose no area
187/// - non-finite coordinates or a non-finite bulge
188/// - a zero-length edge, which leaves the arc centre undefined
189/// - an arc whose implied radius is below tolerance, the zero-radius case
190///   ADR 0050 flagged: such an arc is a point, not a boundary
191/// - a ring enclosing no measurable area: at most `tolerance.linear()^2`,
192///   decided exactly for a ring without arcs (one on a line included, as
193///   the arc path has no rule leaving it out), and from [`arc_ring_area`]
194///   otherwise (#274)
195///
196/// Self-intersection is NOT checked here: a cheap approximation would either
197/// reject valid input or pass invalid input. [`check_arc_polygon`] decides it
198/// exactly, for a ring and for the holes nested in it (#352).
199///
200/// [`check_arc_polygon`]: crate::check_arc_polygon
201pub fn validate_arc_ring(ring: &ArcRing, tolerance: Tolerance) -> Result<(), OverlayError> {
202    let count = ring.vertices.len();
203    if count < 2 {
204        return Err(OverlayError::RingTooShort);
205    }
206    if ring.is_polygonal() && count < 3 {
207        return Err(OverlayError::RingTooShort);
208    }
209    if !ring
210        .vertices
211        .iter()
212        .all(|vertex| vertex.point.is_finite() && vertex.bulge.is_finite())
213    {
214        return Err(OverlayError::NonFinitePoint);
215    }
216    for index in 0..count {
217        let from = ring.vertices[index];
218        let to = ring.vertices[(index + 1) % count];
219        if (to.point - from.point).length() <= tolerance.linear() {
220            return Err(OverlayError::RepeatedVertex);
221        }
222        // A bulge implying a sub-tolerance radius is a point masquerading
223        // as an arc: the chord is shorter than the tolerance relative to
224        // the curvature, so no circle can be recovered from it.
225        if let Some(radius) = arc_edge_radius(from, to) {
226            if radius <= tolerance.linear() {
227                return Err(OverlayError::ZeroRadiusArc);
228            }
229        }
230    }
231    let zero = if ring.is_polygonal() {
232        crate::zero_area(&points(ring), tolerance.linear())
233    } else {
234        arc_ring_area(ring).abs() <= tolerance.linear().powi(2)
235    };
236    if zero {
237        return Err(OverlayError::ZeroArea);
238    }
239    Ok(())
240}
241
242fn points(ring: &ArcRing) -> Vec<Point2> {
243    ring.vertices.iter().map(|vertex| vertex.point).collect()
244}
245
246/// A ring's orientation: `Positive` counter-clockwise.
247///
248/// Exact for a ring without arcs (#274), the sign of [`arc_ring_area`]
249/// otherwise: a circular segment's area is transcendental in its bulge.
250pub(crate) fn arc_orientation(ring: &ArcRing) -> Sign {
251    if ring.is_polygonal() {
252        return crate::orientation(&points(ring));
253    }
254    let area = arc_ring_area(ring);
255    if area > 0.0 {
256        Sign::Positive
257    } else if area < 0.0 {
258        Sign::Negative
259    } else {
260        Sign::Zero
261    }
262}
263
264/// Reverse a ring's orientation, preserving its geometry.
265///
266/// Bulges are stored per departing edge, so reversing the vertex order
267/// alone would attach each bulge to the wrong edge. The bulge list must
268/// shift by one and negate: the edge that left vertex `i` toward `i + 1`
269/// becomes the edge leaving `i + 1` toward `i`, curving the other way.
270pub fn reverse_arc_ring(ring: &ArcRing) -> ArcRing {
271    let count = ring.vertices.len();
272    if count == 0 {
273        return ArcRing::new(Vec::new());
274    }
275    let mut vertices = Vec::with_capacity(count);
276    for index in (0..count).rev() {
277        let point = ring.vertices[index].point;
278        // The edge arriving at `index` came from its predecessor.
279        let predecessor = (index + count - 1) % count;
280        vertices.push(ArcVertex::bulged(point, -ring.vertices[predecessor].bulge));
281    }
282    ArcRing::new(vertices)
283}