Skip to content

Commit a4ec50d

Browse files
TrueDoctorKeavon
andauthored
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>
1 parent e4dd3ce commit a4ec50d

11 files changed

Lines changed: 591 additions & 373 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libraries/path-bool/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ glam = "0.29.0"
2828
regex = "1.10.6"
2929
slotmap = "1.0.7"
3030
lyon_geom = "1.0"
31+
roots = "0.0.8"
32+
rustc-hash = "2.0.0"
33+
smallvec = "1.13.2"
3134

3235
[dev-dependencies]
3336
glob = "0.3"

libraries/path-bool/src/lib.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ mod parsing {
99
mod util {
1010
pub(crate) mod aabb;
1111
pub(crate) mod epsilons;
12+
pub(crate) mod grid;
1213
pub(crate) mod math;
13-
pub(crate) mod quad_tree;
1414
}
1515
mod path;
1616
#[cfg(test)]
@@ -311,4 +311,23 @@ mod test {
311311
assert_eq!(result.len(), 1);
312312
assert!(!result[0].is_empty());
313313
}
314+
#[test]
315+
fn real_01() {
316+
let a = path_from_path_data(
317+
"M 212.67152,105 A 64.171516,64.171516 0 0 1 148.5,169.17152 64.171516,64.171516 0 0 1 84.328484,105 64.171516,64.171516 0 0 1 148.5,40.828484 64.171516,64.171516 0 0 1 212.67152,105 Z",
318+
)
319+
.unwrap();
320+
let b = path_from_path_data(
321+
"m 83.755387,112.6962 h -7.62 v 37.0332 h 7.62 z m 22.097973,9.144 c -3.4544,0 -5.892801,1.4732 -7.620001,4.5212 v -4.064 H 91.12136 v 38.5064 h 7.111999 v -14.3256 c 1.7272,3.048 4.165601,4.4704 7.620001,4.4704 6.604,0 11.4808,-6.1976 11.4808,-14.5288 0,-8.0772 -4.3688,-14.5796 -11.4808,-14.5796 z m -1.6256,5.9436 c 3.6068,0 5.9944,3.5052 5.9944,8.7376 0,4.9784 -2.4892,8.4836 -5.9944,8.4836 -3.556,0 -5.994401,-3.4544 -5.994401,-8.5852 0,-5.1308 2.438401,-8.636 5.994401,-8.636 z m 23.62201,13.97 h -6.9596 c 0.2032,5.9944 4.6228,9.144 12.954,9.144 9.6012,0 11.9888,-5.4864 11.9888,-9.2964 0,-3.556 -1.778,-5.842 -5.3848,-6.9088 l -8.9916,-2.5908 c -1.9812,-0.6096 -2.4892,-1.016 -2.4892,-2.1336 0,-1.524 1.6256,-2.54 4.1148,-2.54 3.4036,0 5.08,1.2192 5.1308,3.7084 h 6.858 c -0.1016,-5.7912 -4.572,-9.2964 -11.938,-9.2964 -6.9596,0 -11.2776,3.5052 -11.2776,9.144 0,5.3848 4.4196,6.2484 5.9944,6.7564 l 8.4836,2.6416 c 1.778,0.5588 2.3876,1.1176 2.3876,2.2352 0,1.6764 -1.9812,2.6924 -5.2832,2.6924 -4.4704,0 -5.1816,-1.6764 -5.588,-3.556 z m 47.59959,7.9756 v -27.432 h -7.112 v 17.1704 c 0,3.2512 -2.286,5.3848 -5.7404,5.3848 -3.048,0 -4.572,-1.6256 -4.572,-4.9276 v -17.6276 h -7.112 v 19.1008 c 0,6.0452 3.3528,9.4996 9.1948,9.4996 3.7084,0 6.1976,-1.3716 8.2296,-4.4196 v 3.2512 z m 6.60404,-27.432 v 27.432 h 7.112 v -16.4592 c 0,-3.3528 1.8288,-5.3848 4.8768,-5.3848 2.3876,0 3.8608,1.3716 3.8608,3.556 v 18.288 h 7.112 v -16.4592 c 0,-3.3528 1.8288,-5.3848 4.8768,-5.3848 2.3876,0 3.8608,1.3716 3.8608,3.556 v 18.288 h 7.112 v -19.4056 c 0,-5.334 -3.2512,-8.4836 -8.7376,-8.4836 -3.4544,0 -5.8928,1.2192 -8.0264,4.064 -1.3208,-2.5908 -4.064,-4.064 -7.4676,-4.064 -3.1496,0 -5.1816,1.0668 -7.5184,3.8608 v -3.4036 z M 81.012192,49.196201 h -7.62 v 37.0332 h 25.3492 v -6.35 h -17.7292 z m 35.305978,9.144 c -8.382,0 -13.5128,5.5372 -13.5128,14.5288 0,9.0424 5.1308,14.5288 13.5636,14.5288 8.3312,0 13.5636,-5.5372 13.5636,-14.3256 0,-9.2964 -5.0292,-14.732 -13.6144,-14.732 z m 0.0508,5.7404 c 3.9116,0 6.4516,3.5052 6.4516,8.89 0,5.1308 -2.6416,8.6868 -6.4516,8.6868 -3.8608,0 -6.4516,-3.556 -6.4516,-8.7884 0,-5.2324 2.5908,-8.7884 6.4516,-8.7884 z m 18.89761,-5.2832 v 27.432 h 7.112 v -14.5796 c 0,-4.1656 2.0828,-6.2484 6.2484,-6.2484 0.762,0 1.27,0.0508 2.2352,0.2032 v -7.2136 c -0.4064,-0.0508 -0.6604,-0.0508 -0.8636,-0.0508 -3.2512,0 -6.096,2.1336 -7.62,5.842 v -5.3848 z m 31.34362,-0.4572 c -7.874,0 -12.7,5.6896 -12.7,14.8844 0,8.7884 4.7752,14.1732 12.5476,14.1732 6.14679,0 11.12519,-3.5052 12.69999,-8.89 h -7.0104 c -0.8636,2.1844 -2.8448,3.4544 -5.43559,3.4544 -4.7752,0 -5.5372,-3.5052 -5.6896,-7.2136 h 18.38959 c 0.0508,-0.6096 0.0508,-0.8636 0.0508,-1.2192 0,-12.1412 -7.366,-15.1892 -12.85239,-15.1892 z m 5.43559,11.684 H 161.1238 c 0.4572,-4.1656 2.2352,-6.2484 5.3848,-6.2484 3.30199,0 5.23239,2.2352 5.53719,6.2484 z m 12.75081,-11.2268 v 27.432 h 7.112 v -16.4592 c 0,-3.3528 1.8288,-5.3848 4.8768,-5.3848 2.3876,0 3.8608,1.3716 3.8608,3.556 v 18.288 h 7.112 v -16.4592 c 0,-3.3528 1.8288,-5.3848 4.8768,-5.3848 2.3876,0 3.8608,1.3716 3.8608,3.556 v 18.288 h 7.112 v -19.4056 c 0,-5.334 -3.2512,-8.4836 -8.7376,-8.4836 -3.4544,0 -5.8928,1.2192 -8.0264,4.064 -1.3208,-2.5908 -4.064,-4.064 -7.4676,-4.064 -3.1496,0 -5.1816,1.0668 -7.5184,3.8608 v -3.4036 z",
322+
)
323+
.unwrap();
324+
325+
let result = path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, PathBooleanOperation::Union).unwrap();
326+
327+
// Add assertions here based on expected results
328+
assert_eq!(result.len(), 1, "Expected 1 resulting path for Union operation");
329+
dbg!(path_to_path_data(&result[0], 0.001));
330+
// Add more specific assertions about the resulting path if needed
331+
assert!(!result[0].is_empty());
332+
}
314333
}

libraries/path-bool/src/path/intersection_path_segment.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ fn subdivide_intersection_segment(int_seg: &IntersectionSegment) -> [Intersectio
3434
seg: seg0,
3535
start_param: int_seg.start_param,
3636
end_param: mid_param,
37-
bounding_box: seg0.bounding_box(),
37+
bounding_box: seg0.approx_bounding_box(),
3838
},
3939
IntersectionSegment {
4040
seg: seg1,
4141
start_param: mid_param,
4242
end_param: int_seg.end_param,
43-
bounding_box: seg1.bounding_box(),
43+
bounding_box: seg1.approx_bounding_box(),
4444
},
4545
]
4646
}
@@ -116,22 +116,23 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
116116
return intersections;
117117
}
118118
_ => (),
119-
}
119+
};
120120

121+
// Fallback for quadratics and arc segments
121122
// https://math.stackexchange.com/questions/20321/how-can-i-tell-when-two-cubic-b%C3%A9zier-curves-intersect
122123

123124
let mut pairs = vec![(
124125
IntersectionSegment {
125126
seg: *seg0,
126127
start_param: 0.,
127128
end_param: 1.,
128-
bounding_box: seg0.bounding_box(),
129+
bounding_box: seg0.approx_bounding_box(),
129130
},
130131
IntersectionSegment {
131132
seg: *seg1,
132133
start_param: 0.,
133134
end_param: 1.,
134-
bounding_box: seg1.bounding_box(),
135+
bounding_box: seg1.approx_bounding_box(),
135136
},
136137
)];
137138
let mut next_pairs = Vec::new();
@@ -145,7 +146,7 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
145146
while !pairs.is_empty() {
146147
next_pairs.clear();
147148

148-
if pairs.len() > 1000 {
149+
if pairs.len() > 256 {
149150
return calculate_overlap_intersections(seg0, seg1, eps);
150151
}
151152

@@ -191,10 +192,6 @@ pub fn path_segment_intersection(seg0: &PathSegment, seg1: &PathSegment, endpoin
191192
std::mem::swap(&mut pairs, &mut next_pairs);
192193
}
193194

194-
if !endpoints {
195-
params.retain(|[s, t]| (s > &eps.param && s < &(1. - eps.param)) || (t > &eps.param && t < &(1. - eps.param)));
196-
}
197-
198195
params
199196
}
200197

libraries/path-bool/src/path/line_segment_aabb.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ const TOP: u8 = 1 << 3;
1010
fn out_code(x: f64, y: f64, bounding_box: &Aabb) -> u8 {
1111
let mut code = INSIDE;
1212

13-
if x < bounding_box.left {
13+
if x < bounding_box.left() {
1414
code |= LEFT;
15-
} else if x > bounding_box.right {
15+
} else if x > bounding_box.right() {
1616
code |= RIGHT;
1717
}
1818

19-
if y < bounding_box.top {
19+
if y < bounding_box.top() {
2020
code |= BOTTOM;
21-
} else if y > bounding_box.bottom {
21+
} else if y > bounding_box.bottom() {
2222
code |= TOP;
2323
}
2424

@@ -57,20 +57,20 @@ pub(crate) fn line_segment_aabb_intersect(seg: LineSegment, bounding_box: &Aabb)
5757
// outcode bit being tested guarantees the denominator is non-zero
5858
if (outcode_out & TOP) != 0 {
5959
// point is above the clip window
60-
x = p0.x + (p1.x - p0.x) * (bounding_box.bottom - p0.y) / (p1.y - p0.y);
61-
y = bounding_box.bottom;
60+
x = p0.x + (p1.x - p0.x) * (bounding_box.bottom() - p0.y) / (p1.y - p0.y);
61+
y = bounding_box.bottom();
6262
} else if (outcode_out & BOTTOM) != 0 {
6363
// point is below the clip window
64-
x = p0.x + (p1.x - p0.x) * (bounding_box.top - p0.y) / (p1.y - p0.y);
65-
y = bounding_box.top;
64+
x = p0.x + (p1.x - p0.x) * (bounding_box.top() - p0.y) / (p1.y - p0.y);
65+
y = bounding_box.top();
6666
} else if (outcode_out & RIGHT) != 0 {
6767
// point is to the right of clip window
68-
y = p0.y + (p1.y - p0.y) * (bounding_box.right - p0.x) / (p1.x - p0.x);
69-
x = bounding_box.right;
68+
y = p0.y + (p1.y - p0.y) * (bounding_box.right() - p0.x) / (p1.x - p0.x);
69+
x = bounding_box.right();
7070
} else if (outcode_out & LEFT) != 0 {
7171
// point is to the left of clip window
72-
y = p0.y + (p1.y - p0.y) * (bounding_box.left - p0.x) / (p1.x - p0.x);
73-
x = bounding_box.left;
72+
y = p0.y + (p1.y - p0.y) * (bounding_box.left() - p0.x) / (p1.x - p0.x);
73+
x = bounding_box.left();
7474
}
7575

7676
// Now we move outside point to intersection point to clip

libraries/path-bool/src/path/path_segment.rs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -588,21 +588,16 @@ impl PathSegment {
588588
/// An [`Aabb`] representing the axis-aligned bounding box of the segment.
589589
pub(crate) fn bounding_box(&self) -> Aabb {
590590
match *self {
591-
PathSegment::Line(start, end) => Aabb {
592-
top: start.y.min(end.y),
593-
right: start.x.max(end.x),
594-
bottom: start.y.max(end.y),
595-
left: start.x.min(end.x),
596-
},
591+
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)),
597592
PathSegment::Cubic(p1, p2, p3, p4) => {
598593
let (left, right) = cubic_bounding_interval(p1.x, p2.x, p3.x, p4.x);
599594
let (top, bottom) = cubic_bounding_interval(p1.y, p2.y, p3.y, p4.y);
600-
Aabb { top, right, bottom, left }
595+
Aabb::new(left, top, right, bottom)
601596
}
602597
PathSegment::Quadratic(p1, p2, p3) => {
603598
let (left, right) = quadratic_bounding_interval(p1.x, p2.x, p3.x);
604599
let (top, bottom) = quadratic_bounding_interval(p1.y, p2.y, p3.y);
605-
Aabb { top, right, bottom, left }
600+
Aabb::new(left, top, right, bottom)
606601
}
607602
PathSegment::Arc(start, rx, ry, phi, _, _, end) => {
608603
if let Some(center_param) = self.arc_segment_to_center() {
@@ -627,11 +622,11 @@ impl PathSegment {
627622
} else {
628623
// TODO: Don't convert to cubics
629624
let cubics = self.arc_segment_to_cubics(PI / 16.);
630-
let mut bounding_box = None;
625+
let mut bounding_box = bounding_box_around_point(start, 0.);
631626
for cubic_seg in cubics {
632-
bounding_box = Some(merge_bounding_boxes(bounding_box, &cubic_seg.bounding_box()));
627+
bounding_box = merge_bounding_boxes(&bounding_box, &cubic_seg.bounding_box());
633628
}
634-
bounding_box.unwrap_or_else(|| bounding_box_around_point(start, 0.))
629+
bounding_box
635630
}
636631
} else {
637632
extend_bounding_box(Some(bounding_box_around_point(start, 0.)), end)
@@ -640,6 +635,30 @@ impl PathSegment {
640635
}
641636
}
642637

638+
/// Computes a loose bounding box that surrounds all anchors, but also the handles of cubic and quadratic segments.
639+
/// 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.
640+
pub(crate) fn approx_bounding_box(&self) -> Aabb {
641+
match *self {
642+
PathSegment::Cubic(p1, p2, p3, p4) => {
643+
// Use the control points to create a bounding box
644+
let left = p1.x.min(p2.x).min(p3.x).min(p4.x);
645+
let right = p1.x.max(p2.x).max(p3.x).max(p4.x);
646+
let top = p1.y.min(p2.y).min(p3.y).min(p4.y);
647+
let bottom = p1.y.max(p2.y).max(p3.y).max(p4.y);
648+
Aabb::new(left, top, right, bottom)
649+
}
650+
PathSegment::Quadratic(p1, p2, p3) => {
651+
// Use the control points to create a bounding box
652+
let left = p1.x.min(p2.x).min(p3.x);
653+
let right = p1.x.max(p2.x).max(p3.x);
654+
let top = p1.y.min(p2.y).min(p3.y);
655+
let bottom = p1.y.max(p2.y).max(p3.y);
656+
Aabb::new(left, top, right, bottom)
657+
}
658+
seg => seg.bounding_box(),
659+
}
660+
}
661+
643662
/// Splits the path segment at a given parameter value.
644663
///
645664
/// # Arguments

0 commit comments

Comments
 (0)