1use std::collections::BTreeMap;
4use std::mem::size_of;
5
6use super::{Path2D, PathVerb};
7use crate::budget::Budget;
8use crate::{FillRule, PixelSize, Point, RasterLimits, ValidationError};
9
10#[cfg(test)]
11mod tests;
12
13pub const MAX_BOOLEAN_EDGES: usize = 8_192;
15
16const SNAP: f64 = 1e-12;
18
19const PROBE: f64 = 1e-9;
21
22const LABEL: &str = "path_boolean";
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum PathBooleanOp {
27 Union,
29 Intersection,
31 Difference,
33 Xor,
35}
36
37impl PathBooleanOp {
38 fn apply(self, first: bool, second: bool) -> bool {
39 match self {
40 Self::Union => first || second,
41 Self::Intersection => first && second,
42 Self::Difference => first && !second,
43 Self::Xor => first != second,
44 }
45 }
46}
47
48#[derive(Clone, Copy, Debug)]
50pub struct PathBooleanOptions {
51 pub fill_rule: FillRule,
53 pub tolerance: f64,
55 pub limits: RasterLimits,
57}
58
59impl Default for PathBooleanOptions {
60 fn default() -> Self {
61 Self {
62 fill_rule: FillRule::NonZero,
63 tolerance: 0.05,
64 limits: RasterLimits::default(),
65 }
66 }
67}
68
69impl Path2D {
70 pub fn boolean(
93 &self,
94 other: &Self,
95 op: PathBooleanOp,
96 options: PathBooleanOptions,
97 ) -> Result<Self, ValidationError> {
98 if !options.tolerance.is_finite() || options.tolerance <= 0.0 {
99 return Err(ValidationError::invalid(
100 LABEL,
101 "tolerance",
102 "Tolerance must be finite and positive",
103 ));
104 }
105 let mut budget = Budget::new(PixelSize::new(1, 1), options.limits)?;
106 let mut first = Operand::flatten(self, options.tolerance, &mut budget)?;
107 let second = Operand::flatten(other, options.tolerance, &mut budget)?;
108 first.segments.extend_from_slice(&second.segments);
109 if first.segments.len() > MAX_BOOLEAN_EDGES {
110 return Err(ValidationError::limit(
111 LABEL,
112 "edges",
113 "Flattened operands exceed the boolean edge limit",
114 ));
115 }
116 let segments = first.segments;
117 let contours = [first.contours, second.contours];
118 let pieces = split_segments(&segments, &mut budget)?;
119 let edges = unique_edges(pieces, &mut budget)?;
120 let scale = extent(&segments);
121 let probe = (scale * PROBE).max(f64::MIN_POSITIVE);
122 let work = (edges.len() as u64).saturating_mul(segments.len() as u64 * 2);
123 budget.work(work, LABEL)?;
124 let mut kept = Vec::new();
125 for (start, end) in edges {
126 let middle = Point::new((start.x + end.x) * 0.5, (start.y + end.y) * 0.5);
127 let (dx, dy) = (end.x - start.x, end.y - start.y);
128 let length = libm::hypot(dx, dy);
129 let normal = (-dy / length * probe, dx / length * probe);
130 let side = |sign: f64| {
131 let point = Point::new(middle.x + sign * normal.0, middle.y + sign * normal.1);
132 op.apply(
133 contains(&contours[0], point, options.fill_rule),
134 contains(&contours[1], point, options.fill_rule),
135 )
136 };
137 let (left, right) = (side(1.0), side(-1.0));
138 if left != right {
139 let edge = if left { (start, end) } else { (end, start) };
140 budget.push(&mut kept, edge, LABEL)?;
141 }
142 }
143 build_path(&kept, &mut budget)
144 }
145}
146
147struct Operand {
148 segments: Vec<(Point, Point)>,
149 contours: Vec<Vec<Point>>,
150}
151
152impl Operand {
153 fn flatten(
154 path: &Path2D,
155 tolerance: f64,
156 budget: &mut Budget,
157 ) -> Result<Self, ValidationError> {
158 let mut bez = kurbo::BezPath::new();
159 for verb in path.verbs() {
160 match *verb {
161 PathVerb::MoveTo(point) => bez.move_to((point.x, point.y)),
162 PathVerb::LineTo(point) => bez.line_to((point.x, point.y)),
163 PathVerb::QuadraticTo(control, end) => {
164 bez.quad_to((control.x, control.y), (end.x, end.y))
165 }
166 PathVerb::CubicTo(first, second, end) => {
167 bez.curve_to((first.x, first.y), (second.x, second.y), (end.x, end.y))
168 }
169 PathVerb::Close => bez.close_path(),
170 }
171 }
172 let mut contours: Vec<Vec<Point>> = Vec::new();
173 let mut count = 0usize;
174 kurbo::flatten(bez, tolerance, |element| {
175 count += 1;
176 if count > MAX_BOOLEAN_EDGES * 2 {
177 return;
178 }
179 match element {
180 kurbo::PathEl::MoveTo(point) => contours.push(vec![Point::new(point.x, point.y)]),
181 kurbo::PathEl::LineTo(point) => {
182 if let Some(contour) = contours.last_mut() {
183 contour.push(Point::new(point.x, point.y));
184 }
185 }
186 _ => {}
187 }
188 });
189 if count > MAX_BOOLEAN_EDGES * 2 {
190 return Err(ValidationError::limit(
191 LABEL,
192 "edges",
193 "Flattened operand exceeds the boolean edge limit",
194 ));
195 }
196 let mut segments = Vec::new();
197 contours.retain_mut(|contour| {
198 contour.dedup();
199 while contour.len() > 1 && contour.first() == contour.last() {
200 contour.pop();
201 }
202 contour.len() >= 3
203 });
204 for contour in &contours {
205 for (index, start) in contour.iter().enumerate() {
206 let end = contour[(index + 1) % contour.len()];
207 budget.push(&mut segments, (*start, end), LABEL)?;
208 }
209 }
210 Ok(Self { segments, contours })
211 }
212}
213
214fn extent(segments: &[(Point, Point)]) -> f64 {
215 segments
216 .iter()
217 .flat_map(|(start, end)| [start.x, start.y, end.x, end.y])
218 .fold(0.0f64, |scale, value| scale.max(value.abs()))
219 .max(1e-6)
220}
221
222fn split_segments(
224 segments: &[(Point, Point)],
225 budget: &mut Budget,
226) -> Result<Vec<Vec<Point>>, ValidationError> {
227 let count = segments.len() as u64;
228 budget.work(count.saturating_mul(count), LABEL)?;
229 budget.bytes(count.saturating_mul(size_of::<Vec<Point>>() as u64), LABEL)?;
230 let mut splits: Vec<Vec<Point>> = vec![Vec::new(); segments.len()];
231 for first in 0..segments.len() {
232 for second in first + 1..segments.len() {
233 let points = intersect(segments[first], segments[second]);
234 for (point, on_first, on_second) in points.into_iter().flatten() {
235 if on_first {
236 budget.push(&mut splits[first], point, LABEL)?;
237 }
238 if on_second {
239 budget.push(&mut splits[second], point, LABEL)?;
240 }
241 }
242 }
243 }
244 let mut pieces = Vec::with_capacity(segments.len());
245 for (&(start, end), mut points) in segments.iter().zip(splits) {
246 let (dx, dy) = (end.x - start.x, end.y - start.y);
247 let along = |point: &Point| (point.x - start.x) * dx + (point.y - start.y) * dy;
248 points.retain(|point| *point != start && *point != end);
249 points.sort_by(|left, right| along(left).total_cmp(&along(right)));
250 points.dedup();
251 let mut chain = Vec::with_capacity(points.len() + 2);
252 chain.push(start);
253 chain.extend(points);
254 chain.push(end);
255 pieces.push(chain);
256 }
257 Ok(pieces)
258}
259
260fn intersect(first: (Point, Point), second: (Point, Point)) -> [Option<(Point, bool, bool)>; 4] {
262 let mut found = [None; 4];
263 let (p, q) = (first.0, second.0);
264 let r = (first.1.x - p.x, first.1.y - p.y);
265 let s = (second.1.x - q.x, second.1.y - q.y);
266 let reach = [
267 (first.0.x.min(first.1.x), first.0.x.max(first.1.x)),
268 (second.0.x.min(second.1.x), second.0.x.max(second.1.x)),
269 (first.0.y.min(first.1.y), first.0.y.max(first.1.y)),
270 (second.0.y.min(second.1.y), second.0.y.max(second.1.y)),
271 ];
272 if reach[0].1 < reach[1].0
273 || reach[1].1 < reach[0].0
274 || reach[2].1 < reach[3].0
275 || reach[3].1 < reach[2].0
276 {
277 return found;
278 }
279 let denominator = r.0 * s.1 - r.1 * s.0;
280 let scale = libm::hypot(r.0, r.1) * libm::hypot(s.0, s.1);
281 let gap = (q.x - p.x, q.y - p.y);
282 if denominator.abs() > SNAP * scale {
283 let t = (gap.0 * s.1 - gap.1 * s.0) / denominator;
284 let u = (gap.0 * r.1 - gap.1 * r.0) / denominator;
285 if (-SNAP..=1.0 + SNAP).contains(&t) && (-SNAP..=1.0 + SNAP).contains(&u) {
286 let point = if t <= SNAP {
287 first.0
288 } else if t >= 1.0 - SNAP {
289 first.1
290 } else if u <= SNAP {
291 second.0
292 } else if u >= 1.0 - SNAP {
293 second.1
294 } else {
295 Point::new(p.x + t * r.0, p.y + t * r.1)
296 };
297 found[0] = Some((point, true, true));
298 }
299 return found;
300 }
301 for (slot, (point, on_first)) in [
302 (second.0, true),
303 (second.1, true),
304 (first.0, false),
305 (first.1, false),
306 ]
307 .into_iter()
308 .enumerate()
309 {
310 let (base, direction) = if on_first { (p, r) } else { (q, s) };
311 let length = libm::hypot(direction.0, direction.1);
312 let offset = (point.x - base.x, point.y - base.y);
313 let distance = (offset.0 * direction.1 - offset.1 * direction.0).abs() / length;
314 let along = (offset.0 * direction.0 + offset.1 * direction.1) / (length * length);
315 if distance <= SNAP * length.max(1.0) && along > 0.0 && along < 1.0 {
316 found[slot] = Some((point, on_first, !on_first));
317 }
318 }
319 found
320}
321
322fn unique_edges(
324 pieces: Vec<Vec<Point>>,
325 budget: &mut Budget,
326) -> Result<Vec<(Point, Point)>, ValidationError> {
327 let mut keyed: Vec<([u64; 4], (Point, Point))> = Vec::new();
328 for chain in pieces {
329 for pair in chain.windows(2) {
330 let (start, end) = (pair[0], pair[1]);
331 if start == end {
332 continue;
333 }
334 let (a, b) = (bits(start), bits(end));
335 let key = if a <= b {
336 [a[0], a[1], b[0], b[1]]
337 } else {
338 [b[0], b[1], a[0], a[1]]
339 };
340 budget.push(&mut keyed, (key, (start, end)), LABEL)?;
341 }
342 }
343 keyed.sort_by_key(|entry| entry.0);
344 keyed.dedup_by_key(|entry| entry.0);
345 Ok(keyed.into_iter().map(|entry| entry.1).collect())
346}
347
348fn bits(point: Point) -> [u64; 2] {
349 [(point.x + 0.0).to_bits(), (point.y + 0.0).to_bits()]
350}
351
352fn contains(contours: &[Vec<Point>], point: Point, rule: FillRule) -> bool {
353 let mut winding = 0i32;
354 for contour in contours {
355 for (index, start) in contour.iter().enumerate() {
356 let end = contour[(index + 1) % contour.len()];
357 if (start.y <= point.y) == (end.y <= point.y) {
358 continue;
359 }
360 let t = (point.y - start.y) / (end.y - start.y);
361 if start.x + t * (end.x - start.x) > point.x {
362 winding += if end.y > start.y { 1 } else { -1 };
363 }
364 }
365 }
366 match rule {
367 FillRule::NonZero => winding != 0,
368 FillRule::EvenOdd => winding % 2 != 0,
369 }
370}
371
372fn build_path(edges: &[(Point, Point)], budget: &mut Budget) -> Result<Path2D, ValidationError> {
374 let mut outgoing: BTreeMap<[u64; 2], Vec<usize>> = BTreeMap::new();
375 for (index, (start, _)) in edges.iter().enumerate() {
376 outgoing.entry(bits(*start)).or_default().push(index);
377 }
378 let mut used = vec![false; edges.len()];
379 let mut path = Path2D::new();
380 for first in 0..edges.len() {
381 if used[first] {
382 continue;
383 }
384 used[first] = true;
385 budget.work(1, LABEL)?;
386 let origin = edges[first].0;
387 let mut ring = vec![origin];
388 let mut current = edges[first].1;
389 while current != origin {
390 ring.push(current);
391 budget.work(1, LABEL)?;
392 let next = outgoing
393 .get(&bits(current))
394 .and_then(|list| list.iter().copied().find(|index| !used[*index]));
395 let Some(next) = next else { break };
396 used[next] = true;
397 current = edges[next].1;
398 }
399 let ring = simplify(ring);
400 if ring.len() < 3 {
401 continue;
402 }
403 path.try_move_to(ring[0])?;
404 for point in &ring[1..] {
405 path.try_line_to(*point)?;
406 }
407 path.try_close()?;
408 }
409 Ok(path)
410}
411
412fn simplify(ring: Vec<Point>) -> Vec<Point> {
413 let mut points = ring;
414 let mut index = 0;
415 while points.len() >= 3 && index < points.len() {
416 let count = points.len();
417 let (before, here, after) = (
418 points[(index + count - 1) % count],
419 points[index],
420 points[(index + 1) % count],
421 );
422 let a = (here.x - before.x, here.y - before.y);
423 let b = (after.x - here.x, after.y - here.y);
424 let cross = a.0 * b.1 - a.1 * b.0;
425 let scale = libm::hypot(a.0, a.1) * libm::hypot(b.0, b.1);
426 if cross.abs() <= SNAP * scale && a.0 * b.0 + a.1 * b.1 > 0.0 {
427 points.remove(index);
428 } else {
429 index += 1;
430 }
431 }
432 points
433}