Skip to main content

xpict_core/geom/
ops.rs

1//! Thin wrappers over `i_overlay` (boolean ops, simplify, outline / stroke).
2
3use i_overlay::core::fill_rule::FillRule;
4use i_overlay::core::overlay_rule::OverlayRule;
5use i_overlay::float::simplify::SimplifyShape;
6use i_overlay::float::single::SingleFloatOverlay;
7use i_overlay::i_shape::float::area::Area;
8use i_overlay::mesh::float::outline::offset::OutlineOffset;
9use i_overlay::mesh::float::stroke::offset::StrokeOffset;
10use i_overlay::mesh::float::style::{LineCap, LineJoin, OutlineStyle, StrokeStyle};
11
12use super::{capsule_polygon, circle_polygon, polygon_to_svg_d};
13
14type Contour = Vec<[f64; 2]>;
15type OverlayShape = Vec<Contour>;
16type Shapes = Vec<OverlayShape>;
17
18/// Multipolygon ink / halo geometry.
19#[derive(Clone, Debug, Default)]
20pub struct Shape {
21    shapes: Shapes,
22}
23
24fn pt(p: (f64, f64)) -> [f64; 2] {
25    [p.0, p.1]
26}
27
28fn xy(p: [f64; 2]) -> (f64, f64) {
29    (p[0], p[1])
30}
31
32impl Shape {
33    fn wrap(shapes: Shapes) -> Self {
34        Self { shapes }
35    }
36
37    pub fn empty() -> Self {
38        Self::wrap(Vec::new())
39    }
40
41    pub fn is_empty(&self) -> bool {
42        self.shapes.is_empty() || self.shapes.iter().all(|s| s.is_empty() || s[0].len() < 3)
43    }
44
45    /// Single exterior ring (lib-simplify cleans winding / collinear).
46    pub fn from_ring(ring: &[(f64, f64)]) -> Self {
47        if ring.len() < 3 {
48            return Self::empty();
49        }
50        let c: Contour = ring.iter().copied().map(pt).collect();
51        Self::wrap(c.simplify_shape(FillRule::NonZero))
52    }
53
54    /// Independent contours → even-odd / XOR shape (glyph counters).
55    pub fn from_contours_evenodd(contours: &[Vec<(f64, f64)>]) -> Self {
56        let parts: Vec<Contour> = contours
57            .iter()
58            .filter(|r| r.len() >= 3)
59            .map(|r| r.iter().copied().map(pt).collect())
60            .collect();
61        if parts.is_empty() {
62            return Self::empty();
63        }
64        Self::wrap(parts.simplify_shape(FillRule::EvenOdd))
65    }
66
67    pub fn disk(cx: f64, cy: f64, radius: f64, quad_segs: u32) -> Self {
68        Self::from_ring(&circle_polygon(cx, cy, radius, quad_segs))
69    }
70
71    pub fn capsule(x1: f64, y1: f64, x2: f64, y2: f64, radius: f64) -> Self {
72        Self::from_ring(&capsule_polygon(x1, y1, x2, y2, radius, 8))
73    }
74
75    pub fn polyline_buffer(pts: &[(f64, f64)], radius: f64) -> Self {
76        if pts.len() < 2 || radius <= 0.0 {
77            return Self::empty();
78        }
79        if pts.len() == 2 {
80            return Self::capsule(pts[0].0, pts[0].1, pts[1].0, pts[1].1, radius);
81        }
82        let path: Contour = pts.iter().copied().map(pt).collect();
83        let r = radius.max(0.05) * 0.2;
84        let style = StrokeStyle::new(radius * 2.0)
85            .line_join(LineJoin::Round(r))
86            .start_cap(LineCap::Round(r))
87            .end_cap(LineCap::Round(r));
88        Self::wrap(path.stroke(style, false))
89    }
90
91    pub fn multipoint_buffer(pts: &[(f64, f64)], radius: f64) -> Self {
92        pts.iter()
93            .fold(Self::empty(), |acc, &(x, y)| acc.union(&Self::disk(x, y, radius, 8)))
94    }
95
96    pub fn annular(cx: f64, cy: f64, r: f64, stroke_width: f64) -> Self {
97        if r <= 0.0 || stroke_width <= 0.0 {
98            return Self::empty();
99        }
100        let outer = Self::disk(cx, cy, r + 0.5 * stroke_width, 16);
101        let inner_r = r - 0.5 * stroke_width;
102        if inner_r > 1e-6 {
103            outer.difference(&Self::disk(cx, cy, inner_r, 16))
104        } else {
105            outer
106        }
107    }
108
109    fn overlay_with(&self, other: &Self, rule: OverlayRule) -> Self {
110        Self::wrap(
111            self.shapes
112                .overlay(&other.shapes, rule, FillRule::NonZero),
113        )
114    }
115
116    pub fn union(&self, other: &Self) -> Self {
117        if self.is_empty() {
118            return other.clone();
119        }
120        if other.is_empty() {
121            return self.clone();
122        }
123        self.overlay_with(other, OverlayRule::Union)
124    }
125
126    pub fn difference(&self, other: &Self) -> Self {
127        if self.is_empty() {
128            return Self::empty();
129        }
130        if other.is_empty() {
131            return self.clone();
132        }
133        self.overlay_with(other, OverlayRule::Difference)
134    }
135
136    pub fn xor(&self, other: &Self) -> Self {
137        self.overlay_with(other, OverlayRule::Xor)
138    }
139
140    /// Outline offset (`dist` may be negative).
141    pub fn buffer(&self, dist: f64) -> Self {
142        if self.is_empty() || dist.abs() < 1e-15 {
143            return self.clone();
144        }
145        let join = LineJoin::Round((dist.abs() * 0.35).max(0.25));
146        Self::wrap(
147            self.shapes
148                .outline(&OutlineStyle::new(dist).line_join(join)),
149        )
150    }
151
152    /// `i_overlay::SimplifyShape` (collinear cleanup; holes stay valid).
153    pub fn simplify(&self) -> Self {
154        if self.is_empty() {
155            return Self::empty();
156        }
157        Self::wrap(self.shapes.simplify_shape(FillRule::NonZero))
158    }
159
160    pub fn point_count(&self) -> usize {
161        self.shapes.iter().flat_map(|s| s.iter()).map(|c| c.len()).sum()
162    }
163
164    /// White knockout: simplify, then outline offset.
165    pub fn halo(&self, dist: f64) -> Self {
166        if self.is_empty() || dist <= 0.0 {
167            return Self::empty();
168        }
169        self.simplify().buffer(dist)
170    }
171
172    pub fn has_holes(&self) -> bool {
173        self.shapes.iter().any(|s| s.len() > 1)
174    }
175
176    pub fn translate(&self, dx: f64, dy: f64) -> Self {
177        let mut shapes = self.shapes.clone();
178        for p in shapes.iter_mut().flatten().flatten() {
179            p[0] += dx;
180            p[1] += dy;
181        }
182        Self::wrap(shapes)
183    }
184
185    pub fn scale(&self, sx: f64, sy: f64, ox: f64, oy: f64) -> Self {
186        let mut shapes = self.shapes.clone();
187        for p in shapes.iter_mut().flatten().flatten() {
188            p[0] = ox + (p[0] - ox) * sx;
189            p[1] = oy + (p[1] - oy) * sy;
190        }
191        // Negative scale flips winding — lib simplify restores topology.
192        Self::wrap(shapes).simplify()
193    }
194
195    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
196        let mut iter = self.shapes.iter().flatten().flatten();
197        let first = iter.next()?;
198        let (mut minx, mut miny, mut maxx, mut maxy) = (first[0], first[1], first[0], first[1]);
199        for p in iter {
200            minx = minx.min(p[0]);
201            miny = miny.min(p[1]);
202            maxx = maxx.max(p[0]);
203            maxy = maxy.max(p[1]);
204        }
205        Some((minx, miny, maxx, maxy))
206    }
207
208    pub fn area(&self) -> f64 {
209        self.shapes.area().abs()
210    }
211
212    pub fn centroid(&self) -> Option<(f64, f64)> {
213        let (minx, miny, maxx, maxy) = self.bounds()?;
214        Some(((minx + maxx) * 0.5, (miny + maxy) * 0.5))
215    }
216
217    pub fn contains(&self, x: f64, y: f64) -> bool {
218        // Even-odd over all contours in each shape (holes flip).
219        self.shapes.iter().any(|shape| {
220            shape
221                .iter()
222                .filter(|c| c.len() >= 3)
223                .fold(false, |inside, c| inside ^ point_in_ring(x, y, c))
224        })
225    }
226
227    pub fn to_svg_d(&self) -> String {
228        self.shapes
229            .iter()
230            .flatten()
231            .filter(|c| c.len() >= 3)
232            .map(|c| {
233                let ring: Vec<(f64, f64)> = c.iter().copied().map(xy).collect();
234                polygon_to_svg_d(&ring)
235            })
236            .filter(|d| !d.is_empty())
237            .collect::<Vec<_>>()
238            .join(" ")
239    }
240
241    /// Leftmost horizontal chord length at `y` (stem-width helper).
242    pub fn horizontal_span_at(&self, y: f64) -> f64 {
243        let mut xs = Vec::new();
244        for contour in self.shapes.iter().flatten() {
245            let n = contour.len();
246            for i in 0..n {
247                let a = contour[i];
248                let b = contour[(i + 1) % n];
249                if (a[1] - y) * (b[1] - y) > 0.0 || (a[1] - b[1]).abs() < 1e-15 {
250                    continue;
251                }
252                let t = (y - a[1]) / (b[1] - a[1]);
253                if (0.0..=1.0).contains(&t) {
254                    xs.push(a[0] + t * (b[0] - a[0]));
255                }
256            }
257        }
258        if xs.len() < 2 {
259            return 0.0;
260        }
261        xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
262        xs.dedup_by(|a, b| (*a - *b).abs() <= 1e-9);
263        if xs.len() < 2 {
264            0.0
265        } else {
266            xs[1] - xs[0]
267        }
268    }
269
270    #[allow(clippy::type_complexity)]
271    pub fn polygons_rings(&self) -> Vec<(Vec<(f64, f64)>, Vec<Vec<(f64, f64)>>)> {
272        self.shapes
273            .iter()
274            .filter(|s| !s.is_empty())
275            .map(|shape| {
276                let exterior: Vec<(f64, f64)> = shape[0].iter().copied().map(xy).collect();
277                let holes: Vec<Vec<(f64, f64)>> = shape[1..]
278                    .iter()
279                    .map(|c| c.iter().copied().map(xy).collect())
280                    .collect();
281                (exterior, holes)
282            })
283            .collect()
284    }
285}
286
287fn point_in_ring(x: f64, y: f64, ring: &Contour) -> bool {
288    let n = ring.len();
289    let mut inside = false;
290    let mut j = n - 1;
291    for i in 0..n {
292        let (xi, yi) = (ring[i][0], ring[i][1]);
293        let (xj, yj) = (ring[j][0], ring[j][1]);
294        if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi + f64::EPSILON) + xi) {
295            inside = !inside;
296        }
297        j = i;
298    }
299    inside
300}