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

@@ -1,64 +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,
}
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
}
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,
impl Default for Aabb {
fn default() -> Self {
Self {
min: DVec2::INFINITY,
max: DVec2::NEG_INFINITY,
}
}
}
impl Aabb {
#[inline]
pub(crate) fn min(&self) -> DVec2 {
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.min.cmple(b.max) & b.min.cmple(a.max)) == BVec2::TRUE
}
#[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),
}
}

View File

@@ -0,0 +1,128 @@
use crate::aabb::Aabb;
use glam::{DVec2, IVec2};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
pub(crate) struct Grid {
cell_factor: f64,
cells: FxHashMap<IVec2, SmallVec<[usize; 6]>>,
}
impl Grid {
pub(crate) fn new(cell_size: f64, edges: usize) -> Self {
Grid {
cell_factor: cell_size.recip(),
cells: FxHashMap::with_capacity_and_hasher(edges, Default::default()),
}
}
pub(crate) fn insert(&mut self, bbox: &Aabb, index: usize) {
let min_cell = self.point_to_cell_floor(bbox.min());
let max_cell = self.point_to_cell_ceil(bbox.max());
for i in min_cell.x..=max_cell.x {
for j in min_cell.y..=max_cell.y {
self.cells.entry((i, j).into()).or_default().push(index);
}
}
}
pub(crate) fn query(&self, bbox: &Aabb, result: &mut BitVec) {
let min_cell = self.point_to_cell_floor(bbox.min());
let max_cell = self.point_to_cell_ceil(bbox.max());
for i in min_cell.x..=max_cell.x {
for j in min_cell.y..=max_cell.y {
if let Some(indices) = self.cells.get(&(i, j).into()) {
for &index in indices {
result.set(index);
}
}
}
}
// result.sort_unstable();
// result.dedup();
}
fn point_to_cell_ceil(&self, point: DVec2) -> IVec2 {
(point * self.cell_factor).ceil().as_ivec2()
}
fn point_to_cell_floor(&self, point: DVec2) -> IVec2 {
(point * self.cell_factor).floor().as_ivec2()
}
}
pub struct BitVec {
data: Vec<u64>,
}
impl BitVec {
pub fn new(capacity: usize) -> Self {
let num_words = capacity.div_ceil(64);
BitVec { data: vec![0; num_words] }
}
pub fn set(&mut self, index: usize) {
let word_index = index / 64;
let bit_index = index % 64;
self.data[word_index] |= 1u64 << bit_index;
}
pub fn clear(&mut self) {
self.data.fill(0);
}
pub fn iter_set_bits(&self) -> BitVecIterator<'_> {
BitVecIterator {
bit_vec: self,
current_word: self.data[0],
word_index: 0,
}
}
}
pub struct BitVecIterator<'a> {
bit_vec: &'a BitVec,
current_word: u64,
word_index: usize,
}
impl<'a> Iterator for BitVecIterator<'a> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.current_word == 0 {
self.word_index += 1;
if self.word_index == self.bit_vec.data.len() {
return None;
}
self.current_word = self.bit_vec.data[self.word_index];
continue;
}
let tz = self.current_word.trailing_zeros() as usize;
self.current_word ^= 1 << tz;
let result = self.word_index * 64 + tz;
return Some(result);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bitvec() {
let mut bv = BitVec::new(200);
bv.set(5);
bv.set(64);
bv.set(128);
bv.set(199);
let set_bits: Vec<usize> = bv.iter_set_bits().collect();
assert_eq!(set_bits, vec![5, 64, 128, 199]);
}
}

View File

@@ -1,121 +0,0 @@
use crate::aabb::Aabb;
use std::collections::HashSet;
pub struct QuadTree<T> {
bounding_box: Aabb,
depth: usize,
inner_node_capacity: usize,
subtrees: Option<Box<[QuadTree<T>; 4]>>,
pairs: Vec<(Aabb, T)>,
}
impl<T: Clone> QuadTree<T> {
pub fn new(bounding_box: Aabb, depth: usize, inner_node_capacity: usize) -> Self {
QuadTree {
bounding_box,
depth,
inner_node_capacity,
subtrees: None,
pairs: Vec::new(),
}
}
pub fn insert(&mut self, bounding_box: Aabb, value: T) -> bool {
if !crate::aabb::bounding_boxes_overlap(&bounding_box, &self.bounding_box) {
return false;
}
if self.depth > 0 && self.pairs.len() >= self.inner_node_capacity {
self.ensure_subtrees();
for tree in self.subtrees.as_mut().unwrap().iter_mut() {
tree.insert(bounding_box, value.clone());
}
} else {
self.pairs.push((bounding_box, value));
}
true
}
pub fn find(&self, bounding_box: &Aabb) -> HashSet<T>
where
T: Eq + std::hash::Hash + Clone,
{
let mut set = HashSet::new();
self.find_internal(bounding_box, &mut set);
set
}
fn find_internal(&self, bounding_box: &Aabb, set: &mut HashSet<T>)
where
T: Eq + std::hash::Hash + Clone,
{
if !crate::aabb::bounding_boxes_overlap(bounding_box, &self.bounding_box) {
return;
}
for (key, value) in &self.pairs {
if crate::aabb::bounding_boxes_overlap(bounding_box, key) {
set.insert(value.clone());
}
}
if let Some(subtrees) = &self.subtrees {
for tree in subtrees.iter() {
tree.find_internal(bounding_box, set);
}
}
}
fn ensure_subtrees(&mut self) {
if self.subtrees.is_some() {
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.;
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,
),
]));
}
}