Skip to main content

geometry_overlay/
buffer.rs

1//! OVL7 — `buffer`: grow a geometry outward by a fixed distance.
2//!
3//! Mirrors `boost/geometry/algorithms/buffer.hpp` and the buffer
4//! strategies under `strategies/buffer/`. A buffer offsets every part of
5//! the input outward by `distance`, rounding or mitering the corners,
6//! and unions the offset pieces into an output polygon.
7//!
8//! Cartesian dispatch covers every static single and homogeneous multi kind.
9//! Spherical and geographic inputs are projected into a local tangent plane,
10//! buffered by the same Cartesian engine, and transformed back. The angular
11//! path is intended for local buffers: unlike Boost's per-segment geodesic
12//! offset formulas, its error grows with the geometry's angular extent and it
13//! rejects projection centers at the poles. This deliberate approximation is
14//! recorded in the project feature-parity map for later reassessment.
15//! Polygon offsets are signed, handle convex and reflex vertices, and move
16//! interior rings in the opposite topological direction from the exterior.
17//!
18//! Join / end / point strategies are modelled as small enums
19//! ([`JoinStrategy`], [`PointStrategy`]) mirroring Boost's
20//! `join_round` / `join_miter` and `point_circle` / `point_square`
21//! strategy types.
22
23// Segment counts convert freely between `usize` and `f64` to lay out
24// circle / arc vertices; the values are small angular subdivisions where
25// the sub-mantissa precision loss and the non-negative truncation are
26// intentional and harmless.
27#![allow(
28    clippy::cast_precision_loss,
29    clippy::cast_possible_truncation,
30    clippy::cast_sign_loss,
31    reason = "angular vertex-count arithmetic; values are small and non-negative"
32)]
33// Zero-length guards and closing-vertex identity compare `f64`s exactly
34// on purpose — these are degenerate-case gates, not tolerance checks.
35#![allow(clippy::float_cmp, reason = "exact degenerate-case guards")]
36
37use alloc::vec::Vec;
38
39use geometry_coords::{
40    CoordinateScalar,
41    math::{atan2, ceil, cos, hypot, mul_add, sin, sqrt},
42};
43use geometry_cs::{
44    AngleUnit, Cartesian, CartesianFamily, CoordinateSystem, FromF64, Geographic, GeographicFamily,
45    Spherical, SphericalFamily,
46};
47use geometry_model::{
48    Box as ModelBox, Linestring, MultiLinestring, MultiPoint, MultiPolygon, Point2D, Polygon, Ring,
49    Segment,
50};
51use geometry_strategy::buffer::{
52    BufferDistanceStrategy, BufferEndStrategy, BufferJoinStrategy, BufferPointStrategy,
53    BufferSettings, CartesianBuffer, DefaultBuffer, DefaultBufferStrategy, GeographicBuffer,
54    SphericalBuffer,
55};
56use geometry_tag::{
57    BoxTag, LinestringTag, MultiLinestringTag, MultiPointTag, MultiPolygonTag, PointTag,
58    PolygonTag, RingTag, SameAs, SegmentTag,
59};
60use geometry_trait::{
61    Box as BoxTrait, Geometry, Linestring as LinestringTrait,
62    MultiLinestring as MultiLinestringTrait, MultiPoint as MultiPointTrait,
63    MultiPolygon as MultiPolygonTrait, Point, PointMut, Polygon as PolygonTrait, Ring as RingTrait,
64    Segment as SegmentTrait, box_max, box_min, segment_end, segment_start,
65};
66
67use crate::operation::{OverlayError, difference_multi, union_multi};
68use crate::predicate::segment_intersection::{SegmentIntersection, segment_intersection};
69
70/// How to fill the wedge at a convex corner of the offset boundary.
71///
72/// Mirrors `strategy::buffer::join_round` / `join_miter`
73/// (`strategies/buffer/buffer_join_round.hpp` and friends).
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum JoinStrategy {
76    /// Fill the corner with a circular arc of `points_per_circle`
77    /// segments. Boost's `join_round`.
78    Round {
79        /// Segment count of a full circle; the arc uses a proportional
80        /// share.
81        points_per_circle: usize,
82    },
83    /// Extend the two offset edges until they meet at a sharp point.
84    /// Boost's `join_miter`.
85    ///
86    /// This compatibility spelling uses Boost's default miter limit of
87    /// five times the buffer distance
88    /// (`strategies/cartesian/buffer_join_miter.hpp:52-60`). Use
89    /// [`BufferSettings`] with [`BufferJoinStrategy::Miter`] to select a
90    /// different limit.
91    Miter,
92}
93
94/// How to approximate a buffered point.
95///
96/// Mirrors `strategy::buffer::point_circle` / `point_square`
97/// (`strategies/buffer/buffer_point_circle.hpp`).
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PointStrategy {
100    /// Approximate the buffer disc with a regular polygon of
101    /// `points_per_circle` vertices. Boost's `point_circle`.
102    Circle {
103        /// Vertex count of the approximating polygon.
104        points_per_circle: usize,
105    },
106    /// Approximate the buffer with an axis-aligned square. Boost's
107    /// `point_square`.
108    Square,
109}
110
111/// Per-geometry implementation selected by [`buffer`].
112///
113/// Rust tag-dispatch adapter for the geometry-specialized call behind
114/// `boost::geometry::buffer` in
115/// `algorithms/detail/buffer/interface.hpp:246-273`.
116#[doc(hidden)]
117pub trait BufferStrategy<G: Geometry, CoordinateStrategy> {
118    fn apply(
119        &self,
120        geometry: &G,
121        settings: BufferSettings,
122        coordinate_strategy: &CoordinateStrategy,
123    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>;
124}
125
126/// Tag-to-buffer implementation picker.
127///
128/// Rust counterpart to the geometry dispatch performed by
129/// `boost::geometry::buffer` in
130/// `algorithms/detail/buffer/interface.hpp:246-273`.
131#[doc(hidden)]
132pub trait BufferStrategyForKind {
133    type S: Default;
134}
135
136/// Point buffer implementation selected for [`PointTag`].
137///
138/// Implements the point arm of the public buffer dispatch from
139/// `algorithms/detail/buffer/interface.hpp:246-273`.
140#[doc(hidden)]
141#[derive(Debug, Default, Clone, Copy)]
142pub struct PointBuffer;
143
144/// Polygon buffer implementation selected for [`PolygonTag`].
145///
146/// Implements the polygon arm of the public buffer dispatch from
147/// `algorithms/detail/buffer/interface.hpp:246-273`.
148#[doc(hidden)]
149#[derive(Debug, Default, Clone, Copy)]
150pub struct PolygonBuffer;
151
152/// Linestring buffer implementation selected for [`LinestringTag`].
153#[doc(hidden)]
154#[derive(Debug, Default, Clone, Copy)]
155pub struct LinestringBuffer;
156
157/// Segment buffer implementation selected for [`SegmentTag`].
158#[doc(hidden)]
159#[derive(Debug, Default, Clone, Copy)]
160pub struct SegmentBuffer;
161
162/// Ring buffer implementation selected for [`RingTag`].
163#[doc(hidden)]
164#[derive(Debug, Default, Clone, Copy)]
165pub struct RingBuffer;
166
167/// Box buffer implementation selected for [`BoxTag`].
168#[doc(hidden)]
169#[derive(Debug, Default, Clone, Copy)]
170pub struct BoxBuffer;
171
172/// Multi-point buffer implementation selected for [`MultiPointTag`].
173#[doc(hidden)]
174#[derive(Debug, Default, Clone, Copy)]
175pub struct MultiPointBuffer;
176
177/// Multi-linestring buffer implementation selected for [`MultiLinestringTag`].
178#[doc(hidden)]
179#[derive(Debug, Default, Clone, Copy)]
180pub struct MultiLinestringBuffer;
181
182/// Multi-polygon buffer implementation selected for [`MultiPolygonTag`].
183#[doc(hidden)]
184#[derive(Debug, Default, Clone, Copy)]
185pub struct MultiPolygonBuffer;
186
187/// Selects the point arm of `buffer_all` from
188/// `algorithms/detail/buffer/interface.hpp:269-273`.
189impl BufferStrategyForKind for PointTag {
190    type S = PointBuffer;
191}
192
193/// Selects the polygon arm of `buffer_all` from
194/// `algorithms/detail/buffer/interface.hpp:269-273`.
195impl BufferStrategyForKind for PolygonTag {
196    type S = PolygonBuffer;
197}
198
199impl BufferStrategyForKind for LinestringTag {
200    type S = LinestringBuffer;
201}
202
203impl BufferStrategyForKind for SegmentTag {
204    type S = SegmentBuffer;
205}
206
207impl BufferStrategyForKind for RingTag {
208    type S = RingBuffer;
209}
210
211impl BufferStrategyForKind for BoxTag {
212    type S = BoxBuffer;
213}
214
215impl BufferStrategyForKind for MultiPointTag {
216    type S = MultiPointBuffer;
217}
218
219impl BufferStrategyForKind for MultiLinestringTag {
220    type S = MultiLinestringBuffer;
221}
222
223impl BufferStrategyForKind for MultiPolygonTag {
224    type S = MultiPolygonBuffer;
225}
226
227/// Buffer a geometry using the public point and join strategies.
228///
229/// Mirrors `boost::geometry::buffer` from
230/// `boost/geometry/algorithms/detail/buffer/interface.hpp:246-273`. Cartesian,
231/// spherical, and geographic dispatch supports point, segment, linestring,
232/// ring, polygon, box, and all three homogeneous multi-geometry kinds. Point
233/// inputs use `point`, linear inputs use all five strategy roles, and areal
234/// inputs use signed distance and join policies.
235///
236/// # Errors
237///
238/// Returns [`OverlayError::Unsupported`] for non-finite distances, asymmetric
239/// areal distances, or degenerate linear inputs.
240#[inline]
241#[must_use = "buffering can fail and the generated geometry should be used"]
242pub fn buffer<G>(
243    geometry: &G,
244    distance: f64,
245    join: JoinStrategy,
246    point: PointStrategy,
247) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
248where
249    G: Geometry,
250    G::Kind: BufferStrategyForKind,
251    <<G::Point as Point>::Cs as CoordinateSystem>::Family:
252        DefaultBuffer<<<G::Point as Point>::Cs as CoordinateSystem>::Family>,
253    <G::Kind as BufferStrategyForKind>::S: BufferStrategy<G, DefaultBufferStrategy<G>>,
254{
255    let settings = BufferSettings {
256        distance: BufferDistanceStrategy::Symmetric(distance),
257        side: geometry_strategy::buffer::BufferSideStrategy::Straight,
258        join: match join {
259            JoinStrategy::Round { points_per_circle } => {
260                BufferJoinStrategy::Round { points_per_circle }
261            }
262            JoinStrategy::Miter => BufferJoinStrategy::Miter { limit: 5.0 },
263        },
264        end: BufferEndStrategy::Round {
265            points_per_circle: 36,
266        },
267        point: match point {
268            PointStrategy::Circle { points_per_circle } => {
269                BufferPointStrategy::Circle { points_per_circle }
270            }
271            PointStrategy::Square => BufferPointStrategy::Square,
272        },
273    };
274    buffer_with(geometry, settings)
275}
276
277/// Buffer a geometry with Boost's complete distance/side/join/end/point
278/// strategy bundle.
279///
280/// Mirrors the five explicit strategy arguments to `boost::geometry::buffer`
281/// from `algorithms/detail/buffer/interface.hpp:246-273`.
282///
283/// # Errors
284///
285/// Returns [`OverlayError::Unsupported`] for non-finite/inapplicable distance
286/// policies or degenerate linear input.
287#[inline]
288#[must_use = "buffering can fail and the generated geometry should be used"]
289pub fn buffer_with<G>(
290    geometry: &G,
291    settings: BufferSettings,
292) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
293where
294    G: Geometry,
295    G::Kind: BufferStrategyForKind,
296    <<G::Point as Point>::Cs as CoordinateSystem>::Family:
297        DefaultBuffer<<<G::Point as Point>::Cs as CoordinateSystem>::Family>,
298    <G::Kind as BufferStrategyForKind>::S: BufferStrategy<G, DefaultBufferStrategy<G>>,
299{
300    buffer_with_strategy(geometry, settings, DefaultBufferStrategy::<G>::default())
301}
302
303/// Buffer a geometry with explicit coordinate-system and five-role strategy
304/// bundles.
305///
306/// Mirrors the explicit strategy overload of `boost::geometry::buffer` from
307/// `algorithms/detail/buffer/interface.hpp:246-273`, together with the
308/// Cartesian, spherical, and geographic umbrella strategies under
309/// `strategies/buffer/`.
310///
311/// [`SphericalBuffer`] and [`GeographicBuffer`] use a geometry-centered local
312/// tangent projection before invoking the Cartesian offset engine. This keeps
313/// distance units explicit and `no_std` compatible, but is a local-extent
314/// approximation rather than Boost's per-segment geodesic construction.
315///
316/// # Errors
317///
318/// Returns [`OverlayError::Unsupported`] for invalid strategy values,
319/// non-finite/inapplicable distances, or degenerate linear input.
320#[inline]
321#[must_use = "buffering can fail and the generated geometry should be used"]
322#[allow(
323    clippy::needless_pass_by_value,
324    reason = "Boost buffer coordinate strategies are small value objects passed explicitly"
325)]
326pub fn buffer_with_strategy<G, CoordinateStrategy>(
327    geometry: &G,
328    settings: BufferSettings,
329    coordinate_strategy: CoordinateStrategy,
330) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
331where
332    G: Geometry,
333    G::Kind: BufferStrategyForKind,
334    <G::Kind as BufferStrategyForKind>::S: BufferStrategy<G, CoordinateStrategy>,
335{
336    <<G::Kind as BufferStrategyForKind>::S as Default>::default().apply(
337        geometry,
338        settings,
339        &coordinate_strategy,
340    )
341}
342
343/// A polygon buffered at a distance of zero.
344///
345/// C++: `buffer_inserter` builds an offsetted ring per input ring and then
346/// finds the turns between them, discards those inside the original, and
347/// traverses what is left. Where the offsetted rings do not meet each other
348/// there are no turns, nothing is discarded and nothing is traversed, and the
349/// rings themselves are the answer — which is the case
350/// `repair_one_polygon` needs and the case this arm answers.
351///
352/// A ring that does meet itself needs `check_turn_in_original` and the buffer
353/// traversal, which are not ported; that asks for something this arm cannot
354/// answer, and it says so rather than guessing.
355fn zero_width_polygon_buffer<G>(
356    polygon: &G,
357) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
358where
359    G: PolygonTrait,
360    G::Point: PointMut + Default + Copy,
361    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
362{
363    use crate::piece_collection::{ZeroWidthOutcome, zero_width_outcome, zero_width_rings};
364
365    let rings = zero_width_rings(polygon);
366    match zero_width_outcome(&rings) {
367        ZeroWidthOutcome::RingsStand => Ok(MultiPolygon(
368            rings.into_iter().map(Polygon::new).collect::<Vec<_>>(),
369        )),
370        ZeroWidthOutcome::NeedsTraversal => Err(OverlayError::Unsupported),
371    }
372}
373
374/// Implements the point arm selected by `buffer_all` at
375/// `algorithms/detail/buffer/interface.hpp:269-273`.
376impl<G> BufferStrategy<G, CartesianBuffer> for PointBuffer
377where
378    G: Point + PointMut + Default + Copy,
379    G::Scalar: CoordinateScalar + Into<f64> + FromF64,
380    <G::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
381{
382    fn apply(
383        &self,
384        point_geometry: &G,
385        settings: BufferSettings,
386        _coordinate_strategy: &CartesianBuffer,
387    ) -> Result<MultiPolygon<Polygon<G>>, OverlayError> {
388        let BufferDistanceStrategy::Symmetric(distance) = settings.distance else {
389            return Err(OverlayError::Unsupported);
390        };
391        if !distance.is_finite() {
392            return Err(OverlayError::Unsupported);
393        }
394        if distance <= 0.0 {
395            return Ok(MultiPolygon(alloc::vec![]));
396        }
397        let point = match settings.point {
398            BufferPointStrategy::Circle { points_per_circle } => {
399                PointStrategy::Circle { points_per_circle }
400            }
401            BufferPointStrategy::Square => PointStrategy::Square,
402        };
403        let ring = buffer_point(point_geometry, distance, point);
404        Ok(MultiPolygon(alloc::vec![Polygon::new(ring)]))
405    }
406}
407
408/// Implements the polygon arm selected by `buffer_all` at
409/// `algorithms/detail/buffer/interface.hpp:269-273`.
410impl<G> BufferStrategy<G, CartesianBuffer> for PolygonBuffer
411where
412    G: PolygonTrait,
413    G::Point: PointMut + Default + Copy,
414    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
415    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
416{
417    fn apply(
418        &self,
419        polygon: &G,
420        settings: BufferSettings,
421        _coordinate_strategy: &CartesianBuffer,
422    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
423        let BufferDistanceStrategy::Symmetric(distance) = settings.distance else {
424            return Err(OverlayError::Unsupported);
425        };
426        if !distance.is_finite() {
427            return Err(OverlayError::Unsupported);
428        }
429        if distance == 0.0 {
430            // C++: a zero-width buffer is not a no-op and not a special case
431            // either — `buffer_inserter` runs its whole pipeline, and every
432            // side simply offsets onto itself. It is what `repair_one_polygon`
433            // falls back on, so it has to answer.
434            return zero_width_polygon_buffer(polygon);
435        }
436        let outer = offset_ring(polygon.exterior(), distance, settings.join, true);
437        let inners = polygon
438            .interiors()
439            .map(|ring| offset_ring(ring, -distance, settings.join, false))
440            .collect::<Vec<_>>();
441        if offset_rings_need_dissolving(outer.as_ref(), &inners, distance) {
442            return dissolve_offset(polygon, distance, settings.join);
443        }
444        let Some(outer) = outer else {
445            return Ok(MultiPolygon(alloc::vec![]));
446        };
447        let inners = inners.into_iter().flatten().collect::<Vec<_>>();
448        let outer_vertices = distinct_vertices(&outer);
449        if inners.iter().any(|inner| {
450            let inner_vertices = distinct_vertices(inner);
451            outer_vertices
452                .iter()
453                .all(|point| point_in_or_on_ring(*point, &inner_vertices))
454        }) {
455            return Ok(MultiPolygon::new());
456        }
457
458        Ok(MultiPolygon(alloc::vec![Polygon::with_inners(
459            outer, inners,
460        )]))
461    }
462}
463
464impl<G> BufferStrategy<G, CartesianBuffer> for LinestringBuffer
465where
466    G: LinestringTrait,
467    G::Point: PointMut + Default + Copy,
468    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
469    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
470{
471    fn apply(
472        &self,
473        line: &G,
474        settings: BufferSettings,
475        _coordinate_strategy: &CartesianBuffer,
476    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
477        let (left, right) = match settings.distance {
478            BufferDistanceStrategy::Symmetric(distance) => (distance, distance),
479            BufferDistanceStrategy::Asymmetric { left, right } => (left, right),
480        };
481        if !left.is_finite() || !right.is_finite() || left < 0.0 || right < 0.0 {
482            return Err(OverlayError::Unsupported);
483        }
484        if left == 0.0 && right == 0.0 {
485            return Ok(MultiPolygon(alloc::vec![]));
486        }
487        let polygon = buffer_linestring(line, left, right, settings.join, settings.end)?;
488        Ok(MultiPolygon(alloc::vec![polygon]))
489    }
490}
491
492impl<G> BufferStrategy<G, CartesianBuffer> for SegmentBuffer
493where
494    G: SegmentTrait,
495    G::Point: PointMut + Default + Copy,
496    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
497    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
498{
499    fn apply(
500        &self,
501        segment: &G,
502        settings: BufferSettings,
503        coordinate_strategy: &CartesianBuffer,
504    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
505        let line: Linestring<G::Point> =
506            Linestring::from_vec(alloc::vec![segment_start(segment), segment_end(segment)]);
507        LinestringBuffer.apply(&line, settings, coordinate_strategy)
508    }
509}
510
511impl<G> BufferStrategy<G, CartesianBuffer> for RingBuffer
512where
513    G: RingTrait,
514    G::Point: PointMut + Default + Copy,
515    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
516    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
517{
518    fn apply(
519        &self,
520        ring: &G,
521        settings: BufferSettings,
522        coordinate_strategy: &CartesianBuffer,
523    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
524        let BufferDistanceStrategy::Symmetric(distance) = settings.distance else {
525            return Err(OverlayError::Unsupported);
526        };
527        if !distance.is_finite() || distance == 0.0 {
528            return Err(OverlayError::Unsupported);
529        }
530        // C++: `buffer_inserter<ring_tag>` is the polygon inserter over one
531        // ring. Its offset can cross itself just as a polygon's can, so it
532        // takes the polygon arm — and with it the dissolve.
533        let polygon: Polygon<G::Point> =
534            Polygon::new(Ring::from_vec(ring.points().copied().collect()));
535        PolygonBuffer.apply(&polygon, settings, coordinate_strategy)
536    }
537}
538
539impl<G> BufferStrategy<G, CartesianBuffer> for BoxBuffer
540where
541    G: BoxTrait,
542    G::Point: PointMut + Default + Copy,
543    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
544    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
545{
546    fn apply(
547        &self,
548        bounds: &G,
549        settings: BufferSettings,
550        coordinate_strategy: &CartesianBuffer,
551    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
552        let minimum = box_min(bounds);
553        let maximum = box_max(bounds);
554        let min_x = minimum.get::<0>().into();
555        let min_y = minimum.get::<1>().into();
556        let max_x = maximum.get::<0>().into();
557        let max_y = maximum.get::<1>().into();
558        let ring: Ring<G::Point> = Ring::from_vec(alloc::vec![
559            make_point(min_x, min_y),
560            make_point(min_x, max_y),
561            make_point(max_x, max_y),
562            make_point(max_x, min_y),
563            make_point(min_x, min_y),
564        ]);
565        RingBuffer.apply(&ring, settings, coordinate_strategy)
566    }
567}
568
569impl<G> BufferStrategy<G, CartesianBuffer> for MultiPointBuffer
570where
571    G: MultiPointTrait<ItemPoint = <G as Geometry>::Point>,
572    G::Point: PointMut + Default + Copy,
573    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
574    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
575{
576    fn apply(
577        &self,
578        points: &G,
579        settings: BufferSettings,
580        coordinate_strategy: &CartesianBuffer,
581    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
582        let mut output = MultiPolygon::new();
583        for point in points.points() {
584            output
585                .0
586                .extend(PointBuffer.apply(point, settings, coordinate_strategy)?.0);
587        }
588        crate::merge::merge_polygons(output.0)
589    }
590}
591
592impl<G> BufferStrategy<G, CartesianBuffer> for MultiLinestringBuffer
593where
594    G: MultiLinestringTrait,
595    G::Point: PointMut + Default + Copy,
596    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
597    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
598{
599    fn apply(
600        &self,
601        lines: &G,
602        settings: BufferSettings,
603        coordinate_strategy: &CartesianBuffer,
604    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
605        let mut output = MultiPolygon::new();
606        for line in lines.linestrings() {
607            output.0.extend(
608                LinestringBuffer
609                    .apply(line, settings, coordinate_strategy)?
610                    .0,
611            );
612        }
613        crate::merge::merge_polygons(output.0)
614    }
615}
616
617impl<G> BufferStrategy<G, CartesianBuffer> for MultiPolygonBuffer
618where
619    G: MultiPolygonTrait,
620    G::Point: PointMut + Default + Copy,
621    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
622    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
623{
624    fn apply(
625        &self,
626        polygons: &G,
627        settings: BufferSettings,
628        coordinate_strategy: &CartesianBuffer,
629    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
630        let mut output = MultiPolygon::new();
631        for polygon in polygons.polygons() {
632            output.0.extend(
633                PolygonBuffer
634                    .apply(polygon, settings, coordinate_strategy)?
635                    .0,
636            );
637        }
638        crate::merge::merge_polygons(output.0)
639    }
640}
641
642trait AngularCoordinateSystem {
643    type Units: AngleUnit;
644}
645
646impl<Units: AngleUnit> AngularCoordinateSystem for Spherical<Units> {
647    type Units = Units;
648}
649
650impl<Units: AngleUnit> AngularCoordinateSystem for Geographic<Units> {
651    type Units = Units;
652}
653
654#[derive(Debug, Clone, Copy)]
655struct LocalProjection {
656    longitude: f64,
657    latitude: f64,
658    east_scale: f64,
659    north_scale: f64,
660}
661
662impl LocalProjection {
663    fn project(self, longitude: f64, latitude: f64) -> (f64, f64) {
664        let mut delta_longitude = longitude - self.longitude;
665        if delta_longitude > core::f64::consts::PI {
666            delta_longitude -= 2.0 * core::f64::consts::PI;
667        } else if delta_longitude < -core::f64::consts::PI {
668            delta_longitude += 2.0 * core::f64::consts::PI;
669        }
670        (
671            delta_longitude * self.east_scale,
672            (latitude - self.latitude) * self.north_scale,
673        )
674    }
675
676    fn unproject(self, x: f64, y: f64) -> (f64, f64) {
677        let mut longitude = self.longitude + x / self.east_scale;
678        if longitude > core::f64::consts::PI {
679            longitude -= 2.0 * core::f64::consts::PI;
680        } else if longitude < -core::f64::consts::PI {
681            longitude += 2.0 * core::f64::consts::PI;
682        }
683        (longitude, self.latitude + y / self.north_scale)
684    }
685}
686
687trait AngularBufferProjection {
688    fn projection(&self, longitude: f64, latitude: f64) -> Result<LocalProjection, OverlayError>;
689}
690
691impl AngularBufferProjection for SphericalBuffer {
692    fn projection(&self, longitude: f64, latitude: f64) -> Result<LocalProjection, OverlayError> {
693        if !self.radius.is_finite() || self.radius <= 0.0 {
694            return Err(OverlayError::Unsupported);
695        }
696        let longitude_scale = cos(latitude);
697        if longitude_scale.abs() <= f64::EPSILON {
698            return Err(OverlayError::Unsupported);
699        }
700        let east_scale = self.radius * longitude_scale;
701        Ok(LocalProjection {
702            longitude,
703            latitude,
704            east_scale,
705            north_scale: self.radius,
706        })
707    }
708}
709
710impl AngularBufferProjection for GeographicBuffer {
711    fn projection(&self, longitude: f64, latitude: f64) -> Result<LocalProjection, OverlayError> {
712        let spheroid = self.spheroid;
713        if !spheroid.equatorial_radius.is_finite()
714            || spheroid.equatorial_radius <= 0.0
715            || !spheroid.flattening.is_finite()
716            || !(0.0..1.0).contains(&spheroid.flattening)
717        {
718            return Err(OverlayError::Unsupported);
719        }
720
721        let eccentricity_squared = spheroid.eccentricity_squared();
722        let sin_latitude = sin(latitude);
723        let denominator = sqrt(1.0 - eccentricity_squared * sin_latitude * sin_latitude);
724        let prime_vertical = spheroid.equatorial_radius / denominator;
725        let meridional = spheroid.equatorial_radius * (1.0 - eccentricity_squared)
726            / (denominator * denominator * denominator);
727        let longitude_scale = cos(latitude);
728        if longitude_scale.abs() <= f64::EPSILON {
729            return Err(OverlayError::Unsupported);
730        }
731        let east_scale = prime_vertical * longitude_scale;
732        Ok(LocalProjection {
733            longitude,
734            latitude,
735            east_scale,
736            north_scale: meridional,
737        })
738    }
739}
740
741fn angular_coordinates<P>(point: &P) -> (f64, f64)
742where
743    P: Point,
744    P::Scalar: Into<f64>,
745    P::Cs: AngularCoordinateSystem,
746{
747    let longitude = <P::Cs as AngularCoordinateSystem>::Units::to_radians(point.get::<0>().into());
748    let latitude = <P::Cs as AngularCoordinateSystem>::Units::to_radians(point.get::<1>().into());
749    (longitude, latitude)
750}
751
752fn angular_point<P>(longitude: f64, latitude: f64) -> P
753where
754    P: PointMut + Default,
755    P::Scalar: FromF64,
756    P::Cs: AngularCoordinateSystem,
757{
758    let mut point = P::default();
759    let longitude = <P::Cs as AngularCoordinateSystem>::Units::from_radians(longitude);
760    let latitude = <P::Cs as AngularCoordinateSystem>::Units::from_radians(latitude);
761    point.set::<0>(P::Scalar::from_f64(longitude));
762    point.set::<1>(P::Scalar::from_f64(latitude));
763    point
764}
765
766fn projection_center(coordinates: &[(f64, f64)]) -> Result<(f64, f64), OverlayError> {
767    if coordinates.is_empty() {
768        return Err(OverlayError::Unsupported);
769    }
770    let mut longitude_sine = 0.0;
771    let mut longitude_cosine = 0.0;
772    let mut latitude = 0.0;
773    for &(longitude, point_latitude) in coordinates {
774        longitude_sine += sin(longitude);
775        longitude_cosine += cos(longitude);
776        latitude += point_latitude;
777    }
778    let count = coordinates.len() as f64;
779    Ok((atan2(longitude_sine, longitude_cosine), latitude / count))
780}
781
782type ProjectedPoint = Point2D<f64, Cartesian>;
783
784fn projected_point<P>(point: &P, projection: LocalProjection) -> ProjectedPoint
785where
786    P: Point,
787    P::Scalar: Into<f64>,
788    P::Cs: AngularCoordinateSystem,
789{
790    let (longitude, latitude) = angular_coordinates(point);
791    let (x, y) = projection.project(longitude, latitude);
792    ProjectedPoint::new(x, y)
793}
794
795fn projected_ring<R>(ring: &R, projection: LocalProjection) -> Ring<ProjectedPoint>
796where
797    R: RingTrait,
798    R::Point: Point,
799    <R::Point as Point>::Scalar: Into<f64>,
800    <R::Point as Point>::Cs: AngularCoordinateSystem,
801{
802    Ring::from_vec(
803        ring.points()
804            .map(|point| projected_point(point, projection))
805            .collect(),
806    )
807}
808
809fn projected_polygon<G>(polygon: &G, projection: LocalProjection) -> Polygon<ProjectedPoint>
810where
811    G: PolygonTrait,
812    G::Point: Point,
813    <G::Point as Point>::Scalar: Into<f64>,
814    <G::Point as Point>::Cs: AngularCoordinateSystem,
815{
816    Polygon::with_inners(
817        projected_ring(polygon.exterior(), projection),
818        polygon
819            .interiors()
820            .map(|ring| projected_ring(ring, projection))
821            .collect(),
822    )
823}
824
825fn unprojected_buffer<P>(
826    polygons: MultiPolygon<Polygon<ProjectedPoint>>,
827    projection: LocalProjection,
828) -> MultiPolygon<Polygon<P>>
829where
830    P: PointMut + Default,
831    P::Scalar: FromF64,
832    P::Cs: AngularCoordinateSystem,
833{
834    MultiPolygon::from_vec(
835        polygons
836            .0
837            .into_iter()
838            .map(|polygon| {
839                let outer = Ring::from_vec(
840                    polygon
841                        .outer
842                        .0
843                        .into_iter()
844                        .map(|point| {
845                            let (longitude, latitude) = projection.unproject(point.x(), point.y());
846                            angular_point(longitude, latitude)
847                        })
848                        .collect(),
849                );
850                let inners = polygon
851                    .inners
852                    .into_iter()
853                    .map(|ring| {
854                        Ring::from_vec(
855                            ring.0
856                                .into_iter()
857                                .map(|point| {
858                                    let (longitude, latitude) =
859                                        projection.unproject(point.x(), point.y());
860                                    angular_point(longitude, latitude)
861                                })
862                                .collect(),
863                        )
864                    })
865                    .collect();
866                Polygon::with_inners(outer, inners)
867            })
868            .collect(),
869    )
870}
871
872fn projection_for_points<'a, P>(
873    points: impl IntoIterator<Item = &'a P>,
874    strategy: &impl AngularBufferProjection,
875) -> Result<LocalProjection, OverlayError>
876where
877    P: Point + 'a,
878    P::Scalar: Into<f64>,
879    P::Cs: AngularCoordinateSystem,
880{
881    let coordinates: Vec<_> = points.into_iter().map(angular_coordinates).collect();
882    let (longitude, latitude) = projection_center(&coordinates)?;
883    strategy.projection(longitude, latitude)
884}
885
886fn projected_point_apply<P>(
887    point: &P,
888    settings: BufferSettings,
889    strategy: &impl AngularBufferProjection,
890) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
891where
892    P: Point + PointMut + Default + Copy,
893    P::Scalar: CoordinateScalar + Into<f64> + FromF64,
894    P::Cs: AngularCoordinateSystem,
895{
896    let projection = projection_for_points(core::iter::once(point), strategy)?;
897    let point = projected_point(point, projection);
898    let output = PointBuffer.apply(&point, settings, &CartesianBuffer)?;
899    Ok(unprojected_buffer(output, projection))
900}
901
902fn projected_linestring_apply<L>(
903    line: &L,
904    settings: BufferSettings,
905    strategy: &impl AngularBufferProjection,
906) -> Result<MultiPolygon<Polygon<L::Point>>, OverlayError>
907where
908    L: LinestringTrait,
909    L::Point: PointMut + Default + Copy,
910    <L::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
911    <L::Point as Point>::Cs: AngularCoordinateSystem,
912{
913    let projection = projection_for_points(line.points(), strategy)?;
914    let projected = Linestring::from_vec(
915        line.points()
916            .map(|point| projected_point(point, projection))
917            .collect(),
918    );
919    let output = LinestringBuffer.apply(&projected, settings, &CartesianBuffer)?;
920    Ok(unprojected_buffer(output, projection))
921}
922
923fn projected_ring_apply<R>(
924    ring: &R,
925    settings: BufferSettings,
926    strategy: &impl AngularBufferProjection,
927) -> Result<MultiPolygon<Polygon<R::Point>>, OverlayError>
928where
929    R: RingTrait,
930    R::Point: PointMut + Default + Copy,
931    <R::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
932    <R::Point as Point>::Cs: AngularCoordinateSystem,
933{
934    let projection = projection_for_points(ring.points(), strategy)?;
935    let output = RingBuffer.apply(
936        &projected_ring(ring, projection),
937        settings,
938        &CartesianBuffer,
939    )?;
940    Ok(unprojected_buffer(output, projection))
941}
942
943fn projected_polygon_apply<G>(
944    polygon: &G,
945    settings: BufferSettings,
946    strategy: &impl AngularBufferProjection,
947) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
948where
949    G: PolygonTrait,
950    G::Point: PointMut + Default + Copy,
951    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
952    <G::Point as Point>::Cs: AngularCoordinateSystem,
953{
954    let mut coordinates = polygon
955        .exterior()
956        .points()
957        .map(angular_coordinates)
958        .collect::<Vec<_>>();
959    for ring in polygon.interiors() {
960        coordinates.extend(ring.points().map(angular_coordinates));
961    }
962    let (longitude, latitude) = projection_center(&coordinates)?;
963    let projection = strategy.projection(longitude, latitude)?;
964    let output = PolygonBuffer.apply(
965        &projected_polygon(polygon, projection),
966        settings,
967        &CartesianBuffer,
968    )?;
969    Ok(unprojected_buffer(output, projection))
970}
971
972macro_rules! impl_angular_buffer_strategy {
973    ($strategy:ty, $family:ty) => {
974        impl<G> BufferStrategy<G, $strategy> for PointBuffer
975        where
976            G: Point + PointMut + Default + Copy,
977            G::Scalar: CoordinateScalar + Into<f64> + FromF64,
978            G::Cs: AngularCoordinateSystem,
979            <G::Cs as CoordinateSystem>::Family: SameAs<$family>,
980        {
981            fn apply(
982                &self,
983                geometry: &G,
984                settings: BufferSettings,
985                coordinate_strategy: &$strategy,
986            ) -> Result<MultiPolygon<Polygon<G>>, OverlayError> {
987                projected_point_apply(geometry, settings, coordinate_strategy)
988            }
989        }
990
991        impl<G> BufferStrategy<G, $strategy> for LinestringBuffer
992        where
993            G: LinestringTrait,
994            G::Point: PointMut + Default + Copy,
995            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
996            <G::Point as Point>::Cs: AngularCoordinateSystem,
997            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
998        {
999            fn apply(
1000                &self,
1001                geometry: &G,
1002                settings: BufferSettings,
1003                coordinate_strategy: &$strategy,
1004            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1005                projected_linestring_apply(geometry, settings, coordinate_strategy)
1006            }
1007        }
1008
1009        impl<G> BufferStrategy<G, $strategy> for SegmentBuffer
1010        where
1011            G: SegmentTrait,
1012            G::Point: PointMut + Default + Copy,
1013            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1014            <G::Point as Point>::Cs: AngularCoordinateSystem,
1015            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1016        {
1017            fn apply(
1018                &self,
1019                geometry: &G,
1020                settings: BufferSettings,
1021                coordinate_strategy: &$strategy,
1022            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1023                let line = Linestring::from_vec(alloc::vec![
1024                    segment_start(geometry),
1025                    segment_end(geometry),
1026                ]);
1027                projected_linestring_apply(&line, settings, coordinate_strategy)
1028            }
1029        }
1030
1031        impl<G> BufferStrategy<G, $strategy> for RingBuffer
1032        where
1033            G: RingTrait,
1034            G::Point: PointMut + Default + Copy,
1035            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1036            <G::Point as Point>::Cs: AngularCoordinateSystem,
1037            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1038        {
1039            fn apply(
1040                &self,
1041                geometry: &G,
1042                settings: BufferSettings,
1043                coordinate_strategy: &$strategy,
1044            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1045                projected_ring_apply(geometry, settings, coordinate_strategy)
1046            }
1047        }
1048
1049        impl<G> BufferStrategy<G, $strategy> for PolygonBuffer
1050        where
1051            G: PolygonTrait,
1052            G::Point: PointMut + Default + Copy,
1053            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1054            <G::Point as Point>::Cs: AngularCoordinateSystem,
1055            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1056        {
1057            fn apply(
1058                &self,
1059                geometry: &G,
1060                settings: BufferSettings,
1061                coordinate_strategy: &$strategy,
1062            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1063                projected_polygon_apply(geometry, settings, coordinate_strategy)
1064            }
1065        }
1066
1067        impl<G> BufferStrategy<G, $strategy> for BoxBuffer
1068        where
1069            G: BoxTrait,
1070            G::Point: PointMut + Default + Copy,
1071            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1072            <G::Point as Point>::Cs: AngularCoordinateSystem,
1073            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1074        {
1075            fn apply(
1076                &self,
1077                geometry: &G,
1078                settings: BufferSettings,
1079                coordinate_strategy: &$strategy,
1080            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1081                let minimum = box_min(geometry);
1082                let maximum = box_max(geometry);
1083                let projection = projection_for_points([&minimum, &maximum], coordinate_strategy)?;
1084                let projected = ModelBox::from_corners(
1085                    projected_point(&minimum, projection),
1086                    projected_point(&maximum, projection),
1087                );
1088                let output = BoxBuffer.apply(&projected, settings, &CartesianBuffer)?;
1089                Ok(unprojected_buffer(output, projection))
1090            }
1091        }
1092
1093        impl<G> BufferStrategy<G, $strategy> for MultiPointBuffer
1094        where
1095            G: MultiPointTrait<ItemPoint = <G as Geometry>::Point>,
1096            G::Point: PointMut + Default + Copy,
1097            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1098            <G::Point as Point>::Cs: AngularCoordinateSystem,
1099            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1100        {
1101            fn apply(
1102                &self,
1103                geometry: &G,
1104                settings: BufferSettings,
1105                coordinate_strategy: &$strategy,
1106            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1107                let projection = projection_for_points(geometry.points(), coordinate_strategy)?;
1108                let projected = MultiPoint::from_vec(
1109                    geometry
1110                        .points()
1111                        .map(|point| projected_point(point, projection))
1112                        .collect(),
1113                );
1114                let output = MultiPointBuffer.apply(&projected, settings, &CartesianBuffer)?;
1115                Ok(unprojected_buffer(output, projection))
1116            }
1117        }
1118
1119        impl<G> BufferStrategy<G, $strategy> for MultiLinestringBuffer
1120        where
1121            G: MultiLinestringTrait,
1122            G::Point: PointMut + Default + Copy,
1123            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1124            <G::Point as Point>::Cs: AngularCoordinateSystem,
1125            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1126        {
1127            fn apply(
1128                &self,
1129                geometry: &G,
1130                settings: BufferSettings,
1131                coordinate_strategy: &$strategy,
1132            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1133                let coordinates = geometry
1134                    .linestrings()
1135                    .flat_map(|line| line.points().map(angular_coordinates))
1136                    .collect::<Vec<_>>();
1137                let (longitude, latitude) = projection_center(&coordinates)?;
1138                let projection = coordinate_strategy.projection(longitude, latitude)?;
1139                let projected = MultiLinestring::from_vec(
1140                    geometry
1141                        .linestrings()
1142                        .map(|line| {
1143                            Linestring::from_vec(
1144                                line.points()
1145                                    .map(|point| projected_point(point, projection))
1146                                    .collect(),
1147                            )
1148                        })
1149                        .collect(),
1150                );
1151                let output = MultiLinestringBuffer.apply(&projected, settings, &CartesianBuffer)?;
1152                Ok(unprojected_buffer(output, projection))
1153            }
1154        }
1155
1156        impl<G> BufferStrategy<G, $strategy> for MultiPolygonBuffer
1157        where
1158            G: MultiPolygonTrait,
1159            G::Point: PointMut + Default + Copy,
1160            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1161            <G::Point as Point>::Cs: AngularCoordinateSystem,
1162            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1163        {
1164            fn apply(
1165                &self,
1166                geometry: &G,
1167                settings: BufferSettings,
1168                coordinate_strategy: &$strategy,
1169            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1170                let coordinates = geometry
1171                    .polygons()
1172                    .flat_map(|polygon| {
1173                        polygon
1174                            .exterior()
1175                            .points()
1176                            .chain(polygon.interiors().flat_map(RingTrait::points))
1177                            .map(angular_coordinates)
1178                    })
1179                    .collect::<Vec<_>>();
1180                let (longitude, latitude) = projection_center(&coordinates)?;
1181                let projection = coordinate_strategy.projection(longitude, latitude)?;
1182                let projected = MultiPolygon::from_vec(
1183                    geometry
1184                        .polygons()
1185                        .map(|polygon| projected_polygon(polygon, projection))
1186                        .collect(),
1187                );
1188                let output = MultiPolygonBuffer.apply(&projected, settings, &CartesianBuffer)?;
1189                Ok(unprojected_buffer(output, projection))
1190            }
1191        }
1192    };
1193}
1194
1195impl_angular_buffer_strategy!(SphericalBuffer, SphericalFamily);
1196impl_angular_buffer_strategy!(GeographicBuffer, GeographicFamily);
1197
1198/// Buffer a point by `distance`, producing the disc (or square)
1199/// approximation.
1200///
1201/// Mirrors the point arm of `boost::geometry::buffer` with a
1202/// `point_circle` / `point_square` strategy
1203/// (`strategies/buffer/buffer_point_circle.hpp`).
1204///
1205/// # Examples
1206///
1207/// ```
1208/// use geometry_cs::Cartesian;
1209/// use geometry_model::Point2D;
1210/// use geometry_overlay::buffer::{buffer_point, PointStrategy};
1211/// use geometry_algorithm::ring_area;
1212///
1213/// type P = Point2D<f64, Cartesian>;
1214/// let disc = buffer_point(&P::new(0.0, 0.0), 1.0, PointStrategy::Circle { points_per_circle: 360 });
1215/// // Area of the 360-gon closely approximates π.
1216/// assert!((ring_area(&disc).abs() - core::f64::consts::PI).abs() < 1e-3);
1217/// ```
1218#[inline]
1219#[must_use]
1220pub fn buffer_point<P>(center: &P, distance: f64, strategy: PointStrategy) -> Ring<P>
1221where
1222    P: PointMut + Default + Copy,
1223    P::Scalar: CoordinateScalar + Into<f64> + FromF64,
1224    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
1225{
1226    let cx: f64 = center.get::<0>().into();
1227    let cy: f64 = center.get::<1>().into();
1228    match strategy {
1229        PointStrategy::Circle { points_per_circle } => {
1230            circle_ring(cx, cy, distance, points_per_circle.max(3))
1231        }
1232        PointStrategy::Square => {
1233            let d = distance;
1234            // Fully-qualified `alloc::vec!`: only the `Vec` *type* is
1235            // imported (line 33), and the bare `vec!` macro is not in the
1236            // `no_std` prelude — matches the crate idiom in `assemble.rs`
1237            // / `traverse/state.rs`.
1238            Ring::from_vec(alloc::vec![
1239                make_point(cx - d, cy - d),
1240                make_point(cx - d, cy + d),
1241                make_point(cx + d, cy + d),
1242                make_point(cx + d, cy - d),
1243                make_point(cx - d, cy - d),
1244            ])
1245        }
1246    }
1247}
1248
1249/// Buffer a **convex** polygon outward by a positive `distance`, rounding
1250/// the corners per `join`.
1251///
1252/// Each vertex of a convex polygon becomes a circular arc of radius
1253/// `distance` in the offset boundary; the arcs are joined by the offset
1254/// edges. Mirrors the convex case of `boost::geometry::buffer`
1255/// (`algorithms/buffer.hpp`) with a `join_round` strategy.
1256///
1257/// # Panics
1258///
1259/// Does not panic; a polygon with fewer than 3 exterior vertices returns
1260/// an empty ring's polygon.
1261///
1262/// # Examples
1263///
1264/// ```
1265/// use geometry_cs::Cartesian;
1266/// use geometry_model::{polygon, Point2D, Polygon};
1267/// use geometry_overlay::buffer::{buffer_convex_polygon, JoinStrategy};
1268/// use geometry_algorithm::ring_area;
1269/// use geometry_trait::Polygon as _;
1270///
1271/// type P = Point2D<f64, Cartesian>;
1272/// let sq: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
1273/// let grown = buffer_convex_polygon(&sq, 1.0, JoinStrategy::Round { points_per_circle: 720 });
1274/// // Area = s² + 4·s·d + π·d² = 4 + 8 + π.
1275/// let expected = 4.0 + 8.0 + core::f64::consts::PI;
1276/// assert!((ring_area(grown.exterior()).abs() - expected).abs() < 5e-2);
1277/// ```
1278#[inline]
1279#[must_use]
1280pub fn buffer_convex_polygon<G, P>(polygon: &G, distance: f64, join: JoinStrategy) -> Polygon<P>
1281where
1282    G: PolygonTrait<Point = P>,
1283    P: PointMut + Default + Copy,
1284    P::Scalar: CoordinateScalar + Into<f64> + FromF64,
1285    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
1286{
1287    let strategy = match join {
1288        JoinStrategy::Round { points_per_circle } => {
1289            BufferJoinStrategy::Round { points_per_circle }
1290        }
1291        JoinStrategy::Miter => BufferJoinStrategy::Miter {
1292            limit: f64::INFINITY,
1293        },
1294    };
1295    offset_ring(polygon.exterior(), distance, strategy, true)
1296        .map_or_else(|| Polygon::new(Ring::new()), Polygon::new)
1297}
1298
1299fn offset_ring<R, P>(
1300    ring: &R,
1301    distance: f64,
1302    join: BufferJoinStrategy,
1303    clockwise: bool,
1304) -> Option<Ring<P>>
1305where
1306    R: RingTrait<Point = P>,
1307    P: PointMut + Default + Copy,
1308    P::Scalar: Into<f64> + FromF64,
1309{
1310    let mut vertices = distinct_vertices(ring);
1311    if vertices.len() < 3 || !distance.is_finite() || distance == 0.0 {
1312        return None;
1313    }
1314    if signed_area_ccw_positive(&vertices) < 0.0 {
1315        vertices.reverse();
1316    }
1317
1318    let count = vertices.len();
1319    let mut boundary = Vec::new();
1320    for index in 0..count {
1321        let previous = vertices[(index + count - 1) % count];
1322        let vertex = vertices[index];
1323        let next = vertices[(index + 1) % count];
1324        let incoming = (vertex.0 - previous.0, vertex.1 - previous.1);
1325        let outgoing = (next.0 - vertex.0, next.1 - vertex.1);
1326        let incoming_normal = outward_normal(incoming.0, incoming.1);
1327        let outgoing_normal = outward_normal(outgoing.0, outgoing.1);
1328        let before = (
1329            vertex.0 + incoming_normal.0 * distance,
1330            vertex.1 + incoming_normal.1 * distance,
1331        );
1332        let after = (
1333            vertex.0 + outgoing_normal.0 * distance,
1334            vertex.1 + outgoing_normal.1 * distance,
1335        );
1336        let intersection = line_intersection(before, incoming, after, outgoing);
1337        let cross = incoming.0 * outgoing.1 - incoming.1 * outgoing.0;
1338        let exterior_join = cross * distance > 0.0;
1339
1340        if !exterior_join {
1341            if let Some(point) = intersection {
1342                boundary.push(point);
1343            } else {
1344                boundary.push(after);
1345            }
1346            continue;
1347        }
1348
1349        push_join_points(
1350            join,
1351            vertex,
1352            before,
1353            after,
1354            intersection,
1355            distance,
1356            &mut boundary,
1357        );
1358    }
1359
1360    boundary.dedup();
1361    if boundary.len() < 3 || signed_area_ccw_positive(&boundary).abs() <= f64::EPSILON {
1362        return None;
1363    }
1364    if distance < 0.0 {
1365        let clearance = distance.abs();
1366        let tolerance = mul_add(clearance, 1e-9, f64::EPSILON * 16.0);
1367        if boundary.iter().any(|point| {
1368            !point_in_or_on_ring(*point, &vertices)
1369                || minimum_boundary_distance(*point, &vertices) + tolerance < clearance
1370        }) {
1371            return None;
1372        }
1373    }
1374    if clockwise == (signed_area_ccw_positive(&boundary) > 0.0) {
1375        boundary.reverse();
1376    }
1377    boundary.push(boundary[0]);
1378    Some(Ring::from_vec(
1379        boundary
1380            .into_iter()
1381            .map(|(x, y)| make_point(x, y))
1382            .collect(),
1383    ))
1384}
1385
1386/// The points the join strategy contributes at a corner the offset turns
1387/// away from: from `before`, the end of the incoming side's offset, to
1388/// `after`, the start of the outgoing side's. `intersection` is where the two
1389/// offset lines meet, the miter point.
1390///
1391/// C++: `join_round::apply` and `join_miter::apply`, whose output range the
1392/// caller appends. Shared by the offsetted ring and the join piece the
1393/// dissolve builds for the same corner, so the two describe one offset.
1394fn push_join_points(
1395    join: BufferJoinStrategy,
1396    vertex: (f64, f64),
1397    before: (f64, f64),
1398    after: (f64, f64),
1399    intersection: Option<(f64, f64)>,
1400    distance: f64,
1401    boundary: &mut Vec<(f64, f64)>,
1402) {
1403    match join {
1404        BufferJoinStrategy::Round { points_per_circle } => {
1405            // The ring is walked counter-clockwise, so an outward offset
1406            // (`distance > 0`) rounds a convex corner counter-clockwise,
1407            // while an inward offset rounds a reflex corner the other
1408            // way; forcing one direction sweeps the long way through
1409            // the material at the other.
1410            boundary.push(before);
1411            push_arc_between(
1412                boundary,
1413                vertex,
1414                before,
1415                after,
1416                distance.abs(),
1417                points_per_circle.max(4),
1418                distance > 0.0,
1419            );
1420            boundary.push(after);
1421        }
1422        BufferJoinStrategy::Miter { limit } => {
1423            if let Some(point) = intersection {
1424                let miter_length = hypot(point.0 - vertex.0, point.1 - vertex.1);
1425                if point.0.is_finite()
1426                    && point.1.is_finite()
1427                    && miter_length <= limit.max(1.0) * distance.abs()
1428                {
1429                    boundary.push(point);
1430                } else {
1431                    boundary.push(before);
1432                    boundary.push(after);
1433                }
1434            } else {
1435                boundary.push(before);
1436                boundary.push(after);
1437            }
1438        }
1439    }
1440}
1441
1442/// Whether the offsetted rings can stand as the answer, or the offset has to
1443/// be rebuilt from its pieces.
1444///
1445/// C++: `buffered_piece_collection` never trusts an offsetted ring — it finds
1446/// the turns between every piece and traverses them whatever the input. This
1447/// port keeps the offsetted ring wherever it is already the answer, which is
1448/// whenever no ring crosses itself or another and every erosion kept its
1449/// clearance, and rebuilds the offset from the pieces only where a ring is
1450/// not simple: a notch narrower than twice the distance closes, a neck
1451/// thinner than that pinches off, a hole's arm fills in.
1452///
1453/// `outer` is the exterior's offsetted ring and `inners` the holes', each
1454/// `None` where `offset_ring` declined. A growing polygon's hole or an
1455/// eroding polygon's exterior that declined may have collapsed only in part,
1456/// so both go to the pieces; an eroding polygon's hole declines only with
1457/// fewer than three distinct vertices, and encloses nothing either way.
1458fn offset_rings_need_dissolving<P>(
1459    outer: Option<&Ring<P>>,
1460    inners: &[Option<Ring<P>>],
1461    distance: f64,
1462) -> bool
1463where
1464    P: PointMut + Default + Copy,
1465    P::Scalar: CoordinateScalar + Into<f64>,
1466{
1467    let eroding = distance < 0.0;
1468    let Some(outer) = outer else {
1469        return eroding;
1470    };
1471    if ring_crosses_itself(outer) {
1472        return true;
1473    }
1474    let mut kept: Vec<&Ring<P>> = Vec::new();
1475    for inner in inners {
1476        match inner {
1477            Some(inner) => kept.push(inner),
1478            None if eroding => {}
1479            None => return true,
1480        }
1481    }
1482    if kept.iter().copied().any(ring_crosses_itself) {
1483        return true;
1484    }
1485    if !eroding {
1486        // Growth moves the exterior outward and every hole inward, away from
1487        // one another; only erosion can run them into each other.
1488        return false;
1489    }
1490    kept.iter().enumerate().any(|(index, inner)| {
1491        rings_cross(outer, inner)
1492            || kept[index + 1..]
1493                .iter()
1494                .any(|other| rings_cross(inner, other))
1495    })
1496}
1497
1498/// The sides of a closed ring as coordinate pairs, with the box of each.
1499fn ring_sides<P>(ring: &Ring<P>) -> Vec<(P, P, [f64; 4])>
1500where
1501    P: Point + Copy,
1502    P::Scalar: Into<f64>,
1503{
1504    let points: Vec<P> = ring.points().copied().collect();
1505    points
1506        .windows(2)
1507        .map(|pair| {
1508            let (ax, ay): (f64, f64) = (pair[0].get::<0>().into(), pair[0].get::<1>().into());
1509            let (bx, by): (f64, f64) = (pair[1].get::<0>().into(), pair[1].get::<1>().into());
1510            (
1511                pair[0],
1512                pair[1],
1513                [ax.min(bx), ay.min(by), ax.max(bx), ay.max(by)],
1514            )
1515        })
1516        .collect()
1517}
1518
1519/// Whether two sides meet, decided by the exact predicate behind a box test
1520/// that keeps the pass cheap for the long arcs a round join produces.
1521/// Coordinates the predicate cannot judge count as not meeting, which leaves
1522/// such a ring on the path it took before the dissolve existed.
1523fn sides_meet<P>(one: &(P, P, [f64; 4]), two: &(P, P, [f64; 4])) -> bool
1524where
1525    P: PointMut + Default + Copy,
1526    P::Scalar: CoordinateScalar + Into<f64>,
1527{
1528    let (a, b) = (&one.2, &two.2);
1529    if a[0] > b[2] || b[0] > a[2] || a[1] > b[3] || b[1] > a[3] {
1530        return false;
1531    }
1532    !matches!(
1533        segment_intersection::<Segment<P>, P>(
1534            &Segment::new(one.0, one.1),
1535            &Segment::new(two.0, two.1)
1536        ),
1537        SegmentIntersection::Disjoint | SegmentIntersection::OutOfRange
1538    )
1539}
1540
1541/// Whether any two sides of the ring that are not neighbours meet.
1542fn ring_crosses_itself<P>(ring: &Ring<P>) -> bool
1543where
1544    P: PointMut + Default + Copy,
1545    P::Scalar: CoordinateScalar + Into<f64>,
1546{
1547    let sides = ring_sides(ring);
1548    let count = sides.len();
1549    (0..count).any(|first| {
1550        let last_neighbour = if first == 0 { count - 1 } else { count };
1551        (first + 2..last_neighbour).any(|second| sides_meet(&sides[first], &sides[second]))
1552    })
1553}
1554
1555/// Whether any side of one ring meets any side of the other.
1556fn rings_cross<P>(one: &Ring<P>, two: &Ring<P>) -> bool
1557where
1558    P: PointMut + Default + Copy,
1559    P::Scalar: CoordinateScalar + Into<f64>,
1560{
1561    let one = ring_sides(one);
1562    let two = ring_sides(two);
1563    one.iter()
1564        .any(|side| two.iter().any(|other| sides_meet(side, other)))
1565}
1566
1567/// The offset rebuilt from Boost's pieces, for a polygon whose offsetted
1568/// rings cannot be trusted.
1569///
1570/// C++: `buffer_inserter` cuts each ring into pieces — a `buffered_segment`
1571/// per side, offset by the distance, and a `buffered_join` at each corner the
1572/// join strategy rounds or miters — and `buffered_piece_collection` finds the
1573/// turns between them and traverses them, so that a stretch of one piece's
1574/// offset that ends up inside another piece never reaches the outline. The
1575/// pieces here are the same ones; in place of Boost's turn machinery they go
1576/// through the overlay engine: merged into one multi-polygon and then,
1577/// growing, unioned with the polygon or, eroding, taken away from it. Each
1578/// piece is the region within the distance of one side or one corner, so the
1579/// polygon with their union is exactly the grown shape and the polygon less
1580/// their union exactly the eroded one — a notch that closes, a neck that
1581/// pinches off into separate polygons, a hole that fills in part.
1582///
1583/// The work grows with the square of the result's vertex count, which is why
1584/// this is kept for the rings the offsetted ring gets wrong.
1585fn dissolve_offset<G, P>(
1586    polygon: &G,
1587    distance: f64,
1588    join: BufferJoinStrategy,
1589) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
1590where
1591    G: PolygonTrait<Point = P>,
1592    P: PointMut + Default + Copy,
1593    P::Scalar: CoordinateScalar + Into<f64> + FromF64,
1594    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
1595{
1596    let mut pieces: Vec<Polygon<P>> = Vec::new();
1597    push_ring_pieces(polygon.exterior(), distance, join, &mut pieces);
1598    for ring in polygon.interiors() {
1599        // A hole's outside is the polygon's inside, so it offsets the other
1600        // way round.
1601        push_ring_pieces(ring, -distance, join, &mut pieces);
1602    }
1603    let pieces = merge_pieces(pieces)?;
1604    if pieces.0.is_empty() {
1605        return Ok(MultiPolygon::new());
1606    }
1607    let original: MultiPolygon<Polygon<P>> = MultiPolygon(alloc::vec![Polygon::with_inners(
1608        Ring::from_vec(polygon.exterior().points().copied().collect()),
1609        polygon
1610            .interiors()
1611            .map(|ring| Ring::from_vec(ring.points().copied().collect()))
1612            .collect(),
1613    )]);
1614    if distance > 0.0 {
1615        union_multi(&original, &pieces)
1616    } else {
1617        difference_multi(&original, &pieces)
1618    }
1619}
1620
1621/// The pieces one ring contributes, offset by `distance` along its outward
1622/// normals: a quadrilateral per side and, at each corner the offset turns
1623/// away from the ring, the join's wedge. A negative distance puts them on
1624/// the ring's inner side, which erodes an exterior and grows a hole.
1625///
1626/// C++: `buffer_range::iterate` — `add_side_piece` for every side and
1627/// `add_join` between consecutive sides, then the closing join. The corner
1628/// points are the ones `offset_ring` places, so the pieces and the offsetted
1629/// ring describe one offset.
1630fn push_ring_pieces<R, P>(
1631    ring: &R,
1632    distance: f64,
1633    join: BufferJoinStrategy,
1634    pieces: &mut Vec<Polygon<P>>,
1635) where
1636    R: RingTrait<Point = P>,
1637    P: PointMut + Default + Copy,
1638    P::Scalar: Into<f64> + FromF64,
1639{
1640    let mut vertices = distinct_vertices(ring);
1641    vertices.dedup();
1642    while vertices.len() > 1 && vertices.last() == vertices.first() {
1643        vertices.pop();
1644    }
1645    if vertices.len() < 3 || distance == 0.0 {
1646        return;
1647    }
1648    if signed_area_ccw_positive(&vertices) < 0.0 {
1649        vertices.reverse();
1650    }
1651
1652    let count = vertices.len();
1653    for index in 0..count {
1654        let previous = vertices[(index + count - 1) % count];
1655        let vertex = vertices[index];
1656        let next = vertices[(index + 1) % count];
1657        let incoming = (vertex.0 - previous.0, vertex.1 - previous.1);
1658        let outgoing = (next.0 - vertex.0, next.1 - vertex.1);
1659        let incoming_normal = outward_normal(incoming.0, incoming.1);
1660        let outgoing_normal = outward_normal(outgoing.0, outgoing.1);
1661        let before = (
1662            vertex.0 + incoming_normal.0 * distance,
1663            vertex.1 + incoming_normal.1 * distance,
1664        );
1665        let after = (
1666            vertex.0 + outgoing_normal.0 * distance,
1667            vertex.1 + outgoing_normal.1 * distance,
1668        );
1669        let far = (
1670            next.0 + outgoing_normal.0 * distance,
1671            next.1 + outgoing_normal.1 * distance,
1672        );
1673        push_piece(pieces, alloc::vec![vertex, next, far, after]);
1674
1675        let cross = incoming.0 * outgoing.1 - incoming.1 * outgoing.0;
1676        if cross * distance > 0.0 {
1677            let intersection = line_intersection(before, incoming, after, outgoing);
1678            let mut wedge = alloc::vec![vertex, before];
1679            push_join_points(
1680                join,
1681                vertex,
1682                before,
1683                after,
1684                intersection,
1685                distance,
1686                &mut wedge,
1687            );
1688            wedge.push(after);
1689            push_piece(pieces, wedge);
1690        }
1691    }
1692}
1693
1694/// One piece as a closed ring, dropped when it encloses nothing.
1695fn push_piece<P>(pieces: &mut Vec<Polygon<P>>, mut boundary: Vec<(f64, f64)>)
1696where
1697    P: PointMut + Default + Copy,
1698    P::Scalar: FromF64,
1699{
1700    boundary.dedup();
1701    if boundary.len() < 3 || signed_area_ccw_positive(&boundary).abs() <= f64::EPSILON {
1702        return;
1703    }
1704    boundary.push(boundary[0]);
1705    pieces.push(Polygon::new(Ring::from_vec(
1706        boundary
1707            .into_iter()
1708            .map(|(x, y)| make_point(x, y))
1709            .collect(),
1710    )));
1711}
1712
1713/// The union of the pieces, merged neighbour with neighbour and then pair
1714/// with pair, so each union is between parts of like size and the work grows
1715/// with the result rather than with the number of pieces.
1716fn merge_pieces<P>(pieces: Vec<Polygon<P>>) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
1717where
1718    P: PointMut + Default + Copy,
1719    P::Scalar: CoordinateScalar + Into<f64>,
1720    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
1721{
1722    let mut merged: Vec<MultiPolygon<Polygon<P>>> = pieces
1723        .into_iter()
1724        .map(|piece| MultiPolygon(alloc::vec![piece]))
1725        .collect();
1726    while merged.len() > 1 {
1727        let mut next = Vec::with_capacity(merged.len().div_ceil(2));
1728        let mut pairs = merged.into_iter();
1729        while let Some(left) = pairs.next() {
1730            match pairs.next() {
1731                Some(right) => next.push(union_multi(&left, &right)?),
1732                None => next.push(left),
1733            }
1734        }
1735        merged = next;
1736    }
1737    Ok(merged.pop().unwrap_or_default())
1738}
1739
1740fn buffer_linestring<L, P>(
1741    line: &L,
1742    left: f64,
1743    right: f64,
1744    join: BufferJoinStrategy,
1745    end: BufferEndStrategy,
1746) -> Result<Polygon<P>, OverlayError>
1747where
1748    L: LinestringTrait<Point = P>,
1749    P: PointMut + Default + Copy,
1750    P::Scalar: Into<f64> + FromF64,
1751{
1752    let mut vertices: Vec<(f64, f64)> = Vec::new();
1753    for point in line.points() {
1754        let value = (point.get::<0>().into(), point.get::<1>().into());
1755        if vertices.last().copied() != Some(value) {
1756            vertices.push(value);
1757        }
1758    }
1759    if vertices.len() < 2 {
1760        return Err(OverlayError::Unsupported);
1761    }
1762
1763    let left_path = offset_path(&vertices, left, true, join);
1764    let right_path = offset_path(&vertices, right, false, join);
1765    debug_assert!(!left_path.is_empty() && !right_path.is_empty());
1766    let mut boundary = left_path;
1767    match end {
1768        BufferEndStrategy::Flat => {}
1769        BufferEndStrategy::Round { points_per_circle } => {
1770            let center = *vertices.last().expect("linestring has an endpoint");
1771            let from = *boundary.last().expect("left path has an endpoint");
1772            let to = *right_path.last().expect("right path has an endpoint");
1773            push_end_arc(
1774                &mut boundary,
1775                center,
1776                from,
1777                to,
1778                points_per_circle.max(4),
1779                true,
1780            );
1781        }
1782    }
1783    boundary.extend(right_path.iter().rev().copied());
1784    if let BufferEndStrategy::Round { points_per_circle } = end {
1785        let to = boundary[0];
1786        push_end_arc(
1787            &mut boundary,
1788            vertices[0],
1789            right_path[0],
1790            to,
1791            points_per_circle.max(4),
1792            true,
1793        );
1794    }
1795    let first = boundary[0];
1796    boundary.push(first);
1797    Ok(Polygon::new(Ring::from_vec(
1798        boundary
1799            .into_iter()
1800            .map(|(x, y)| make_point(x, y))
1801            .collect(),
1802    )))
1803}
1804
1805fn offset_path(
1806    vertices: &[(f64, f64)],
1807    distance: f64,
1808    left: bool,
1809    join: BufferJoinStrategy,
1810) -> Vec<(f64, f64)> {
1811    let side = if left { 1.0 } else { -1.0 };
1812    let normals: Vec<(f64, f64)> = vertices
1813        .windows(2)
1814        .map(|edge| {
1815            let dx = edge[1].0 - edge[0].0;
1816            let dy = edge[1].1 - edge[0].1;
1817            let length = hypot(dx, dy);
1818            (-dy / length * side, dx / length * side)
1819        })
1820        .collect();
1821    let mut path = Vec::with_capacity(vertices.len());
1822    path.push((
1823        vertices[0].0 + normals[0].0 * distance,
1824        vertices[0].1 + normals[0].1 * distance,
1825    ));
1826    for index in 1..vertices.len() - 1 {
1827        let vertex = vertices[index];
1828        let previous = vertices[index - 1];
1829        let next = vertices[index + 1];
1830        let before = (
1831            vertex.0 + normals[index - 1].0 * distance,
1832            vertex.1 + normals[index - 1].1 * distance,
1833        );
1834        let after = (
1835            vertex.0 + normals[index].0 * distance,
1836            vertex.1 + normals[index].1 * distance,
1837        );
1838        let intersection = line_intersection(
1839            before,
1840            (vertex.0 - previous.0, vertex.1 - previous.1),
1841            after,
1842            (next.0 - vertex.0, next.1 - vertex.1),
1843        );
1844        match (join, intersection) {
1845            (BufferJoinStrategy::Miter { limit }, Some(point))
1846                if point.0.is_finite() && point.1.is_finite() =>
1847            {
1848                let miter_length = hypot(point.0 - vertex.0, point.1 - vertex.1);
1849                if distance == 0.0 || miter_length <= limit.max(1.0) * distance.abs() {
1850                    path.push(point);
1851                } else {
1852                    path.push(before);
1853                    path.push(after);
1854                }
1855            }
1856            (BufferJoinStrategy::Round { points_per_circle }, _) => {
1857                path.push(before);
1858                push_arc_between(
1859                    &mut path,
1860                    vertex,
1861                    before,
1862                    after,
1863                    distance.abs(),
1864                    points_per_circle.max(4),
1865                    left,
1866                );
1867                path.push(after);
1868            }
1869            _ => {
1870                path.push(before);
1871                path.push(after);
1872            }
1873        }
1874    }
1875    let last = vertices.len() - 1;
1876    path.push((
1877        vertices[last].0 + normals[last - 1].0 * distance,
1878        vertices[last].1 + normals[last - 1].1 * distance,
1879    ));
1880    path
1881}
1882
1883fn line_intersection(
1884    first_origin: (f64, f64),
1885    first_direction: (f64, f64),
1886    second_origin: (f64, f64),
1887    second_direction: (f64, f64),
1888) -> Option<(f64, f64)> {
1889    let denominator =
1890        first_direction.0 * second_direction.1 - first_direction.1 * second_direction.0;
1891    if denominator.abs() <= f64::EPSILON {
1892        return None;
1893    }
1894    let delta = (
1895        second_origin.0 - first_origin.0,
1896        second_origin.1 - first_origin.1,
1897    );
1898    let factor = (delta.0 * second_direction.1 - delta.1 * second_direction.0) / denominator;
1899    Some((
1900        first_origin.0 + factor * first_direction.0,
1901        first_origin.1 + factor * first_direction.1,
1902    ))
1903}
1904
1905fn push_arc_between(
1906    output: &mut Vec<(f64, f64)>,
1907    center: (f64, f64),
1908    from: (f64, f64),
1909    to: (f64, f64),
1910    radius: f64,
1911    points_per_circle: usize,
1912    counterclockwise: bool,
1913) {
1914    if radius == 0.0 {
1915        return;
1916    }
1917    let start = atan2(from.1 - center.1, from.0 - center.0);
1918    let mut end = atan2(to.1 - center.1, to.0 - center.0);
1919    if counterclockwise {
1920        while end < start {
1921            end += core::f64::consts::TAU;
1922        }
1923    } else {
1924        while end > start {
1925            end -= core::f64::consts::TAU;
1926        }
1927    }
1928    let sweep = end - start;
1929    let steps =
1930        ceil((sweep.abs() / core::f64::consts::TAU) * points_per_circle as f64).max(1.0) as usize;
1931    for step in 1..steps {
1932        let angle = start + sweep * step as f64 / steps as f64;
1933        output.push((
1934            center.0 + radius * cos(angle),
1935            center.1 + radius * sin(angle),
1936        ));
1937    }
1938}
1939
1940fn push_end_arc(
1941    output: &mut Vec<(f64, f64)>,
1942    center: (f64, f64),
1943    from: (f64, f64),
1944    to: (f64, f64),
1945    points_per_circle: usize,
1946    clockwise: bool,
1947) {
1948    let radius =
1949        hypot(from.0 - center.0, from.1 - center.1).max(hypot(to.0 - center.0, to.1 - center.1));
1950    push_arc_between(
1951        output,
1952        center,
1953        from,
1954        to,
1955        radius,
1956        points_per_circle,
1957        !clockwise,
1958    );
1959}
1960
1961/// Materialise an output point from the `f64` kernel coordinates.
1962fn make_point<P>(x: f64, y: f64) -> P
1963where
1964    P: PointMut + Default,
1965    P::Scalar: FromF64,
1966{
1967    let mut p = P::default();
1968    p.set::<0>(P::Scalar::from_f64(x));
1969    p.set::<1>(P::Scalar::from_f64(y));
1970    p
1971}
1972
1973/// A regular-polygon approximation of a circle, clockwise and closed.
1974fn circle_ring<P>(cx: f64, cy: f64, r: f64, segments: usize) -> Ring<P>
1975where
1976    P: PointMut + Default + Copy,
1977    P::Scalar: FromF64,
1978{
1979    let mut pts = Vec::with_capacity(segments + 1);
1980    let step = core::f64::consts::TAU / segments as f64;
1981    for k in 0..segments {
1982        let a = -step * k as f64;
1983        pts.push(make_point(cx + r * cos(a), cy + r * sin(a)));
1984    }
1985    pts.push(pts[0]);
1986    Ring::from_vec(pts)
1987}
1988
1989/// Distinct consecutive vertices of a ring as `f64` pairs (drops the
1990/// closing repeat).
1991fn distinct_vertices<R>(ring: &R) -> Vec<(f64, f64)>
1992where
1993    R: RingTrait,
1994    <R::Point as Point>::Scalar: Into<f64>,
1995{
1996    let mut pts: Vec<(f64, f64)> = ring
1997        .points()
1998        .map(|p| (p.get::<0>().into(), p.get::<1>().into()))
1999        .collect();
2000    if pts.len() >= 2 {
2001        let first = pts[0];
2002        let last = pts[pts.len() - 1];
2003        if first == last {
2004            pts.pop();
2005        }
2006    }
2007    pts
2008}
2009
2010/// The standard math signed area of the vertex ring (counter-clockwise
2011/// positive), via the shoelace sum over the closed loop. Used only to
2012/// detect winding for normalisation.
2013fn signed_area_ccw_positive(verts: &[(f64, f64)]) -> f64 {
2014    let n = verts.len();
2015    let mut acc = 0.0;
2016    for i in 0..n {
2017        let a = verts[i];
2018        let b = verts[(i + 1) % n];
2019        acc += a.0 * b.1 - b.0 * a.1;
2020    }
2021    acc * 0.5
2022}
2023
2024fn minimum_boundary_distance(point: (f64, f64), vertices: &[(f64, f64)]) -> f64 {
2025    let mut minimum = f64::INFINITY;
2026    for index in 0..vertices.len() {
2027        let start = vertices[index];
2028        let end = vertices[(index + 1) % vertices.len()];
2029        let delta = (end.0 - start.0, end.1 - start.1);
2030        let length_squared = delta.0 * delta.0 + delta.1 * delta.1;
2031        let fraction = if length_squared == 0.0 {
2032            0.0
2033        } else {
2034            (((point.0 - start.0) * delta.0 + (point.1 - start.1) * delta.1) / length_squared)
2035                .clamp(0.0, 1.0)
2036        };
2037        let nearest = (start.0 + fraction * delta.0, start.1 + fraction * delta.1);
2038        minimum = minimum.min(hypot(point.0 - nearest.0, point.1 - nearest.1));
2039    }
2040    minimum
2041}
2042
2043fn point_in_or_on_ring(point: (f64, f64), vertices: &[(f64, f64)]) -> bool {
2044    let scale = vertices.iter().fold(1.0_f64, |acc, vertex| {
2045        acc.max(vertex.0.abs()).max(vertex.1.abs())
2046    });
2047    if minimum_boundary_distance(point, vertices) <= scale * 1e-12 {
2048        return true;
2049    }
2050
2051    let mut inside = false;
2052    for index in 0..vertices.len() {
2053        let start = vertices[index];
2054        let end = vertices[(index + 1) % vertices.len()];
2055        if (start.1 > point.1) != (end.1 > point.1)
2056            && point.0 < (end.0 - start.0) * (point.1 - start.1) / (end.1 - start.1) + start.0
2057        {
2058            inside = !inside;
2059        }
2060    }
2061    inside
2062}
2063
2064/// The outward unit normal of a directed CCW edge with delta
2065/// `(dx, dy)` (pointing to the edge's right).
2066fn outward_normal(dx: f64, dy: f64) -> (f64, f64) {
2067    let len = (dx * dx + dy * dy).sqrt();
2068    if len == 0.0 {
2069        return (0.0, 0.0);
2070    }
2071    // Right-hand normal of (dx, dy) is (dy, -dx).
2072    (dy / len, -dx / len)
2073}
2074
2075#[cfg(test)]
2076mod tests {
2077    //! OVL7 done-when: buffered areas match the closed-form values.
2078    //! Mirrors `test/algorithms/buffer/`.
2079
2080    use super::{
2081        BufferJoinStrategy, dissolve_offset, offset_rings_need_dissolving, push_piece,
2082        push_ring_pieces,
2083    };
2084    use super::{JoinStrategy, PointStrategy, buffer, buffer_convex_polygon, buffer_point};
2085    use alloc::vec::Vec;
2086    use geometry_algorithm::ring_area;
2087    use geometry_cs::Cartesian;
2088    use geometry_model::{Point2D, Polygon, Ring, polygon};
2089    use geometry_trait::{MultiPolygon as _, Polygon as _};
2090
2091    type P = Point2D<f64, Cartesian>;
2092
2093    fn close(a: f64, b: f64, tol: f64) {
2094        assert!((a - b).abs() < tol, "expected {b}, got {a}");
2095    }
2096
2097    #[test]
2098    fn point_circle_area_approximates_pi_r_squared() {
2099        let disc = buffer_point(
2100            &P::new(0.0, 0.0),
2101            2.0,
2102            PointStrategy::Circle {
2103                points_per_circle: 720,
2104            },
2105        );
2106        // π·r² = π·4.
2107        close(ring_area(&disc).abs(), core::f64::consts::PI * 4.0, 1e-2);
2108    }
2109
2110    #[test]
2111    fn point_square_area() {
2112        let sq = buffer_point(&P::new(0.0, 0.0), 3.0, PointStrategy::Square);
2113        // A square of half-side 3 → side 6 → area 36.
2114        close(ring_area(&sq).abs(), 36.0, 1e-9);
2115    }
2116
2117    #[test]
2118    fn convex_square_round_buffer_area() {
2119        let sq: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
2120        let grown = buffer_convex_polygon(
2121            &sq,
2122            1.0,
2123            JoinStrategy::Round {
2124                points_per_circle: 720,
2125            },
2126        );
2127        // s² + 4·s·d + π·d² = 4 + 8 + π.
2128        let expected = 4.0 + 8.0 + core::f64::consts::PI;
2129        close(ring_area(grown.exterior()).abs(), expected, 1e-2);
2130    }
2131
2132    #[test]
2133    fn convex_triangle_round_buffer_grows() {
2134        let tri: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (0.0, 3.0), (0.0, 0.0)]];
2135        let base = ring_area(tri.exterior()).abs(); // 6
2136        let grown = buffer_convex_polygon(
2137            &tri,
2138            0.5,
2139            JoinStrategy::Round {
2140                points_per_circle: 360,
2141            },
2142        );
2143        // The buffered area must exceed the original.
2144        assert!(ring_area(grown.exterior()).abs() > base);
2145    }
2146
2147    #[test]
2148    fn buffer_is_winding_independent() {
2149        // Regression: the same square listed clockwise and counter-
2150        // clockwise must buffer to the same grown area. The winding
2151        // normalisation makes the outward offset direction correct for
2152        // both.
2153        let ccw: Polygon<P> =
2154            polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
2155        let cw: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)]];
2156        let j = JoinStrategy::Round {
2157            points_per_circle: 720,
2158        };
2159        let expected = 4.0 + 8.0 + core::f64::consts::PI;
2160        let grown_from_counterclockwise =
2161            ring_area(buffer_convex_polygon(&ccw, 1.0, j).exterior()).abs();
2162        let grown_from_clockwise = ring_area(buffer_convex_polygon(&cw, 1.0, j).exterior()).abs();
2163        close(grown_from_counterclockwise, expected, 5e-2);
2164        close(grown_from_clockwise, expected, 5e-2);
2165    }
2166
2167    #[test]
2168    fn miter_square_area_is_16() {
2169        // Regression: the old Miter arm placed the corner point at
2170        // distance d along the bisector (ON the round arc), yielding
2171        // 14.83 — smaller than even the round buffer. A true miter
2172        // corner is the offset-edge intersection at √2·d, so the
2173        // buffered 2×2 square is s² + 4·s·d + 4·d² = 16 exactly.
2174        let sq: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
2175        let grown = buffer_convex_polygon(&sq, 1.0, JoinStrategy::Miter);
2176        close(ring_area(grown.exterior()).abs(), 16.0, 1e-9);
2177    }
2178
2179    #[test]
2180    fn miter_contains_near_corner_probe() {
2181        // A point at distance 0.99 < d from the input corner, in the
2182        // 22.5° direction, was EXCLUDED by the old chord-cut corner.
2183        use geometry_algorithm::within;
2184        let sq: Polygon<P> = polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
2185        let grown = buffer_convex_polygon(&sq, 1.0, JoinStrategy::Miter);
2186        let ang = 22.5_f64.to_radians();
2187        let probe = P::new(2.0 + 0.99 * ang.cos(), 2.0 + 0.99 * ang.sin());
2188        assert!(
2189            within(&probe, &grown),
2190            "buffer must contain points within d"
2191        );
2192    }
2193
2194    #[test]
2195    fn miter_is_superset_of_round_by_area() {
2196        // A miter fills the wedge beyond the round arc, so its area
2197        // can never be below the round join's.
2198        let j_round = JoinStrategy::Round {
2199            points_per_circle: 720,
2200        };
2201        let square: Polygon<P> =
2202            polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
2203        let triangle: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (0.0, 3.0), (0.0, 0.0)]];
2204        for pg in [square, triangle] {
2205            let m =
2206                ring_area(buffer_convex_polygon(&pg, 1.0, JoinStrategy::Miter).exterior()).abs();
2207            let r = ring_area(buffer_convex_polygon(&pg, 1.0, j_round).exterior()).abs();
2208            assert!(m >= r - 1e-9, "miter {m} must not be below round {r}");
2209        }
2210    }
2211
2212    #[test]
2213    fn non_model_polygon_buffers_like_the_model_polygon() {
2214        // The generic signature accepts any `Polygon` trait impl — a
2215        // hand-rolled type must buffer to the same area as the same
2216        // shape held in a model polygon.
2217        use geometry_model::Ring;
2218        use geometry_tag::PolygonTag;
2219        use geometry_trait::{Geometry, Polygon as PolygonTrait};
2220
2221        struct Parcel {
2222            outer: Ring<P>,
2223        }
2224        impl Geometry for Parcel {
2225            type Kind = PolygonTag;
2226            type Point = P;
2227        }
2228        impl PolygonTrait for Parcel {
2229            type Ring = Ring<P>;
2230            fn exterior(&self) -> &Ring<P> {
2231                &self.outer
2232            }
2233            fn interiors(&self) -> impl ExactSizeIterator<Item = &Ring<P>> {
2234                core::iter::empty()
2235            }
2236        }
2237
2238        let pts = vec![
2239            P::new(0.0, 0.0),
2240            P::new(2.0, 0.0),
2241            P::new(2.0, 2.0),
2242            P::new(0.0, 2.0),
2243            P::new(0.0, 0.0),
2244        ];
2245        let parcel = Parcel {
2246            outer: Ring::from_vec(pts.clone()),
2247        };
2248        let model: Polygon<P> = Polygon::new(Ring::from_vec(pts));
2249        let j = JoinStrategy::Round {
2250            points_per_circle: 360,
2251        };
2252        let parcel_buffer = buffer(&parcel, 1.0, j, PointStrategy::Square).unwrap();
2253        let model_buffer = buffer(&model, 1.0, j, PointStrategy::Square).unwrap();
2254        let a = ring_area(parcel_buffer.polygons().next().unwrap().exterior()).abs();
2255        let b = ring_area(model_buffer.polygons().next().unwrap().exterior()).abs();
2256        close(a, b, 1e-12);
2257    }
2258
2259    #[test]
2260    fn miter_is_winding_independent() {
2261        // Same square listed CW and CCW buffers to the same miter area.
2262        let ccw: Polygon<P> =
2263            polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
2264        let cw: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)]];
2265        close(
2266            ring_area(buffer_convex_polygon(&ccw, 1.0, JoinStrategy::Miter).exterior()).abs(),
2267            16.0,
2268            1e-9,
2269        );
2270        close(
2271            ring_area(buffer_convex_polygon(&cw, 1.0, JoinStrategy::Miter).exterior()).abs(),
2272            16.0,
2273            1e-9,
2274        );
2275    }
2276
2277    // ---- The pieces path, at the edges the shaped inputs never reach ----
2278    //
2279    // `dissolve_offset` is only entered for a ring the offsetted ring gets
2280    // wrong, and the area assertions above all go through the simple path.
2281    // These drive the pieces builder and its gatekeeper directly, because
2282    // the degenerate inputs they guard against cannot be produced by a
2283    // well-shaped polygon — which is exactly why a missing guard here would
2284    // surface as a malformed ring far downstream in the overlay engine.
2285
2286    const JOIN: BufferJoinStrategy = BufferJoinStrategy::Miter { limit: 5.0 };
2287
2288    fn ring(points: &[(f64, f64)]) -> Ring<P> {
2289        Ring::from_vec(points.iter().map(|&(x, y)| P::new(x, y)).collect())
2290    }
2291
2292    fn unit_square() -> Ring<P> {
2293        ring(&[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)])
2294    }
2295
2296    /// A ring below three distinct vertices encloses nothing, and a zero
2297    /// distance moves nothing. Either way the ring must contribute no
2298    /// piece rather than a zero-area sliver for the engine to dissolve.
2299    #[test]
2300    fn a_ring_that_encloses_nothing_contributes_no_piece() {
2301        let mut pieces: Vec<Polygon<P>> = Vec::new();
2302
2303        push_ring_pieces(
2304            &ring(&[(0.0, 0.0), (1.0, 0.0), (0.0, 0.0)]),
2305            1.0,
2306            JOIN,
2307            &mut pieces,
2308        );
2309        assert!(pieces.is_empty(), "a two-vertex ring produced a piece");
2310
2311        push_ring_pieces(&ring(&[(0.0, 0.0)]), 1.0, JOIN, &mut pieces);
2312        assert!(pieces.is_empty(), "a single vertex produced a piece");
2313
2314        push_ring_pieces(&unit_square(), 0.0, JOIN, &mut pieces);
2315        assert!(pieces.is_empty(), "a zero distance produced a piece");
2316    }
2317
2318    /// A ring may close on a repeated vertex more than once. Only one
2319    /// repeat is dropped as the closure, so the rest have to be trimmed —
2320    /// otherwise the last side is zero-length and its piece degenerate.
2321    /// The trimmed ring must give exactly the pieces the clean one does.
2322    #[test]
2323    fn repeated_closing_vertices_are_trimmed_before_the_sides_are_cut() {
2324        let mut clean: Vec<Polygon<P>> = Vec::new();
2325        push_ring_pieces(&unit_square(), 1.0, JOIN, &mut clean);
2326
2327        let mut repeated: Vec<Polygon<P>> = Vec::new();
2328        push_ring_pieces(
2329            &ring(&[
2330                (0.0, 0.0),
2331                (1.0, 0.0),
2332                (1.0, 1.0),
2333                (0.0, 1.0),
2334                (0.0, 0.0),
2335                (0.0, 0.0),
2336                (0.0, 0.0),
2337            ]),
2338            1.0,
2339            JOIN,
2340            &mut repeated,
2341        );
2342
2343        assert!(!clean.is_empty(), "the square should cut into pieces");
2344        assert_eq!(clean, repeated);
2345    }
2346
2347    /// A piece is kept only if it encloses area. A boundary of fewer than
2348    /// three points, or one whose points are collinear, bounds nothing and
2349    /// must be dropped at the source.
2350    #[test]
2351    fn a_piece_bounding_no_area_is_dropped() {
2352        let mut pieces: Vec<Polygon<P>> = Vec::new();
2353
2354        push_piece::<P>(&mut pieces, alloc::vec![(0.0, 0.0), (1.0, 0.0)]);
2355        assert!(pieces.is_empty(), "a two-point boundary was kept");
2356
2357        push_piece::<P>(&mut pieces, alloc::vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)]);
2358        assert!(pieces.is_empty(), "a collinear boundary was kept");
2359
2360        // A boundary that does enclose area is kept and closed.
2361        push_piece::<P>(&mut pieces, alloc::vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]);
2362        assert_eq!(pieces.len(), 1);
2363        assert_eq!(pieces[0].exterior().0.len(), 4);
2364    }
2365
2366    /// Whether the offsetted rings can stand, at the two hole outcomes
2367    /// that separate erosion from growth. A hole that `offset_ring`
2368    /// declined has collapsed; eroding, the hole is being filled in and a
2369    /// collapsed one encloses nothing, so it is no reason to rebuild.
2370    /// Growing, the same hole may have collapsed only in part, so it is.
2371    #[test]
2372    fn a_collapsed_hole_forces_the_pieces_path_only_when_growing() {
2373        let outer = unit_square();
2374        assert!(!offset_rings_need_dissolving(Some(&outer), &[None], -0.1));
2375        assert!(offset_rings_need_dissolving(Some(&outer), &[None], 0.1));
2376    }
2377
2378    /// A hole whose own offsetted ring crosses itself is not a usable
2379    /// answer whichever way the buffer runs, so it goes to the pieces —
2380    /// the hole-side counterpart of the exterior's self-crossing check.
2381    #[test]
2382    fn a_self_crossing_hole_forces_the_pieces_path() {
2383        let outer = unit_square();
2384        // A bow tie: the two diagonals cross.
2385        let bow_tie = ring(&[(0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0)]);
2386        assert!(offset_rings_need_dissolving(
2387            Some(&outer),
2388            &[Some(bow_tie.clone())],
2389            -0.1
2390        ));
2391        assert!(offset_rings_need_dissolving(
2392            Some(&outer),
2393            &[Some(bow_tie)],
2394            0.1
2395        ));
2396    }
2397
2398    /// When every ring of the polygon is degenerate there are no pieces to
2399    /// merge, and the dissolve must answer with an empty multi-polygon
2400    /// rather than unioning the original back in — a zero-distance buffer
2401    /// of a square is the square, but its *pieces* are nothing.
2402    #[test]
2403    fn a_dissolve_with_no_pieces_is_empty() {
2404        let square: Polygon<P> =
2405            polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
2406        let dissolved = dissolve_offset(&square, 0.0, JOIN).expect("no pieces is not an error");
2407        assert!(dissolved.0.is_empty());
2408    }
2409}