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