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};
50use geometry_strategy::buffer::{
51    BufferDistanceStrategy, BufferEndStrategy, BufferJoinStrategy, BufferPointStrategy,
52    BufferSettings, CartesianBuffer, DefaultBuffer, DefaultBufferStrategy, GeographicBuffer,
53    SphericalBuffer,
54};
55use geometry_tag::{
56    BoxTag, LinestringTag, MultiLinestringTag, MultiPointTag, MultiPolygonTag, PointTag,
57    PolygonTag, RingTag, SameAs, SegmentTag,
58};
59use geometry_trait::{
60    Box as BoxTrait, Geometry, Linestring as LinestringTrait,
61    MultiLinestring as MultiLinestringTrait, MultiPoint as MultiPointTrait,
62    MultiPolygon as MultiPolygonTrait, Point, PointMut, Polygon as PolygonTrait, Ring as RingTrait,
63    Segment as SegmentTrait, box_max, box_min, segment_end, segment_start,
64};
65
66use crate::operation::OverlayError;
67
68/// How to fill the wedge at a convex corner of the offset boundary.
69///
70/// Mirrors `strategy::buffer::join_round` / `join_miter`
71/// (`strategies/buffer/buffer_join_round.hpp` and friends).
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum JoinStrategy {
74    /// Fill the corner with a circular arc of `points_per_circle`
75    /// segments. Boost's `join_round`.
76    Round {
77        /// Segment count of a full circle; the arc uses a proportional
78        /// share.
79        points_per_circle: usize,
80    },
81    /// Extend the two offset edges until they meet at a sharp point.
82    /// Boost's `join_miter`.
83    ///
84    /// This compatibility spelling uses Boost's default miter limit of
85    /// five times the buffer distance
86    /// (`strategies/cartesian/buffer_join_miter.hpp:52-60`). Use
87    /// [`BufferSettings`] with [`BufferJoinStrategy::Miter`] to select a
88    /// different limit.
89    Miter,
90}
91
92/// How to approximate a buffered point.
93///
94/// Mirrors `strategy::buffer::point_circle` / `point_square`
95/// (`strategies/buffer/buffer_point_circle.hpp`).
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum PointStrategy {
98    /// Approximate the buffer disc with a regular polygon of
99    /// `points_per_circle` vertices. Boost's `point_circle`.
100    Circle {
101        /// Vertex count of the approximating polygon.
102        points_per_circle: usize,
103    },
104    /// Approximate the buffer with an axis-aligned square. Boost's
105    /// `point_square`.
106    Square,
107}
108
109/// Per-geometry implementation selected by [`buffer`].
110///
111/// Rust tag-dispatch adapter for the geometry-specialized call behind
112/// `boost::geometry::buffer` in
113/// `algorithms/detail/buffer/interface.hpp:246-273`.
114#[doc(hidden)]
115pub trait BufferStrategy<G: Geometry, CoordinateStrategy> {
116    fn apply(
117        &self,
118        geometry: &G,
119        settings: BufferSettings,
120        coordinate_strategy: &CoordinateStrategy,
121    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>;
122}
123
124/// Tag-to-buffer implementation picker.
125///
126/// Rust counterpart to the geometry dispatch performed by
127/// `boost::geometry::buffer` in
128/// `algorithms/detail/buffer/interface.hpp:246-273`.
129#[doc(hidden)]
130pub trait BufferStrategyForKind {
131    type S: Default;
132}
133
134/// Point buffer implementation selected for [`PointTag`].
135///
136/// Implements the point arm of the public buffer dispatch from
137/// `algorithms/detail/buffer/interface.hpp:246-273`.
138#[doc(hidden)]
139#[derive(Debug, Default, Clone, Copy)]
140pub struct PointBuffer;
141
142/// Polygon buffer implementation selected for [`PolygonTag`].
143///
144/// Implements the polygon arm of the public buffer dispatch from
145/// `algorithms/detail/buffer/interface.hpp:246-273`.
146#[doc(hidden)]
147#[derive(Debug, Default, Clone, Copy)]
148pub struct PolygonBuffer;
149
150/// Linestring buffer implementation selected for [`LinestringTag`].
151#[doc(hidden)]
152#[derive(Debug, Default, Clone, Copy)]
153pub struct LinestringBuffer;
154
155/// Segment buffer implementation selected for [`SegmentTag`].
156#[doc(hidden)]
157#[derive(Debug, Default, Clone, Copy)]
158pub struct SegmentBuffer;
159
160/// Ring buffer implementation selected for [`RingTag`].
161#[doc(hidden)]
162#[derive(Debug, Default, Clone, Copy)]
163pub struct RingBuffer;
164
165/// Box buffer implementation selected for [`BoxTag`].
166#[doc(hidden)]
167#[derive(Debug, Default, Clone, Copy)]
168pub struct BoxBuffer;
169
170/// Multi-point buffer implementation selected for [`MultiPointTag`].
171#[doc(hidden)]
172#[derive(Debug, Default, Clone, Copy)]
173pub struct MultiPointBuffer;
174
175/// Multi-linestring buffer implementation selected for [`MultiLinestringTag`].
176#[doc(hidden)]
177#[derive(Debug, Default, Clone, Copy)]
178pub struct MultiLinestringBuffer;
179
180/// Multi-polygon buffer implementation selected for [`MultiPolygonTag`].
181#[doc(hidden)]
182#[derive(Debug, Default, Clone, Copy)]
183pub struct MultiPolygonBuffer;
184
185/// Selects the point arm of `buffer_all` from
186/// `algorithms/detail/buffer/interface.hpp:269-273`.
187impl BufferStrategyForKind for PointTag {
188    type S = PointBuffer;
189}
190
191/// Selects the polygon arm of `buffer_all` from
192/// `algorithms/detail/buffer/interface.hpp:269-273`.
193impl BufferStrategyForKind for PolygonTag {
194    type S = PolygonBuffer;
195}
196
197impl BufferStrategyForKind for LinestringTag {
198    type S = LinestringBuffer;
199}
200
201impl BufferStrategyForKind for SegmentTag {
202    type S = SegmentBuffer;
203}
204
205impl BufferStrategyForKind for RingTag {
206    type S = RingBuffer;
207}
208
209impl BufferStrategyForKind for BoxTag {
210    type S = BoxBuffer;
211}
212
213impl BufferStrategyForKind for MultiPointTag {
214    type S = MultiPointBuffer;
215}
216
217impl BufferStrategyForKind for MultiLinestringTag {
218    type S = MultiLinestringBuffer;
219}
220
221impl BufferStrategyForKind for MultiPolygonTag {
222    type S = MultiPolygonBuffer;
223}
224
225/// Buffer a geometry using the public point and join strategies.
226///
227/// Mirrors `boost::geometry::buffer` from
228/// `boost/geometry/algorithms/detail/buffer/interface.hpp:246-273`. Cartesian,
229/// spherical, and geographic dispatch supports point, segment, linestring,
230/// ring, polygon, box, and all three homogeneous multi-geometry kinds. Point
231/// inputs use `point`, linear inputs use all five strategy roles, and areal
232/// inputs use signed distance and join policies.
233///
234/// # Errors
235///
236/// Returns [`OverlayError::Unsupported`] for non-finite distances, asymmetric
237/// areal distances, or degenerate linear inputs.
238#[inline]
239#[must_use = "buffering can fail and the generated geometry should be used"]
240pub fn buffer<G>(
241    geometry: &G,
242    distance: f64,
243    join: JoinStrategy,
244    point: PointStrategy,
245) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
246where
247    G: Geometry,
248    G::Kind: BufferStrategyForKind,
249    <<G::Point as Point>::Cs as CoordinateSystem>::Family:
250        DefaultBuffer<<<G::Point as Point>::Cs as CoordinateSystem>::Family>,
251    <G::Kind as BufferStrategyForKind>::S: BufferStrategy<G, DefaultBufferStrategy<G>>,
252{
253    let settings = BufferSettings {
254        distance: BufferDistanceStrategy::Symmetric(distance),
255        side: geometry_strategy::buffer::BufferSideStrategy::Straight,
256        join: match join {
257            JoinStrategy::Round { points_per_circle } => {
258                BufferJoinStrategy::Round { points_per_circle }
259            }
260            JoinStrategy::Miter => BufferJoinStrategy::Miter { limit: 5.0 },
261        },
262        end: BufferEndStrategy::Round {
263            points_per_circle: 36,
264        },
265        point: match point {
266            PointStrategy::Circle { points_per_circle } => {
267                BufferPointStrategy::Circle { points_per_circle }
268            }
269            PointStrategy::Square => BufferPointStrategy::Square,
270        },
271    };
272    buffer_with(geometry, settings)
273}
274
275/// Buffer a geometry with Boost's complete distance/side/join/end/point
276/// strategy bundle.
277///
278/// Mirrors the five explicit strategy arguments to `boost::geometry::buffer`
279/// from `algorithms/detail/buffer/interface.hpp:246-273`.
280///
281/// # Errors
282///
283/// Returns [`OverlayError::Unsupported`] for non-finite/inapplicable distance
284/// policies or degenerate linear input.
285#[inline]
286#[must_use = "buffering can fail and the generated geometry should be used"]
287pub fn buffer_with<G>(
288    geometry: &G,
289    settings: BufferSettings,
290) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
291where
292    G: Geometry,
293    G::Kind: BufferStrategyForKind,
294    <<G::Point as Point>::Cs as CoordinateSystem>::Family:
295        DefaultBuffer<<<G::Point as Point>::Cs as CoordinateSystem>::Family>,
296    <G::Kind as BufferStrategyForKind>::S: BufferStrategy<G, DefaultBufferStrategy<G>>,
297{
298    buffer_with_strategy(geometry, settings, DefaultBufferStrategy::<G>::default())
299}
300
301/// Buffer a geometry with explicit coordinate-system and five-role strategy
302/// bundles.
303///
304/// Mirrors the explicit strategy overload of `boost::geometry::buffer` from
305/// `algorithms/detail/buffer/interface.hpp:246-273`, together with the
306/// Cartesian, spherical, and geographic umbrella strategies under
307/// `strategies/buffer/`.
308///
309/// [`SphericalBuffer`] and [`GeographicBuffer`] use a geometry-centered local
310/// tangent projection before invoking the Cartesian offset engine. This keeps
311/// distance units explicit and `no_std` compatible, but is a local-extent
312/// approximation rather than Boost's per-segment geodesic construction.
313///
314/// # Errors
315///
316/// Returns [`OverlayError::Unsupported`] for invalid strategy values,
317/// non-finite/inapplicable distances, or degenerate linear input.
318#[inline]
319#[must_use = "buffering can fail and the generated geometry should be used"]
320#[allow(
321    clippy::needless_pass_by_value,
322    reason = "Boost buffer coordinate strategies are small value objects passed explicitly"
323)]
324pub fn buffer_with_strategy<G, CoordinateStrategy>(
325    geometry: &G,
326    settings: BufferSettings,
327    coordinate_strategy: CoordinateStrategy,
328) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
329where
330    G: Geometry,
331    G::Kind: BufferStrategyForKind,
332    <G::Kind as BufferStrategyForKind>::S: BufferStrategy<G, CoordinateStrategy>,
333{
334    <<G::Kind as BufferStrategyForKind>::S as Default>::default().apply(
335        geometry,
336        settings,
337        &coordinate_strategy,
338    )
339}
340
341/// A polygon buffered at a distance of zero.
342///
343/// C++: `buffer_inserter` builds an offsetted ring per input ring and then
344/// finds the turns between them, discards those inside the original, and
345/// traverses what is left. Where the offsetted rings do not meet each other
346/// there are no turns, nothing is discarded and nothing is traversed, and the
347/// rings themselves are the answer — which is the case
348/// `repair_one_polygon` needs and the case this arm answers.
349///
350/// A ring that does meet itself needs `check_turn_in_original` and the buffer
351/// traversal, which are not ported; that asks for something this arm cannot
352/// answer, and it says so rather than guessing.
353fn zero_width_polygon_buffer<G>(
354    polygon: &G,
355) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
356where
357    G: PolygonTrait,
358    G::Point: PointMut + Default + Copy,
359    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
360{
361    use crate::piece_collection::{ZeroWidthOutcome, zero_width_outcome, zero_width_rings};
362
363    let rings = zero_width_rings(polygon);
364    match zero_width_outcome(&rings) {
365        ZeroWidthOutcome::RingsStand => Ok(MultiPolygon(
366            rings.into_iter().map(Polygon::new).collect::<Vec<_>>(),
367        )),
368        ZeroWidthOutcome::NeedsTraversal => Err(OverlayError::Unsupported),
369    }
370}
371
372/// Implements the point arm selected by `buffer_all` at
373/// `algorithms/detail/buffer/interface.hpp:269-273`.
374impl<G> BufferStrategy<G, CartesianBuffer> for PointBuffer
375where
376    G: Point + PointMut + Default + Copy,
377    G::Scalar: CoordinateScalar + Into<f64> + FromF64,
378    <G::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
379{
380    fn apply(
381        &self,
382        point_geometry: &G,
383        settings: BufferSettings,
384        _coordinate_strategy: &CartesianBuffer,
385    ) -> Result<MultiPolygon<Polygon<G>>, OverlayError> {
386        let BufferDistanceStrategy::Symmetric(distance) = settings.distance else {
387            return Err(OverlayError::Unsupported);
388        };
389        if !distance.is_finite() {
390            return Err(OverlayError::Unsupported);
391        }
392        if distance <= 0.0 {
393            return Ok(MultiPolygon(alloc::vec![]));
394        }
395        let point = match settings.point {
396            BufferPointStrategy::Circle { points_per_circle } => {
397                PointStrategy::Circle { points_per_circle }
398            }
399            BufferPointStrategy::Square => PointStrategy::Square,
400        };
401        let ring = buffer_point(point_geometry, distance, point);
402        Ok(MultiPolygon(alloc::vec![Polygon::new(ring)]))
403    }
404}
405
406/// Implements the polygon arm selected by `buffer_all` at
407/// `algorithms/detail/buffer/interface.hpp:269-273`.
408impl<G> BufferStrategy<G, CartesianBuffer> for PolygonBuffer
409where
410    G: PolygonTrait,
411    G::Point: PointMut + Default + Copy,
412    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
413    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
414{
415    fn apply(
416        &self,
417        polygon: &G,
418        settings: BufferSettings,
419        _coordinate_strategy: &CartesianBuffer,
420    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
421        let BufferDistanceStrategy::Symmetric(distance) = settings.distance else {
422            return Err(OverlayError::Unsupported);
423        };
424        if !distance.is_finite() {
425            return Err(OverlayError::Unsupported);
426        }
427        if distance == 0.0 {
428            // C++: a zero-width buffer is not a no-op and not a special case
429            // either — `buffer_inserter` runs its whole pipeline, and every
430            // side simply offsets onto itself. It is what `repair_one_polygon`
431            // falls back on, so it has to answer.
432            return zero_width_polygon_buffer(polygon);
433        }
434        let Some(outer) = offset_ring(polygon.exterior(), distance, settings.join, true) else {
435            return Ok(MultiPolygon(alloc::vec![]));
436        };
437        let inners = polygon
438            .interiors()
439            .filter_map(|ring| offset_ring(ring, -distance, settings.join, false))
440            .collect::<Vec<_>>();
441        let outer_vertices = distinct_vertices(&outer);
442        if inners.iter().any(|inner| {
443            let inner_vertices = distinct_vertices(inner);
444            outer_vertices
445                .iter()
446                .all(|point| point_in_or_on_ring(*point, &inner_vertices))
447        }) {
448            return Ok(MultiPolygon::new());
449        }
450
451        Ok(MultiPolygon(alloc::vec![Polygon::with_inners(
452            outer, inners,
453        )]))
454    }
455}
456
457impl<G> BufferStrategy<G, CartesianBuffer> for LinestringBuffer
458where
459    G: LinestringTrait,
460    G::Point: PointMut + Default + Copy,
461    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
462    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
463{
464    fn apply(
465        &self,
466        line: &G,
467        settings: BufferSettings,
468        _coordinate_strategy: &CartesianBuffer,
469    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
470        let (left, right) = match settings.distance {
471            BufferDistanceStrategy::Symmetric(distance) => (distance, distance),
472            BufferDistanceStrategy::Asymmetric { left, right } => (left, right),
473        };
474        if !left.is_finite() || !right.is_finite() || left < 0.0 || right < 0.0 {
475            return Err(OverlayError::Unsupported);
476        }
477        if left == 0.0 && right == 0.0 {
478            return Ok(MultiPolygon(alloc::vec![]));
479        }
480        let polygon = buffer_linestring(line, left, right, settings.join, settings.end)?;
481        Ok(MultiPolygon(alloc::vec![polygon]))
482    }
483}
484
485impl<G> BufferStrategy<G, CartesianBuffer> for SegmentBuffer
486where
487    G: SegmentTrait,
488    G::Point: PointMut + Default + Copy,
489    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
490    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
491{
492    fn apply(
493        &self,
494        segment: &G,
495        settings: BufferSettings,
496        coordinate_strategy: &CartesianBuffer,
497    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
498        let line: Linestring<G::Point> =
499            Linestring::from_vec(alloc::vec![segment_start(segment), segment_end(segment)]);
500        LinestringBuffer.apply(&line, settings, coordinate_strategy)
501    }
502}
503
504impl<G> BufferStrategy<G, CartesianBuffer> for RingBuffer
505where
506    G: RingTrait,
507    G::Point: PointMut + Default + Copy,
508    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
509    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
510{
511    fn apply(
512        &self,
513        ring: &G,
514        settings: BufferSettings,
515        _coordinate_strategy: &CartesianBuffer,
516    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
517        let BufferDistanceStrategy::Symmetric(distance) = settings.distance else {
518            return Err(OverlayError::Unsupported);
519        };
520        if !distance.is_finite() || distance == 0.0 {
521            return Err(OverlayError::Unsupported);
522        }
523        Ok(offset_ring(ring, distance, settings.join, true)
524            .map_or_else(MultiPolygon::new, |outer| {
525                MultiPolygon::from_vec(alloc::vec![Polygon::new(outer)])
526            }))
527    }
528}
529
530impl<G> BufferStrategy<G, CartesianBuffer> for BoxBuffer
531where
532    G: BoxTrait,
533    G::Point: PointMut + Default + Copy,
534    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
535    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
536{
537    fn apply(
538        &self,
539        bounds: &G,
540        settings: BufferSettings,
541        coordinate_strategy: &CartesianBuffer,
542    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
543        let minimum = box_min(bounds);
544        let maximum = box_max(bounds);
545        let min_x = minimum.get::<0>().into();
546        let min_y = minimum.get::<1>().into();
547        let max_x = maximum.get::<0>().into();
548        let max_y = maximum.get::<1>().into();
549        let ring: Ring<G::Point> = Ring::from_vec(alloc::vec![
550            make_point(min_x, min_y),
551            make_point(min_x, max_y),
552            make_point(max_x, max_y),
553            make_point(max_x, min_y),
554            make_point(min_x, min_y),
555        ]);
556        RingBuffer.apply(&ring, settings, coordinate_strategy)
557    }
558}
559
560impl<G> BufferStrategy<G, CartesianBuffer> for MultiPointBuffer
561where
562    G: MultiPointTrait<ItemPoint = <G as Geometry>::Point>,
563    G::Point: PointMut + Default + Copy,
564    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
565    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
566{
567    fn apply(
568        &self,
569        points: &G,
570        settings: BufferSettings,
571        coordinate_strategy: &CartesianBuffer,
572    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
573        let mut output = MultiPolygon::new();
574        for point in points.points() {
575            output
576                .0
577                .extend(PointBuffer.apply(point, settings, coordinate_strategy)?.0);
578        }
579        crate::merge::merge_polygons(output.0)
580    }
581}
582
583impl<G> BufferStrategy<G, CartesianBuffer> for MultiLinestringBuffer
584where
585    G: MultiLinestringTrait,
586    G::Point: PointMut + Default + Copy,
587    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
588    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
589{
590    fn apply(
591        &self,
592        lines: &G,
593        settings: BufferSettings,
594        coordinate_strategy: &CartesianBuffer,
595    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
596        let mut output = MultiPolygon::new();
597        for line in lines.linestrings() {
598            output.0.extend(
599                LinestringBuffer
600                    .apply(line, settings, coordinate_strategy)?
601                    .0,
602            );
603        }
604        crate::merge::merge_polygons(output.0)
605    }
606}
607
608impl<G> BufferStrategy<G, CartesianBuffer> for MultiPolygonBuffer
609where
610    G: MultiPolygonTrait,
611    G::Point: PointMut + Default + Copy,
612    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
613    <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
614{
615    fn apply(
616        &self,
617        polygons: &G,
618        settings: BufferSettings,
619        coordinate_strategy: &CartesianBuffer,
620    ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
621        let mut output = MultiPolygon::new();
622        for polygon in polygons.polygons() {
623            output.0.extend(
624                PolygonBuffer
625                    .apply(polygon, settings, coordinate_strategy)?
626                    .0,
627            );
628        }
629        crate::merge::merge_polygons(output.0)
630    }
631}
632
633trait AngularCoordinateSystem {
634    type Units: AngleUnit;
635}
636
637impl<Units: AngleUnit> AngularCoordinateSystem for Spherical<Units> {
638    type Units = Units;
639}
640
641impl<Units: AngleUnit> AngularCoordinateSystem for Geographic<Units> {
642    type Units = Units;
643}
644
645#[derive(Debug, Clone, Copy)]
646struct LocalProjection {
647    longitude: f64,
648    latitude: f64,
649    east_scale: f64,
650    north_scale: f64,
651}
652
653impl LocalProjection {
654    fn project(self, longitude: f64, latitude: f64) -> (f64, f64) {
655        let mut delta_longitude = longitude - self.longitude;
656        if delta_longitude > core::f64::consts::PI {
657            delta_longitude -= 2.0 * core::f64::consts::PI;
658        } else if delta_longitude < -core::f64::consts::PI {
659            delta_longitude += 2.0 * core::f64::consts::PI;
660        }
661        (
662            delta_longitude * self.east_scale,
663            (latitude - self.latitude) * self.north_scale,
664        )
665    }
666
667    fn unproject(self, x: f64, y: f64) -> (f64, f64) {
668        let mut longitude = self.longitude + x / self.east_scale;
669        if longitude > core::f64::consts::PI {
670            longitude -= 2.0 * core::f64::consts::PI;
671        } else if longitude < -core::f64::consts::PI {
672            longitude += 2.0 * core::f64::consts::PI;
673        }
674        (longitude, self.latitude + y / self.north_scale)
675    }
676}
677
678trait AngularBufferProjection {
679    fn projection(&self, longitude: f64, latitude: f64) -> Result<LocalProjection, OverlayError>;
680}
681
682impl AngularBufferProjection for SphericalBuffer {
683    fn projection(&self, longitude: f64, latitude: f64) -> Result<LocalProjection, OverlayError> {
684        if !self.radius.is_finite() || self.radius <= 0.0 {
685            return Err(OverlayError::Unsupported);
686        }
687        let longitude_scale = cos(latitude);
688        if longitude_scale.abs() <= f64::EPSILON {
689            return Err(OverlayError::Unsupported);
690        }
691        let east_scale = self.radius * longitude_scale;
692        Ok(LocalProjection {
693            longitude,
694            latitude,
695            east_scale,
696            north_scale: self.radius,
697        })
698    }
699}
700
701impl AngularBufferProjection for GeographicBuffer {
702    fn projection(&self, longitude: f64, latitude: f64) -> Result<LocalProjection, OverlayError> {
703        let spheroid = self.spheroid;
704        if !spheroid.equatorial_radius.is_finite()
705            || spheroid.equatorial_radius <= 0.0
706            || !spheroid.flattening.is_finite()
707            || !(0.0..1.0).contains(&spheroid.flattening)
708        {
709            return Err(OverlayError::Unsupported);
710        }
711
712        let eccentricity_squared = spheroid.eccentricity_squared();
713        let sin_latitude = sin(latitude);
714        let denominator = sqrt(1.0 - eccentricity_squared * sin_latitude * sin_latitude);
715        let prime_vertical = spheroid.equatorial_radius / denominator;
716        let meridional = spheroid.equatorial_radius * (1.0 - eccentricity_squared)
717            / (denominator * denominator * denominator);
718        let longitude_scale = cos(latitude);
719        if longitude_scale.abs() <= f64::EPSILON {
720            return Err(OverlayError::Unsupported);
721        }
722        let east_scale = prime_vertical * longitude_scale;
723        Ok(LocalProjection {
724            longitude,
725            latitude,
726            east_scale,
727            north_scale: meridional,
728        })
729    }
730}
731
732fn angular_coordinates<P>(point: &P) -> (f64, f64)
733where
734    P: Point,
735    P::Scalar: Into<f64>,
736    P::Cs: AngularCoordinateSystem,
737{
738    let longitude = <P::Cs as AngularCoordinateSystem>::Units::to_radians(point.get::<0>().into());
739    let latitude = <P::Cs as AngularCoordinateSystem>::Units::to_radians(point.get::<1>().into());
740    (longitude, latitude)
741}
742
743fn angular_point<P>(longitude: f64, latitude: f64) -> P
744where
745    P: PointMut + Default,
746    P::Scalar: FromF64,
747    P::Cs: AngularCoordinateSystem,
748{
749    let mut point = P::default();
750    let longitude = <P::Cs as AngularCoordinateSystem>::Units::from_radians(longitude);
751    let latitude = <P::Cs as AngularCoordinateSystem>::Units::from_radians(latitude);
752    point.set::<0>(P::Scalar::from_f64(longitude));
753    point.set::<1>(P::Scalar::from_f64(latitude));
754    point
755}
756
757fn projection_center(coordinates: &[(f64, f64)]) -> Result<(f64, f64), OverlayError> {
758    if coordinates.is_empty() {
759        return Err(OverlayError::Unsupported);
760    }
761    let mut longitude_sine = 0.0;
762    let mut longitude_cosine = 0.0;
763    let mut latitude = 0.0;
764    for &(longitude, point_latitude) in coordinates {
765        longitude_sine += sin(longitude);
766        longitude_cosine += cos(longitude);
767        latitude += point_latitude;
768    }
769    let count = coordinates.len() as f64;
770    Ok((atan2(longitude_sine, longitude_cosine), latitude / count))
771}
772
773type ProjectedPoint = Point2D<f64, Cartesian>;
774
775fn projected_point<P>(point: &P, projection: LocalProjection) -> ProjectedPoint
776where
777    P: Point,
778    P::Scalar: Into<f64>,
779    P::Cs: AngularCoordinateSystem,
780{
781    let (longitude, latitude) = angular_coordinates(point);
782    let (x, y) = projection.project(longitude, latitude);
783    ProjectedPoint::new(x, y)
784}
785
786fn projected_ring<R>(ring: &R, projection: LocalProjection) -> Ring<ProjectedPoint>
787where
788    R: RingTrait,
789    R::Point: Point,
790    <R::Point as Point>::Scalar: Into<f64>,
791    <R::Point as Point>::Cs: AngularCoordinateSystem,
792{
793    Ring::from_vec(
794        ring.points()
795            .map(|point| projected_point(point, projection))
796            .collect(),
797    )
798}
799
800fn projected_polygon<G>(polygon: &G, projection: LocalProjection) -> Polygon<ProjectedPoint>
801where
802    G: PolygonTrait,
803    G::Point: Point,
804    <G::Point as Point>::Scalar: Into<f64>,
805    <G::Point as Point>::Cs: AngularCoordinateSystem,
806{
807    Polygon::with_inners(
808        projected_ring(polygon.exterior(), projection),
809        polygon
810            .interiors()
811            .map(|ring| projected_ring(ring, projection))
812            .collect(),
813    )
814}
815
816fn unprojected_buffer<P>(
817    polygons: MultiPolygon<Polygon<ProjectedPoint>>,
818    projection: LocalProjection,
819) -> MultiPolygon<Polygon<P>>
820where
821    P: PointMut + Default,
822    P::Scalar: FromF64,
823    P::Cs: AngularCoordinateSystem,
824{
825    MultiPolygon::from_vec(
826        polygons
827            .0
828            .into_iter()
829            .map(|polygon| {
830                let outer = Ring::from_vec(
831                    polygon
832                        .outer
833                        .0
834                        .into_iter()
835                        .map(|point| {
836                            let (longitude, latitude) = projection.unproject(point.x(), point.y());
837                            angular_point(longitude, latitude)
838                        })
839                        .collect(),
840                );
841                let inners = polygon
842                    .inners
843                    .into_iter()
844                    .map(|ring| {
845                        Ring::from_vec(
846                            ring.0
847                                .into_iter()
848                                .map(|point| {
849                                    let (longitude, latitude) =
850                                        projection.unproject(point.x(), point.y());
851                                    angular_point(longitude, latitude)
852                                })
853                                .collect(),
854                        )
855                    })
856                    .collect();
857                Polygon::with_inners(outer, inners)
858            })
859            .collect(),
860    )
861}
862
863fn projection_for_points<'a, P>(
864    points: impl IntoIterator<Item = &'a P>,
865    strategy: &impl AngularBufferProjection,
866) -> Result<LocalProjection, OverlayError>
867where
868    P: Point + 'a,
869    P::Scalar: Into<f64>,
870    P::Cs: AngularCoordinateSystem,
871{
872    let coordinates: Vec<_> = points.into_iter().map(angular_coordinates).collect();
873    let (longitude, latitude) = projection_center(&coordinates)?;
874    strategy.projection(longitude, latitude)
875}
876
877fn projected_point_apply<P>(
878    point: &P,
879    settings: BufferSettings,
880    strategy: &impl AngularBufferProjection,
881) -> Result<MultiPolygon<Polygon<P>>, OverlayError>
882where
883    P: Point + PointMut + Default + Copy,
884    P::Scalar: CoordinateScalar + Into<f64> + FromF64,
885    P::Cs: AngularCoordinateSystem,
886{
887    let projection = projection_for_points(core::iter::once(point), strategy)?;
888    let point = projected_point(point, projection);
889    let output = PointBuffer.apply(&point, settings, &CartesianBuffer)?;
890    Ok(unprojected_buffer(output, projection))
891}
892
893fn projected_linestring_apply<L>(
894    line: &L,
895    settings: BufferSettings,
896    strategy: &impl AngularBufferProjection,
897) -> Result<MultiPolygon<Polygon<L::Point>>, OverlayError>
898where
899    L: LinestringTrait,
900    L::Point: PointMut + Default + Copy,
901    <L::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
902    <L::Point as Point>::Cs: AngularCoordinateSystem,
903{
904    let projection = projection_for_points(line.points(), strategy)?;
905    let projected = Linestring::from_vec(
906        line.points()
907            .map(|point| projected_point(point, projection))
908            .collect(),
909    );
910    let output = LinestringBuffer.apply(&projected, settings, &CartesianBuffer)?;
911    Ok(unprojected_buffer(output, projection))
912}
913
914fn projected_ring_apply<R>(
915    ring: &R,
916    settings: BufferSettings,
917    strategy: &impl AngularBufferProjection,
918) -> Result<MultiPolygon<Polygon<R::Point>>, OverlayError>
919where
920    R: RingTrait,
921    R::Point: PointMut + Default + Copy,
922    <R::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
923    <R::Point as Point>::Cs: AngularCoordinateSystem,
924{
925    let projection = projection_for_points(ring.points(), strategy)?;
926    let output = RingBuffer.apply(
927        &projected_ring(ring, projection),
928        settings,
929        &CartesianBuffer,
930    )?;
931    Ok(unprojected_buffer(output, projection))
932}
933
934fn projected_polygon_apply<G>(
935    polygon: &G,
936    settings: BufferSettings,
937    strategy: &impl AngularBufferProjection,
938) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError>
939where
940    G: PolygonTrait,
941    G::Point: PointMut + Default + Copy,
942    <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
943    <G::Point as Point>::Cs: AngularCoordinateSystem,
944{
945    let mut coordinates = polygon
946        .exterior()
947        .points()
948        .map(angular_coordinates)
949        .collect::<Vec<_>>();
950    for ring in polygon.interiors() {
951        coordinates.extend(ring.points().map(angular_coordinates));
952    }
953    let (longitude, latitude) = projection_center(&coordinates)?;
954    let projection = strategy.projection(longitude, latitude)?;
955    let output = PolygonBuffer.apply(
956        &projected_polygon(polygon, projection),
957        settings,
958        &CartesianBuffer,
959    )?;
960    Ok(unprojected_buffer(output, projection))
961}
962
963macro_rules! impl_angular_buffer_strategy {
964    ($strategy:ty, $family:ty) => {
965        impl<G> BufferStrategy<G, $strategy> for PointBuffer
966        where
967            G: Point + PointMut + Default + Copy,
968            G::Scalar: CoordinateScalar + Into<f64> + FromF64,
969            G::Cs: AngularCoordinateSystem,
970            <G::Cs as CoordinateSystem>::Family: SameAs<$family>,
971        {
972            fn apply(
973                &self,
974                geometry: &G,
975                settings: BufferSettings,
976                coordinate_strategy: &$strategy,
977            ) -> Result<MultiPolygon<Polygon<G>>, OverlayError> {
978                projected_point_apply(geometry, settings, coordinate_strategy)
979            }
980        }
981
982        impl<G> BufferStrategy<G, $strategy> for LinestringBuffer
983        where
984            G: LinestringTrait,
985            G::Point: PointMut + Default + Copy,
986            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
987            <G::Point as Point>::Cs: AngularCoordinateSystem,
988            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
989        {
990            fn apply(
991                &self,
992                geometry: &G,
993                settings: BufferSettings,
994                coordinate_strategy: &$strategy,
995            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
996                projected_linestring_apply(geometry, settings, coordinate_strategy)
997            }
998        }
999
1000        impl<G> BufferStrategy<G, $strategy> for SegmentBuffer
1001        where
1002            G: SegmentTrait,
1003            G::Point: PointMut + Default + Copy,
1004            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1005            <G::Point as Point>::Cs: AngularCoordinateSystem,
1006            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1007        {
1008            fn apply(
1009                &self,
1010                geometry: &G,
1011                settings: BufferSettings,
1012                coordinate_strategy: &$strategy,
1013            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1014                let line = Linestring::from_vec(alloc::vec![
1015                    segment_start(geometry),
1016                    segment_end(geometry),
1017                ]);
1018                projected_linestring_apply(&line, settings, coordinate_strategy)
1019            }
1020        }
1021
1022        impl<G> BufferStrategy<G, $strategy> for RingBuffer
1023        where
1024            G: RingTrait,
1025            G::Point: PointMut + Default + Copy,
1026            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1027            <G::Point as Point>::Cs: AngularCoordinateSystem,
1028            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1029        {
1030            fn apply(
1031                &self,
1032                geometry: &G,
1033                settings: BufferSettings,
1034                coordinate_strategy: &$strategy,
1035            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1036                projected_ring_apply(geometry, settings, coordinate_strategy)
1037            }
1038        }
1039
1040        impl<G> BufferStrategy<G, $strategy> for PolygonBuffer
1041        where
1042            G: PolygonTrait,
1043            G::Point: PointMut + Default + Copy,
1044            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1045            <G::Point as Point>::Cs: AngularCoordinateSystem,
1046            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1047        {
1048            fn apply(
1049                &self,
1050                geometry: &G,
1051                settings: BufferSettings,
1052                coordinate_strategy: &$strategy,
1053            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1054                projected_polygon_apply(geometry, settings, coordinate_strategy)
1055            }
1056        }
1057
1058        impl<G> BufferStrategy<G, $strategy> for BoxBuffer
1059        where
1060            G: BoxTrait,
1061            G::Point: PointMut + Default + Copy,
1062            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1063            <G::Point as Point>::Cs: AngularCoordinateSystem,
1064            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1065        {
1066            fn apply(
1067                &self,
1068                geometry: &G,
1069                settings: BufferSettings,
1070                coordinate_strategy: &$strategy,
1071            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1072                let minimum = box_min(geometry);
1073                let maximum = box_max(geometry);
1074                let projection = projection_for_points([&minimum, &maximum], coordinate_strategy)?;
1075                let projected = ModelBox::from_corners(
1076                    projected_point(&minimum, projection),
1077                    projected_point(&maximum, projection),
1078                );
1079                let output = BoxBuffer.apply(&projected, settings, &CartesianBuffer)?;
1080                Ok(unprojected_buffer(output, projection))
1081            }
1082        }
1083
1084        impl<G> BufferStrategy<G, $strategy> for MultiPointBuffer
1085        where
1086            G: MultiPointTrait<ItemPoint = <G as Geometry>::Point>,
1087            G::Point: PointMut + Default + Copy,
1088            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1089            <G::Point as Point>::Cs: AngularCoordinateSystem,
1090            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1091        {
1092            fn apply(
1093                &self,
1094                geometry: &G,
1095                settings: BufferSettings,
1096                coordinate_strategy: &$strategy,
1097            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1098                let projection = projection_for_points(geometry.points(), coordinate_strategy)?;
1099                let projected = MultiPoint::from_vec(
1100                    geometry
1101                        .points()
1102                        .map(|point| projected_point(point, projection))
1103                        .collect(),
1104                );
1105                let output = MultiPointBuffer.apply(&projected, settings, &CartesianBuffer)?;
1106                Ok(unprojected_buffer(output, projection))
1107            }
1108        }
1109
1110        impl<G> BufferStrategy<G, $strategy> for MultiLinestringBuffer
1111        where
1112            G: MultiLinestringTrait,
1113            G::Point: PointMut + Default + Copy,
1114            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1115            <G::Point as Point>::Cs: AngularCoordinateSystem,
1116            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1117        {
1118            fn apply(
1119                &self,
1120                geometry: &G,
1121                settings: BufferSettings,
1122                coordinate_strategy: &$strategy,
1123            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1124                let coordinates = geometry
1125                    .linestrings()
1126                    .flat_map(|line| line.points().map(angular_coordinates))
1127                    .collect::<Vec<_>>();
1128                let (longitude, latitude) = projection_center(&coordinates)?;
1129                let projection = coordinate_strategy.projection(longitude, latitude)?;
1130                let projected = MultiLinestring::from_vec(
1131                    geometry
1132                        .linestrings()
1133                        .map(|line| {
1134                            Linestring::from_vec(
1135                                line.points()
1136                                    .map(|point| projected_point(point, projection))
1137                                    .collect(),
1138                            )
1139                        })
1140                        .collect(),
1141                );
1142                let output = MultiLinestringBuffer.apply(&projected, settings, &CartesianBuffer)?;
1143                Ok(unprojected_buffer(output, projection))
1144            }
1145        }
1146
1147        impl<G> BufferStrategy<G, $strategy> for MultiPolygonBuffer
1148        where
1149            G: MultiPolygonTrait,
1150            G::Point: PointMut + Default + Copy,
1151            <G::Point as Point>::Scalar: CoordinateScalar + Into<f64> + FromF64,
1152            <G::Point as Point>::Cs: AngularCoordinateSystem,
1153            <<G::Point as Point>::Cs as CoordinateSystem>::Family: SameAs<$family>,
1154        {
1155            fn apply(
1156                &self,
1157                geometry: &G,
1158                settings: BufferSettings,
1159                coordinate_strategy: &$strategy,
1160            ) -> Result<MultiPolygon<Polygon<G::Point>>, OverlayError> {
1161                let coordinates = geometry
1162                    .polygons()
1163                    .flat_map(|polygon| {
1164                        polygon
1165                            .exterior()
1166                            .points()
1167                            .chain(polygon.interiors().flat_map(RingTrait::points))
1168                            .map(angular_coordinates)
1169                    })
1170                    .collect::<Vec<_>>();
1171                let (longitude, latitude) = projection_center(&coordinates)?;
1172                let projection = coordinate_strategy.projection(longitude, latitude)?;
1173                let projected = MultiPolygon::from_vec(
1174                    geometry
1175                        .polygons()
1176                        .map(|polygon| projected_polygon(polygon, projection))
1177                        .collect(),
1178                );
1179                let output = MultiPolygonBuffer.apply(&projected, settings, &CartesianBuffer)?;
1180                Ok(unprojected_buffer(output, projection))
1181            }
1182        }
1183    };
1184}
1185
1186impl_angular_buffer_strategy!(SphericalBuffer, SphericalFamily);
1187impl_angular_buffer_strategy!(GeographicBuffer, GeographicFamily);
1188
1189/// Buffer a point by `distance`, producing the disc (or square)
1190/// approximation.
1191///
1192/// Mirrors the point arm of `boost::geometry::buffer` with a
1193/// `point_circle` / `point_square` strategy
1194/// (`strategies/buffer/buffer_point_circle.hpp`).
1195///
1196/// # Examples
1197///
1198/// ```
1199/// use geometry_cs::Cartesian;
1200/// use geometry_model::Point2D;
1201/// use geometry_overlay::buffer::{buffer_point, PointStrategy};
1202/// use geometry_algorithm::ring_area;
1203///
1204/// type P = Point2D<f64, Cartesian>;
1205/// let disc = buffer_point(&P::new(0.0, 0.0), 1.0, PointStrategy::Circle { points_per_circle: 360 });
1206/// // Area of the 360-gon closely approximates π.
1207/// assert!((ring_area(&disc).abs() - core::f64::consts::PI).abs() < 1e-3);
1208/// ```
1209#[inline]
1210#[must_use]
1211pub fn buffer_point<P>(center: &P, distance: f64, strategy: PointStrategy) -> Ring<P>
1212where
1213    P: PointMut + Default + Copy,
1214    P::Scalar: CoordinateScalar + Into<f64> + FromF64,
1215    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
1216{
1217    let cx: f64 = center.get::<0>().into();
1218    let cy: f64 = center.get::<1>().into();
1219    match strategy {
1220        PointStrategy::Circle { points_per_circle } => {
1221            circle_ring(cx, cy, distance, points_per_circle.max(3))
1222        }
1223        PointStrategy::Square => {
1224            let d = distance;
1225            // Fully-qualified `alloc::vec!`: only the `Vec` *type* is
1226            // imported (line 33), and the bare `vec!` macro is not in the
1227            // `no_std` prelude — matches the crate idiom in `assemble.rs`
1228            // / `traverse/state.rs`.
1229            Ring::from_vec(alloc::vec![
1230                make_point(cx - d, cy - d),
1231                make_point(cx - d, cy + d),
1232                make_point(cx + d, cy + d),
1233                make_point(cx + d, cy - d),
1234                make_point(cx - d, cy - d),
1235            ])
1236        }
1237    }
1238}
1239
1240/// Buffer a **convex** polygon outward by a positive `distance`, rounding
1241/// the corners per `join`.
1242///
1243/// Each vertex of a convex polygon becomes a circular arc of radius
1244/// `distance` in the offset boundary; the arcs are joined by the offset
1245/// edges. Mirrors the convex case of `boost::geometry::buffer`
1246/// (`algorithms/buffer.hpp`) with a `join_round` strategy.
1247///
1248/// # Panics
1249///
1250/// Does not panic; a polygon with fewer than 3 exterior vertices returns
1251/// an empty ring's polygon.
1252///
1253/// # Examples
1254///
1255/// ```
1256/// use geometry_cs::Cartesian;
1257/// use geometry_model::{polygon, Point2D, Polygon};
1258/// use geometry_overlay::buffer::{buffer_convex_polygon, JoinStrategy};
1259/// use geometry_algorithm::ring_area;
1260/// use geometry_trait::Polygon as _;
1261///
1262/// type P = Point2D<f64, Cartesian>;
1263/// 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)]];
1264/// let grown = buffer_convex_polygon(&sq, 1.0, JoinStrategy::Round { points_per_circle: 720 });
1265/// // Area = s² + 4·s·d + π·d² = 4 + 8 + π.
1266/// let expected = 4.0 + 8.0 + core::f64::consts::PI;
1267/// assert!((ring_area(grown.exterior()).abs() - expected).abs() < 5e-2);
1268/// ```
1269#[inline]
1270#[must_use]
1271pub fn buffer_convex_polygon<G, P>(polygon: &G, distance: f64, join: JoinStrategy) -> Polygon<P>
1272where
1273    G: PolygonTrait<Point = P>,
1274    P: PointMut + Default + Copy,
1275    P::Scalar: CoordinateScalar + Into<f64> + FromF64,
1276    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
1277{
1278    let strategy = match join {
1279        JoinStrategy::Round { points_per_circle } => {
1280            BufferJoinStrategy::Round { points_per_circle }
1281        }
1282        JoinStrategy::Miter => BufferJoinStrategy::Miter {
1283            limit: f64::INFINITY,
1284        },
1285    };
1286    offset_ring(polygon.exterior(), distance, strategy, true)
1287        .map_or_else(|| Polygon::new(Ring::new()), Polygon::new)
1288}
1289
1290fn offset_ring<R, P>(
1291    ring: &R,
1292    distance: f64,
1293    join: BufferJoinStrategy,
1294    clockwise: bool,
1295) -> Option<Ring<P>>
1296where
1297    R: RingTrait<Point = P>,
1298    P: PointMut + Default + Copy,
1299    P::Scalar: Into<f64> + FromF64,
1300{
1301    let mut vertices = distinct_vertices(ring);
1302    if vertices.len() < 3 || !distance.is_finite() || distance == 0.0 {
1303        return None;
1304    }
1305    if signed_area_ccw_positive(&vertices) < 0.0 {
1306        vertices.reverse();
1307    }
1308
1309    let count = vertices.len();
1310    let mut boundary = Vec::new();
1311    for index in 0..count {
1312        let previous = vertices[(index + count - 1) % count];
1313        let vertex = vertices[index];
1314        let next = vertices[(index + 1) % count];
1315        let incoming = (vertex.0 - previous.0, vertex.1 - previous.1);
1316        let outgoing = (next.0 - vertex.0, next.1 - vertex.1);
1317        let incoming_normal = outward_normal(incoming.0, incoming.1);
1318        let outgoing_normal = outward_normal(outgoing.0, outgoing.1);
1319        let before = (
1320            vertex.0 + incoming_normal.0 * distance,
1321            vertex.1 + incoming_normal.1 * distance,
1322        );
1323        let after = (
1324            vertex.0 + outgoing_normal.0 * distance,
1325            vertex.1 + outgoing_normal.1 * distance,
1326        );
1327        let intersection = line_intersection(before, incoming, after, outgoing);
1328        let cross = incoming.0 * outgoing.1 - incoming.1 * outgoing.0;
1329        let exterior_join = cross * distance > 0.0;
1330
1331        if !exterior_join {
1332            if let Some(point) = intersection {
1333                boundary.push(point);
1334            } else {
1335                boundary.push(after);
1336            }
1337            continue;
1338        }
1339
1340        match join {
1341            BufferJoinStrategy::Round { points_per_circle } => {
1342                boundary.push(before);
1343                push_arc_between(
1344                    &mut boundary,
1345                    vertex,
1346                    before,
1347                    after,
1348                    distance.abs(),
1349                    points_per_circle.max(4),
1350                    true,
1351                );
1352                boundary.push(after);
1353            }
1354            BufferJoinStrategy::Miter { limit } => {
1355                if let Some(point) = intersection {
1356                    let miter_length = hypot(point.0 - vertex.0, point.1 - vertex.1);
1357                    if point.0.is_finite()
1358                        && point.1.is_finite()
1359                        && miter_length <= limit.max(1.0) * distance.abs()
1360                    {
1361                        boundary.push(point);
1362                    } else {
1363                        boundary.push(before);
1364                        boundary.push(after);
1365                    }
1366                } else {
1367                    boundary.push(before);
1368                    boundary.push(after);
1369                }
1370            }
1371        }
1372    }
1373
1374    boundary.dedup();
1375    if boundary.len() < 3 || signed_area_ccw_positive(&boundary).abs() <= f64::EPSILON {
1376        return None;
1377    }
1378    if distance < 0.0 {
1379        let clearance = distance.abs();
1380        let tolerance = mul_add(clearance, 1e-9, f64::EPSILON * 16.0);
1381        if boundary.iter().any(|point| {
1382            !point_in_or_on_ring(*point, &vertices)
1383                || minimum_boundary_distance(*point, &vertices) + tolerance < clearance
1384        }) {
1385            return None;
1386        }
1387    }
1388    if clockwise == (signed_area_ccw_positive(&boundary) > 0.0) {
1389        boundary.reverse();
1390    }
1391    boundary.push(boundary[0]);
1392    Some(Ring::from_vec(
1393        boundary
1394            .into_iter()
1395            .map(|(x, y)| make_point(x, y))
1396            .collect(),
1397    ))
1398}
1399
1400fn buffer_linestring<L, P>(
1401    line: &L,
1402    left: f64,
1403    right: f64,
1404    join: BufferJoinStrategy,
1405    end: BufferEndStrategy,
1406) -> Result<Polygon<P>, OverlayError>
1407where
1408    L: LinestringTrait<Point = P>,
1409    P: PointMut + Default + Copy,
1410    P::Scalar: Into<f64> + FromF64,
1411{
1412    let mut vertices: Vec<(f64, f64)> = Vec::new();
1413    for point in line.points() {
1414        let value = (point.get::<0>().into(), point.get::<1>().into());
1415        if vertices.last().copied() != Some(value) {
1416            vertices.push(value);
1417        }
1418    }
1419    if vertices.len() < 2 {
1420        return Err(OverlayError::Unsupported);
1421    }
1422
1423    let left_path = offset_path(&vertices, left, true, join);
1424    let right_path = offset_path(&vertices, right, false, join);
1425    debug_assert!(!left_path.is_empty() && !right_path.is_empty());
1426    let mut boundary = left_path;
1427    match end {
1428        BufferEndStrategy::Flat => {}
1429        BufferEndStrategy::Round { points_per_circle } => {
1430            let center = *vertices.last().expect("linestring has an endpoint");
1431            let from = *boundary.last().expect("left path has an endpoint");
1432            let to = *right_path.last().expect("right path has an endpoint");
1433            push_end_arc(
1434                &mut boundary,
1435                center,
1436                from,
1437                to,
1438                points_per_circle.max(4),
1439                true,
1440            );
1441        }
1442    }
1443    boundary.extend(right_path.iter().rev().copied());
1444    if let BufferEndStrategy::Round { points_per_circle } = end {
1445        let to = boundary[0];
1446        push_end_arc(
1447            &mut boundary,
1448            vertices[0],
1449            right_path[0],
1450            to,
1451            points_per_circle.max(4),
1452            true,
1453        );
1454    }
1455    let first = boundary[0];
1456    boundary.push(first);
1457    Ok(Polygon::new(Ring::from_vec(
1458        boundary
1459            .into_iter()
1460            .map(|(x, y)| make_point(x, y))
1461            .collect(),
1462    )))
1463}
1464
1465fn offset_path(
1466    vertices: &[(f64, f64)],
1467    distance: f64,
1468    left: bool,
1469    join: BufferJoinStrategy,
1470) -> Vec<(f64, f64)> {
1471    let side = if left { 1.0 } else { -1.0 };
1472    let normals: Vec<(f64, f64)> = vertices
1473        .windows(2)
1474        .map(|edge| {
1475            let dx = edge[1].0 - edge[0].0;
1476            let dy = edge[1].1 - edge[0].1;
1477            let length = hypot(dx, dy);
1478            (-dy / length * side, dx / length * side)
1479        })
1480        .collect();
1481    let mut path = Vec::with_capacity(vertices.len());
1482    path.push((
1483        vertices[0].0 + normals[0].0 * distance,
1484        vertices[0].1 + normals[0].1 * distance,
1485    ));
1486    for index in 1..vertices.len() - 1 {
1487        let vertex = vertices[index];
1488        let previous = vertices[index - 1];
1489        let next = vertices[index + 1];
1490        let before = (
1491            vertex.0 + normals[index - 1].0 * distance,
1492            vertex.1 + normals[index - 1].1 * distance,
1493        );
1494        let after = (
1495            vertex.0 + normals[index].0 * distance,
1496            vertex.1 + normals[index].1 * distance,
1497        );
1498        let intersection = line_intersection(
1499            before,
1500            (vertex.0 - previous.0, vertex.1 - previous.1),
1501            after,
1502            (next.0 - vertex.0, next.1 - vertex.1),
1503        );
1504        match (join, intersection) {
1505            (BufferJoinStrategy::Miter { limit }, Some(point))
1506                if point.0.is_finite() && point.1.is_finite() =>
1507            {
1508                let miter_length = hypot(point.0 - vertex.0, point.1 - vertex.1);
1509                if distance == 0.0 || miter_length <= limit.max(1.0) * distance.abs() {
1510                    path.push(point);
1511                } else {
1512                    path.push(before);
1513                    path.push(after);
1514                }
1515            }
1516            (BufferJoinStrategy::Round { points_per_circle }, _) => {
1517                path.push(before);
1518                push_arc_between(
1519                    &mut path,
1520                    vertex,
1521                    before,
1522                    after,
1523                    distance.abs(),
1524                    points_per_circle.max(4),
1525                    left,
1526                );
1527                path.push(after);
1528            }
1529            _ => {
1530                path.push(before);
1531                path.push(after);
1532            }
1533        }
1534    }
1535    let last = vertices.len() - 1;
1536    path.push((
1537        vertices[last].0 + normals[last - 1].0 * distance,
1538        vertices[last].1 + normals[last - 1].1 * distance,
1539    ));
1540    path
1541}
1542
1543fn line_intersection(
1544    first_origin: (f64, f64),
1545    first_direction: (f64, f64),
1546    second_origin: (f64, f64),
1547    second_direction: (f64, f64),
1548) -> Option<(f64, f64)> {
1549    let denominator =
1550        first_direction.0 * second_direction.1 - first_direction.1 * second_direction.0;
1551    if denominator.abs() <= f64::EPSILON {
1552        return None;
1553    }
1554    let delta = (
1555        second_origin.0 - first_origin.0,
1556        second_origin.1 - first_origin.1,
1557    );
1558    let factor = (delta.0 * second_direction.1 - delta.1 * second_direction.0) / denominator;
1559    Some((
1560        first_origin.0 + factor * first_direction.0,
1561        first_origin.1 + factor * first_direction.1,
1562    ))
1563}
1564
1565fn push_arc_between(
1566    output: &mut Vec<(f64, f64)>,
1567    center: (f64, f64),
1568    from: (f64, f64),
1569    to: (f64, f64),
1570    radius: f64,
1571    points_per_circle: usize,
1572    counterclockwise: bool,
1573) {
1574    if radius == 0.0 {
1575        return;
1576    }
1577    let start = atan2(from.1 - center.1, from.0 - center.0);
1578    let mut end = atan2(to.1 - center.1, to.0 - center.0);
1579    if counterclockwise {
1580        while end < start {
1581            end += core::f64::consts::TAU;
1582        }
1583    } else {
1584        while end > start {
1585            end -= core::f64::consts::TAU;
1586        }
1587    }
1588    let sweep = end - start;
1589    let steps =
1590        ceil((sweep.abs() / core::f64::consts::TAU) * points_per_circle as f64).max(1.0) as usize;
1591    for step in 1..steps {
1592        let angle = start + sweep * step as f64 / steps as f64;
1593        output.push((
1594            center.0 + radius * cos(angle),
1595            center.1 + radius * sin(angle),
1596        ));
1597    }
1598}
1599
1600fn push_end_arc(
1601    output: &mut Vec<(f64, f64)>,
1602    center: (f64, f64),
1603    from: (f64, f64),
1604    to: (f64, f64),
1605    points_per_circle: usize,
1606    clockwise: bool,
1607) {
1608    let radius =
1609        hypot(from.0 - center.0, from.1 - center.1).max(hypot(to.0 - center.0, to.1 - center.1));
1610    push_arc_between(
1611        output,
1612        center,
1613        from,
1614        to,
1615        radius,
1616        points_per_circle,
1617        !clockwise,
1618    );
1619}
1620
1621/// Materialise an output point from the `f64` kernel coordinates.
1622fn make_point<P>(x: f64, y: f64) -> P
1623where
1624    P: PointMut + Default,
1625    P::Scalar: FromF64,
1626{
1627    let mut p = P::default();
1628    p.set::<0>(P::Scalar::from_f64(x));
1629    p.set::<1>(P::Scalar::from_f64(y));
1630    p
1631}
1632
1633/// A regular-polygon approximation of a circle, clockwise and closed.
1634fn circle_ring<P>(cx: f64, cy: f64, r: f64, segments: usize) -> Ring<P>
1635where
1636    P: PointMut + Default + Copy,
1637    P::Scalar: FromF64,
1638{
1639    let mut pts = Vec::with_capacity(segments + 1);
1640    let step = core::f64::consts::TAU / segments as f64;
1641    for k in 0..segments {
1642        let a = -step * k as f64;
1643        pts.push(make_point(cx + r * cos(a), cy + r * sin(a)));
1644    }
1645    pts.push(pts[0]);
1646    Ring::from_vec(pts)
1647}
1648
1649/// Distinct consecutive vertices of a ring as `f64` pairs (drops the
1650/// closing repeat).
1651fn distinct_vertices<R>(ring: &R) -> Vec<(f64, f64)>
1652where
1653    R: RingTrait,
1654    <R::Point as Point>::Scalar: Into<f64>,
1655{
1656    let mut pts: Vec<(f64, f64)> = ring
1657        .points()
1658        .map(|p| (p.get::<0>().into(), p.get::<1>().into()))
1659        .collect();
1660    if pts.len() >= 2 {
1661        let first = pts[0];
1662        let last = pts[pts.len() - 1];
1663        if first == last {
1664            pts.pop();
1665        }
1666    }
1667    pts
1668}
1669
1670/// The standard math signed area of the vertex ring (counter-clockwise
1671/// positive), via the shoelace sum over the closed loop. Used only to
1672/// detect winding for normalisation.
1673fn signed_area_ccw_positive(verts: &[(f64, f64)]) -> f64 {
1674    let n = verts.len();
1675    let mut acc = 0.0;
1676    for i in 0..n {
1677        let a = verts[i];
1678        let b = verts[(i + 1) % n];
1679        acc += a.0 * b.1 - b.0 * a.1;
1680    }
1681    acc * 0.5
1682}
1683
1684fn minimum_boundary_distance(point: (f64, f64), vertices: &[(f64, f64)]) -> f64 {
1685    let mut minimum = f64::INFINITY;
1686    for index in 0..vertices.len() {
1687        let start = vertices[index];
1688        let end = vertices[(index + 1) % vertices.len()];
1689        let delta = (end.0 - start.0, end.1 - start.1);
1690        let length_squared = delta.0 * delta.0 + delta.1 * delta.1;
1691        let fraction = if length_squared == 0.0 {
1692            0.0
1693        } else {
1694            (((point.0 - start.0) * delta.0 + (point.1 - start.1) * delta.1) / length_squared)
1695                .clamp(0.0, 1.0)
1696        };
1697        let nearest = (start.0 + fraction * delta.0, start.1 + fraction * delta.1);
1698        minimum = minimum.min(hypot(point.0 - nearest.0, point.1 - nearest.1));
1699    }
1700    minimum
1701}
1702
1703fn point_in_or_on_ring(point: (f64, f64), vertices: &[(f64, f64)]) -> bool {
1704    let scale = vertices.iter().fold(1.0_f64, |acc, vertex| {
1705        acc.max(vertex.0.abs()).max(vertex.1.abs())
1706    });
1707    if minimum_boundary_distance(point, vertices) <= scale * 1e-12 {
1708        return true;
1709    }
1710
1711    let mut inside = false;
1712    for index in 0..vertices.len() {
1713        let start = vertices[index];
1714        let end = vertices[(index + 1) % vertices.len()];
1715        if (start.1 > point.1) != (end.1 > point.1)
1716            && point.0 < (end.0 - start.0) * (point.1 - start.1) / (end.1 - start.1) + start.0
1717        {
1718            inside = !inside;
1719        }
1720    }
1721    inside
1722}
1723
1724/// The outward unit normal of a directed CCW edge with delta
1725/// `(dx, dy)` (pointing to the edge's right).
1726fn outward_normal(dx: f64, dy: f64) -> (f64, f64) {
1727    let len = (dx * dx + dy * dy).sqrt();
1728    if len == 0.0 {
1729        return (0.0, 0.0);
1730    }
1731    // Right-hand normal of (dx, dy) is (dy, -dx).
1732    (dy / len, -dx / len)
1733}
1734
1735#[cfg(test)]
1736mod tests {
1737    //! OVL7 done-when: buffered areas match the closed-form values.
1738    //! Mirrors `test/algorithms/buffer/`.
1739
1740    use super::{JoinStrategy, PointStrategy, buffer, buffer_convex_polygon, buffer_point};
1741    use geometry_algorithm::ring_area;
1742    use geometry_cs::Cartesian;
1743    use geometry_model::{Point2D, Polygon, polygon};
1744    use geometry_trait::{MultiPolygon as _, Polygon as _};
1745
1746    type P = Point2D<f64, Cartesian>;
1747
1748    fn close(a: f64, b: f64, tol: f64) {
1749        assert!((a - b).abs() < tol, "expected {b}, got {a}");
1750    }
1751
1752    #[test]
1753    fn point_circle_area_approximates_pi_r_squared() {
1754        let disc = buffer_point(
1755            &P::new(0.0, 0.0),
1756            2.0,
1757            PointStrategy::Circle {
1758                points_per_circle: 720,
1759            },
1760        );
1761        // π·r² = π·4.
1762        close(ring_area(&disc).abs(), core::f64::consts::PI * 4.0, 1e-2);
1763    }
1764
1765    #[test]
1766    fn point_square_area() {
1767        let sq = buffer_point(&P::new(0.0, 0.0), 3.0, PointStrategy::Square);
1768        // A square of half-side 3 → side 6 → area 36.
1769        close(ring_area(&sq).abs(), 36.0, 1e-9);
1770    }
1771
1772    #[test]
1773    fn convex_square_round_buffer_area() {
1774        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)]];
1775        let grown = buffer_convex_polygon(
1776            &sq,
1777            1.0,
1778            JoinStrategy::Round {
1779                points_per_circle: 720,
1780            },
1781        );
1782        // s² + 4·s·d + π·d² = 4 + 8 + π.
1783        let expected = 4.0 + 8.0 + core::f64::consts::PI;
1784        close(ring_area(grown.exterior()).abs(), expected, 1e-2);
1785    }
1786
1787    #[test]
1788    fn convex_triangle_round_buffer_grows() {
1789        let tri: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (0.0, 3.0), (0.0, 0.0)]];
1790        let base = ring_area(tri.exterior()).abs(); // 6
1791        let grown = buffer_convex_polygon(
1792            &tri,
1793            0.5,
1794            JoinStrategy::Round {
1795                points_per_circle: 360,
1796            },
1797        );
1798        // The buffered area must exceed the original.
1799        assert!(ring_area(grown.exterior()).abs() > base);
1800    }
1801
1802    #[test]
1803    fn buffer_is_winding_independent() {
1804        // Regression: the same square listed clockwise and counter-
1805        // clockwise must buffer to the same grown area. The winding
1806        // normalisation makes the outward offset direction correct for
1807        // both.
1808        let ccw: Polygon<P> =
1809            polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
1810        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)]];
1811        let j = JoinStrategy::Round {
1812            points_per_circle: 720,
1813        };
1814        let expected = 4.0 + 8.0 + core::f64::consts::PI;
1815        let grown_from_counterclockwise =
1816            ring_area(buffer_convex_polygon(&ccw, 1.0, j).exterior()).abs();
1817        let grown_from_clockwise = ring_area(buffer_convex_polygon(&cw, 1.0, j).exterior()).abs();
1818        close(grown_from_counterclockwise, expected, 5e-2);
1819        close(grown_from_clockwise, expected, 5e-2);
1820    }
1821
1822    #[test]
1823    fn miter_square_area_is_16() {
1824        // Regression: the old Miter arm placed the corner point at
1825        // distance d along the bisector (ON the round arc), yielding
1826        // 14.83 — smaller than even the round buffer. A true miter
1827        // corner is the offset-edge intersection at √2·d, so the
1828        // buffered 2×2 square is s² + 4·s·d + 4·d² = 16 exactly.
1829        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)]];
1830        let grown = buffer_convex_polygon(&sq, 1.0, JoinStrategy::Miter);
1831        close(ring_area(grown.exterior()).abs(), 16.0, 1e-9);
1832    }
1833
1834    #[test]
1835    fn miter_contains_near_corner_probe() {
1836        // A point at distance 0.99 < d from the input corner, in the
1837        // 22.5° direction, was EXCLUDED by the old chord-cut corner.
1838        use geometry_algorithm::within;
1839        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)]];
1840        let grown = buffer_convex_polygon(&sq, 1.0, JoinStrategy::Miter);
1841        let ang = 22.5_f64.to_radians();
1842        let probe = P::new(2.0 + 0.99 * ang.cos(), 2.0 + 0.99 * ang.sin());
1843        assert!(
1844            within(&probe, &grown),
1845            "buffer must contain points within d"
1846        );
1847    }
1848
1849    #[test]
1850    fn miter_is_superset_of_round_by_area() {
1851        // A miter fills the wedge beyond the round arc, so its area
1852        // can never be below the round join's.
1853        let j_round = JoinStrategy::Round {
1854            points_per_circle: 720,
1855        };
1856        let square: Polygon<P> =
1857            polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
1858        let triangle: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (0.0, 3.0), (0.0, 0.0)]];
1859        for pg in [square, triangle] {
1860            let m =
1861                ring_area(buffer_convex_polygon(&pg, 1.0, JoinStrategy::Miter).exterior()).abs();
1862            let r = ring_area(buffer_convex_polygon(&pg, 1.0, j_round).exterior()).abs();
1863            assert!(m >= r - 1e-9, "miter {m} must not be below round {r}");
1864        }
1865    }
1866
1867    #[test]
1868    fn non_model_polygon_buffers_like_the_model_polygon() {
1869        // The generic signature accepts any `Polygon` trait impl — a
1870        // hand-rolled type must buffer to the same area as the same
1871        // shape held in a model polygon.
1872        use geometry_model::Ring;
1873        use geometry_tag::PolygonTag;
1874        use geometry_trait::{Geometry, Polygon as PolygonTrait};
1875
1876        struct Parcel {
1877            outer: Ring<P>,
1878        }
1879        impl Geometry for Parcel {
1880            type Kind = PolygonTag;
1881            type Point = P;
1882        }
1883        impl PolygonTrait for Parcel {
1884            type Ring = Ring<P>;
1885            fn exterior(&self) -> &Ring<P> {
1886                &self.outer
1887            }
1888            fn interiors(&self) -> impl ExactSizeIterator<Item = &Ring<P>> {
1889                core::iter::empty()
1890            }
1891        }
1892
1893        let pts = vec![
1894            P::new(0.0, 0.0),
1895            P::new(2.0, 0.0),
1896            P::new(2.0, 2.0),
1897            P::new(0.0, 2.0),
1898            P::new(0.0, 0.0),
1899        ];
1900        let parcel = Parcel {
1901            outer: Ring::from_vec(pts.clone()),
1902        };
1903        let model: Polygon<P> = Polygon::new(Ring::from_vec(pts));
1904        let j = JoinStrategy::Round {
1905            points_per_circle: 360,
1906        };
1907        let parcel_buffer = buffer(&parcel, 1.0, j, PointStrategy::Square).unwrap();
1908        let model_buffer = buffer(&model, 1.0, j, PointStrategy::Square).unwrap();
1909        let a = ring_area(parcel_buffer.polygons().next().unwrap().exterior()).abs();
1910        let b = ring_area(model_buffer.polygons().next().unwrap().exterior()).abs();
1911        close(a, b, 1e-12);
1912    }
1913
1914    #[test]
1915    fn miter_is_winding_independent() {
1916        // Same square listed CW and CCW buffers to the same miter area.
1917        let ccw: Polygon<P> =
1918            polygon![[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (0.0, 0.0)]];
1919        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)]];
1920        close(
1921            ring_area(buffer_convex_polygon(&ccw, 1.0, JoinStrategy::Miter).exterior()).abs(),
1922            16.0,
1923            1e-9,
1924        );
1925        close(
1926            ring_area(buffer_convex_polygon(&cw, 1.0, JoinStrategy::Miter).exterior()).abs(),
1927            16.0,
1928            1e-9,
1929        );
1930    }
1931}