Skip to content

Improve performance of path bool library #2191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a1f3f07
Improve perf of path bool lib
TrueDoctor Sep 24, 2024
2d11355
Use swap remove
TrueDoctor Sep 25, 2024
ddf5f79
Use outer/inner bounding box for inclusion testing
TrueDoctor Sep 25, 2024
cbd54c8
Reuse allocations for hit testing
TrueDoctor Sep 25, 2024
9a568bd
Use direct root finding for inclusion testing
TrueDoctor Sep 25, 2024
577cb23
Reuse bounding box
TrueDoctor Sep 25, 2024
ee8871c
Use faster hash and specify capacities
TrueDoctor Sep 25, 2024
f9e5053
Use hashmap based approach for find vertices
TrueDoctor Sep 26, 2024
b0d7d81
Unroll find_vertecies loop and use 32 bit positions
TrueDoctor Sep 26, 2024
d497dcf
Tune initial vec capacities
TrueDoctor Sep 26, 2024
cb4d19d
Remove unused bounding boxes
TrueDoctor Sep 26, 2024
982473f
Use smallvec for storing outgoing edges
TrueDoctor Sep 26, 2024
76f48a5
Improve allocations for compute_minor
TrueDoctor Sep 26, 2024
716ea50
Use approximate bounding box for edge finding
TrueDoctor Sep 27, 2024
635c913
Transition aabb to use glam vecs
TrueDoctor Sep 27, 2024
e90dc26
Make find vertecies use 64 bit again this is slower but less likely t…
TrueDoctor Sep 27, 2024
247b5dc
Improve intersection candidate finding
TrueDoctor Sep 27, 2024
5653662
Remove loop check in bit vec iter
TrueDoctor Sep 27, 2024
1b91571
Special case cubic line intersections
TrueDoctor Sep 27, 2024
75a40a1
Optimize grid rounding and add debug output
TrueDoctor Sep 30, 2024
89cf008
Remove checks from append_subpath
TrueDoctor Jan 11, 2025
98bdbc2
Remove file write
TrueDoctor Jan 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Transition aabb to use glam vecs
  • Loading branch information
TrueDoctor committed Jan 11, 2025
commit 635c913c02f45621ae9becc696be33f6b798f755
24 changes: 12 additions & 12 deletions libraries/path-bool/src/path/line_segment_aabb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
21 changes: 8 additions & 13 deletions libraries/path-bool/src/path/path_segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,21 +597,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() {
Expand All @@ -636,11 +631,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)
Expand All @@ -656,15 +651,15 @@ impl PathSegment {
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 { top, right, bottom, left }
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 { top, right, bottom, left }
Aabb::new(left, top, right, bottom)
}
seg => seg.bounding_box(),
}
Expand Down
40 changes: 16 additions & 24 deletions libraries/path-bool/src/path_boolean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ new_key_type! {
//
// SPDX-License-Identifier: MIT

use crate::aabb::{bounding_box_around_point, bounding_box_max_extent, merge_bounding_boxes, Aabb};
use crate::aabb::{bounding_box_around_point, bounding_box_max_extent, expand_bounding_box, extend_bounding_box, merge_bounding_boxes, Aabb};
use crate::epsilons::Epsilons;
use crate::intersection_path_segment::{path_segment_intersection, segments_equal};
use crate::path::Path;
Expand Down Expand Up @@ -261,13 +261,11 @@ impl DualGraphHalfEdge {
}
}

fn outer_boundnig_box(&self) -> (DVec2, DVec2) {
fn outer_boundnig_box(&self) -> Aabb {
self.segments
.iter()
.map(|seg| seg.approx_bounding_box())
.fold((DVec2::INFINITY, DVec2::NEG_INFINITY), |(min, max), new| {
(min.min((new.left, new.top).into()), max.max((new.right, new.bottom).into()))
})
.fold(Default::default(), |old, new| merge_bounding_boxes(&old, &new))
}
}

Expand All @@ -285,8 +283,8 @@ struct DualGraphComponent {
edges: Vec<DualEdgeKey>,
vertices: Vec<DualVertexKey>,
outer_face: Option<DualVertexKey>,
inner_bb: (DVec2, DVec2),
outer_bb: (DVec2, DVec2),
inner_bb: Aabb,
outer_bb: Aabb,
}

/// Represents the dual graph of the MinorGraph.
Expand Down Expand Up @@ -434,16 +432,14 @@ fn split_at_self_intersections(edges: &mut Vec<MajorGraphEdgeStage1>) {
/// * A vector of split edges (MajorGraphEdgeStage2).
/// * An optional overall bounding box (AaBb) for all edges.
fn split_at_intersections(edges: &[MajorGraphEdgeStage1]) -> (Vec<MajorGraphEdgeStage1>, Option<Aabb>) {
if edges.is_empty() {
return (Vec::new(), None);
}

// Step 1: Add bounding boxes to edges
let with_bounding_box: Vec<MajorGraphEdgeStage2> = edges.iter().map(|(seg, parent)| (*seg, *parent, seg.approx_bounding_box())).collect();

// Step 2: Calculate total bounding box
let total_bounding_box = with_bounding_box.iter().fold(None, |acc, (_, _, bb)| Some(merge_bounding_boxes(acc, bb)));

let total_bounding_box = match total_bounding_box {
Some(bb) => bb,
None => return (Vec::new(), None),
};
let total_bounding_box = with_bounding_box.iter().fold(Default::default(), |acc, (_, _, bb)| merge_bounding_boxes(&acc, &bb));

// Step 3: Create edge tree for efficient intersection checks
let mut edge_tree = QuadTree::new(total_bounding_box, INTERSECTION_TREE_DEPTH, 8);
Expand Down Expand Up @@ -1211,12 +1207,13 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
.incident_edges
.iter()
.map(|edge_key| dual_edges[*edge_key].start_segment().start())
.fold((DVec2::INFINITY, DVec2::NEG_INFINITY), |(min, max), point| (min.min(point), max.max(point)));
.fold(Default::default(), |bbox, point| extend_bounding_box(Some(bbox), point));
let outer_bb = dual_vertices[outer_face_key]
.incident_edges
.iter()
.map(|edge_key| dual_edges[*edge_key].outer_boundnig_box())
.fold((DVec2::INFINITY, DVec2::NEG_INFINITY), |(min, max), (bb_min, bb_max)| (min.min(bb_min), max.max(bb_max)));
.reduce(|acc, new| merge_bounding_boxes(&acc, &new))
.unwrap_or_default();
// } else {
// *windings.iter().find(|(&_, winding)| (winding < &0) ^ reverse_winding).expect("No outer face of a component found.").0
// };
Expand Down Expand Up @@ -1249,7 +1246,7 @@ fn get_next_edge(edge_key: MinorEdgeKey, graph: &MinorGraph) -> MinorEdgeKey {
}

fn test_inclusion(a: &DualGraphComponent, b: &DualGraphComponent, edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>, vertices: &SlotMap<DualVertexKey, DualGraphVertex>) -> Option<DualVertexKey> {
if a.inner_bb.0.cmpge(b.outer_bb.0) & a.inner_bb.1.cmple(b.outer_bb.1) != BVec2::TRUE {
if a.inner_bb.min().cmpge(b.outer_bb.min()) & a.inner_bb.max().cmple(b.outer_bb.max()) != BVec2::TRUE {
return None;
}
let tested_point = edges[a.edges[0]].segments[0].start();
Expand All @@ -1271,7 +1268,7 @@ fn test_inclusion(a: &DualGraphComponent, b: &DualGraphComponent, edges: &SlotMa
None
}
fn bounding_box_intersects_horizontal_ray(bounding_box: &Aabb, point: DVec2) -> bool {
bounding_box.right >= point[0] && (bounding_box.top..bounding_box.bottom).contains(&point[1])
bounding_box.right() >= point[0] && (bounding_box.top()..bounding_box.bottom()).contains(&point[1])
}

#[derive(Copy, Clone)]
Expand Down Expand Up @@ -2002,12 +1999,7 @@ mod tests {

#[test]
fn test_bounding_box_intersects_horizontal_ray() {
let bbox = Aabb {
top: 10.,
right: 40.,
bottom: 30.,
left: 20.,
};
let bbox = Aabb::new(20., 10., 40., 30.);

assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(0., 29.)));
assert!(bounding_box_intersects_horizontal_ray(&bbox, DVec2::new(20., 29.)));
Expand Down
92 changes: 57 additions & 35 deletions libraries/path-bool/src/util/aabb.rs
Original file line number Diff line number Diff line change
@@ -1,70 +1,92 @@
use glam::DVec2;
use glam::{BVec2, DVec2};

#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Aabb {
pub top: f64,
pub right: f64,
pub bottom: f64,
pub left: f64,
min: DVec2,
max: DVec2,
}

impl Default for Aabb {
fn default() -> Self {
Self {
min: DVec2::INFINITY,
max: DVec2::NEG_INFINITY,
}
}
}

impl Aabb {
#[inline]
pub(crate) fn min(&self) -> DVec2 {
DVec2::new(self.left, self.top)
self.min
}
#[inline]
pub(crate) fn max(&self) -> DVec2 {
self.max
}

pub(crate) const fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
Aabb {
min: DVec2::new(left, top),
max: DVec2::new(right, bottom),
}
}
#[inline]
pub(crate) fn top(&self) -> f64 {
self.min.y
}
#[inline]
pub(crate) fn left(&self) -> f64 {
self.min.x
}
#[inline]
pub(crate) fn right(&self) -> f64 {
self.max.x
}
#[inline]
pub(crate) fn bottom(&self) -> f64 {
self.max.y
}
}

#[inline]
pub(crate) fn bounding_boxes_overlap(a: &Aabb, b: &Aabb) -> bool {
a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom
(a.min.cmple(b.max) & b.min.cmple(a.max)) == BVec2::TRUE
}

pub(crate) fn merge_bounding_boxes(a: Option<Aabb>, b: &Aabb) -> Aabb {
match a {
Some(a) => Aabb {
top: a.top.min(b.top),
right: a.right.max(b.right),
bottom: a.bottom.max(b.bottom),
left: a.left.min(b.left),
},
None => *b,
#[inline]
pub(crate) fn merge_bounding_boxes(a: &Aabb, b: &Aabb) -> Aabb {
Aabb {
min: a.min.min(b.min),
max: a.max.max(b.max),
}
}

#[inline]
pub(crate) fn extend_bounding_box(bounding_box: Option<Aabb>, point: DVec2) -> Aabb {
match bounding_box {
Some(bb) => Aabb {
top: bb.top.min(point.y),
right: bb.right.max(point.x),
bottom: bb.bottom.max(point.y),
left: bb.left.min(point.x),
},
None => Aabb {
top: point.y,
right: point.x,
bottom: point.y,
left: point.x,
min: bb.min.min(point),
max: bb.max.max(point),
},
None => Aabb { min: point, max: point },
}
}

pub(crate) fn bounding_box_max_extent(bounding_box: &Aabb) -> f64 {
(bounding_box.right - bounding_box.left).max(bounding_box.bottom - bounding_box.top)
(bounding_box.max - bounding_box.min).max_element()
}

pub(crate) fn bounding_box_around_point(point: DVec2, padding: f64) -> Aabb {
Aabb {
top: point.y - padding,
right: point.x + padding,
bottom: point.y + padding,
left: point.x - padding,
min: point - DVec2::splat(padding),
max: point + DVec2::splat(padding),
}
}

pub(crate) fn expand_bounding_box(bounding_box: &Aabb, padding: f64) -> Aabb {
Aabb {
top: bounding_box.top - padding,
right: bounding_box.right + padding,
bottom: bounding_box.bottom + padding,
left: bounding_box.left - padding,
min: bounding_box.min - DVec2::splat(padding),
max: bounding_box.max + DVec2::splat(padding),
}
}
48 changes: 6 additions & 42 deletions libraries/path-bool/src/util/quad_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,50 +72,14 @@ impl<T: Clone> QuadTree<T> {
return;
}

let mid_x = (self.bounding_box.left + self.bounding_box.right) / 2.;
let mid_y = (self.bounding_box.top + self.bounding_box.bottom) / 2.;
let mid_x = (self.bounding_box.left() + self.bounding_box.right()) / 2.;
let mid_y = (self.bounding_box.top() + self.bounding_box.bottom()) / 2.;

self.subtrees = Some(Box::new([
QuadTree::new(
Aabb {
top: self.bounding_box.top,
right: mid_x,
bottom: mid_y,
left: self.bounding_box.left,
},
self.depth - 1,
self.inner_node_capacity,
),
QuadTree::new(
Aabb {
top: self.bounding_box.top,
right: self.bounding_box.right,
bottom: mid_y,
left: mid_x,
},
self.depth - 1,
self.inner_node_capacity,
),
QuadTree::new(
Aabb {
top: mid_y,
right: mid_x,
bottom: self.bounding_box.bottom,
left: self.bounding_box.left,
},
self.depth - 1,
self.inner_node_capacity,
),
QuadTree::new(
Aabb {
top: mid_y,
right: self.bounding_box.right,
bottom: self.bounding_box.bottom,
left: mid_x,
},
self.depth - 1,
self.inner_node_capacity,
),
QuadTree::new(Aabb::new(self.bounding_box.left(), self.bounding_box.top(), mid_x, mid_y), self.depth - 1, self.inner_node_capacity),
QuadTree::new(Aabb::new(mid_x, self.bounding_box.top(), self.bounding_box.right(), mid_y), self.depth - 1, self.inner_node_capacity),
QuadTree::new(Aabb::new(self.bounding_box.left(), mid_y, mid_x, self.bounding_box.bottom()), self.depth - 1, self.inner_node_capacity),
QuadTree::new(Aabb::new(mid_x, mid_y, self.bounding_box.right(), self.bounding_box.bottom()), self.depth - 1, self.inner_node_capacity),
]));
}
}