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}