mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 10:58:04 +08:00
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:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
128
libraries/path-bool/src/util/grid.rs
Normal file
128
libraries/path-bool/src/util/grid.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user