Improve robustness and performance of the boolean operation algorithm (#2191)

* Improve perf of path bool lib

* Use swap remove

* Use outer/inner bounding box for inclusion testing

* Reuse allocations for hit testing

* Use direct root finding for inclusion testing

* Reuse bounding box

* Use faster hash and specify capacities

* Use hashmap based approach for find vertices

* Unroll find_vertecies loop and use 32 bit positions

* Tune initial vec capacities

* Remove unused bounding boxes

* Use smallvec for storing outgoing edges

* Improve allocations for compute_minor

* Use approximate bounding box for edge finding

* Transition aabb to use glam vecs

* Make find vertecies use 64 bit again this is slower but less likely to cause issues

* Improve intersection candidate finding

* Remove loop check in bit vec iter

* Special case cubic line intersections

* Optimize grid rounding and add debug output

* Remove file write

* Remove faulty line intersection

* Fix grid rounding

* Improve robustness and cleanaup code

* Make elided lifetime explicit

* Fix tests

* Fix a boolean ops crash

* Add comment

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2025-08-22 01:15:36 +02:00
committed by GitHub
parent e4dd3ce806
commit a4ec50d8ba
11 changed files with 596 additions and 378 deletions

View File

@@ -34,13 +34,13 @@ fn subdivide_intersection_segment(int_seg: &IntersectionSegment) -> [Intersectio
seg: seg0,
start_param: int_seg.start_param,
end_param: mid_param,
bounding_box: seg0.bounding_box(),
bounding_box: seg0.approx_bounding_box(),
},
IntersectionSegment {
seg: seg1,
start_param: mid_param,
end_param: int_seg.end_param,
bounding_box: seg1.bounding_box(),
bounding_box: seg1.approx_bounding_box(),
},
]
}
@@ -116,8 +116,9 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
return intersections;
}
_ => (),
}
};
// Fallback for quadratics and arc segments
// https://math.stackexchange.com/questions/20321/how-can-i-tell-when-two-cubic-b%C3%A9zier-curves-intersect
let mut pairs = vec![(
@@ -125,13 +126,13 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
seg: *seg0,
start_param: 0.,
end_param: 1.,
bounding_box: seg0.bounding_box(),
bounding_box: seg0.approx_bounding_box(),
},
IntersectionSegment {
seg: *seg1,
start_param: 0.,
end_param: 1.,
bounding_box: seg1.bounding_box(),
bounding_box: seg1.approx_bounding_box(),
},
)];
let mut next_pairs = Vec::new();
@@ -145,7 +146,7 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
while !pairs.is_empty() {
next_pairs.clear();
if pairs.len() > 1000 {
if pairs.len() > 256 {
return calculate_overlap_intersections(seg0, seg1, eps);
}
@@ -191,10 +192,6 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
std::mem::swap(&mut pairs, &mut next_pairs);
}
if !endpoints {
params.retain(|[s, t]| (s > &eps.param && s < &(1. - eps.param)) || (t > &eps.param && t < &(1. - eps.param)));
}
params
}

View File

@@ -10,15 +10,15 @@ const TOP: u8 = 1 << 3;
fn out_code(x: f64, y: f64, bounding_box: &Aabb) -> u8 {
let mut code = INSIDE;
if x < bounding_box.left {
if x < bounding_box.left() {
code |= LEFT;
} else if x > bounding_box.right {
} else if x > bounding_box.right() {
code |= RIGHT;
}
if y < bounding_box.top {
if y < bounding_box.top() {
code |= BOTTOM;
} else if y > bounding_box.bottom {
} else if y > bounding_box.bottom() {
code |= TOP;
}
@@ -57,20 +57,20 @@ pub(crate) fn line_segment_aabb_intersect(seg: LineSegment, bounding_box: &Aabb)
// outcode bit being tested guarantees the denominator is non-zero
if (outcode_out & TOP) != 0 {
// point is above the clip window
x = p0.x + (p1.x - p0.x) * (bounding_box.bottom - p0.y) / (p1.y - p0.y);
y = bounding_box.bottom;
x = p0.x + (p1.x - p0.x) * (bounding_box.bottom() - p0.y) / (p1.y - p0.y);
y = bounding_box.bottom();
} else if (outcode_out & BOTTOM) != 0 {
// point is below the clip window
x = p0.x + (p1.x - p0.x) * (bounding_box.top - p0.y) / (p1.y - p0.y);
y = bounding_box.top;
x = p0.x + (p1.x - p0.x) * (bounding_box.top() - p0.y) / (p1.y - p0.y);
y = bounding_box.top();
} else if (outcode_out & RIGHT) != 0 {
// point is to the right of clip window
y = p0.y + (p1.y - p0.y) * (bounding_box.right - p0.x) / (p1.x - p0.x);
x = bounding_box.right;
y = p0.y + (p1.y - p0.y) * (bounding_box.right() - p0.x) / (p1.x - p0.x);
x = bounding_box.right();
} else if (outcode_out & LEFT) != 0 {
// point is to the left of clip window
y = p0.y + (p1.y - p0.y) * (bounding_box.left - p0.x) / (p1.x - p0.x);
x = bounding_box.left;
y = p0.y + (p1.y - p0.y) * (bounding_box.left() - p0.x) / (p1.x - p0.x);
x = bounding_box.left();
}
// Now we move outside point to intersection point to clip

View File

@@ -588,21 +588,16 @@ impl PathSegment {
/// An [`Aabb`] representing the axis-aligned bounding box of the segment.
pub(crate) fn bounding_box(&self) -> Aabb {
match *self {
PathSegment::Line(start, end) => Aabb {
top: start.y.min(end.y),
right: start.x.max(end.x),
bottom: start.y.max(end.y),
left: start.x.min(end.x),
},
PathSegment::Line(start, end) => Aabb::new(start.x.min(end.x), start.y.min(end.y), start.x.max(end.x), start.y.max(end.y)),
PathSegment::Cubic(p1, p2, p3, p4) => {
let (left, right) = cubic_bounding_interval(p1.x, p2.x, p3.x, p4.x);
let (top, bottom) = cubic_bounding_interval(p1.y, p2.y, p3.y, p4.y);
Aabb { top, right, bottom, left }
Aabb::new(left, top, right, bottom)
}
PathSegment::Quadratic(p1, p2, p3) => {
let (left, right) = quadratic_bounding_interval(p1.x, p2.x, p3.x);
let (top, bottom) = quadratic_bounding_interval(p1.y, p2.y, p3.y);
Aabb { top, right, bottom, left }
Aabb::new(left, top, right, bottom)
}
PathSegment::Arc(start, rx, ry, phi, _, _, end) => {
if let Some(center_param) = self.arc_segment_to_center() {
@@ -627,11 +622,11 @@ impl PathSegment {
} else {
// TODO: Don't convert to cubics
let cubics = self.arc_segment_to_cubics(PI / 16.);
let mut bounding_box = None;
let mut bounding_box = bounding_box_around_point(start, 0.);
for cubic_seg in cubics {
bounding_box = Some(merge_bounding_boxes(bounding_box, &cubic_seg.bounding_box()));
bounding_box = merge_bounding_boxes(&bounding_box, &cubic_seg.bounding_box());
}
bounding_box.unwrap_or_else(|| bounding_box_around_point(start, 0.))
bounding_box
}
} else {
extend_bounding_box(Some(bounding_box_around_point(start, 0.)), end)
@@ -640,6 +635,30 @@ impl PathSegment {
}
}
/// Computes a loose bounding box that surrounds all anchors, but also the handles of cubic and quadratic segments.
/// This will usually be larger than the actual bounding box, but is faster to compute because it does not have to find where each curve reaches its maximum and minimum.
pub(crate) fn approx_bounding_box(&self) -> Aabb {
match *self {
PathSegment::Cubic(p1, p2, p3, p4) => {
// Use the control points to create a bounding box
let left = p1.x.min(p2.x).min(p3.x).min(p4.x);
let right = p1.x.max(p2.x).max(p3.x).max(p4.x);
let top = p1.y.min(p2.y).min(p3.y).min(p4.y);
let bottom = p1.y.max(p2.y).max(p3.y).max(p4.y);
Aabb::new(left, top, right, bottom)
}
PathSegment::Quadratic(p1, p2, p3) => {
// Use the control points to create a bounding box
let left = p1.x.min(p2.x).min(p3.x);
let right = p1.x.max(p2.x).max(p3.x);
let top = p1.y.min(p2.y).min(p3.y);
let bottom = p1.y.max(p2.y).max(p3.y);
Aabb::new(left, top, right, bottom)
}
seg => seg.bounding_box(),
}
}
/// Splits the path segment at a given parameter value.
///
/// # Arguments