From 9a62c1c089c2a9ee3b6b3b963163621eea8553f9 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Tue, 15 Apr 2025 15:37:20 +0200 Subject: [PATCH] Fix Poisson-disk sampling with negative space from nested subpaths (#2569) * Fix poisson disk sampling with nested subpaths Previously all subpaths were considered independently for the poisson disk sampling evaluation. We now check agains all subpaths which might contain the point to fix shapes with holes such as fonts with letters with holes in them * Fix wasm demo * Fix counting overlapping areas twice * Rename shape variables to subpath variants --- libraries/bezier-rs/src/subpath/solvers.rs | 20 +++++++++++++++++-- node-graph/gcore/Cargo.toml | 2 +- node-graph/gcore/src/vector/vector_nodes.rs | 14 +++++++++---- .../other/bezier-rs-demos/wasm/src/subpath.rs | 4 +++- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index a148ebce..0058eccb 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -389,7 +389,7 @@ impl Subpath { /// /// While the conceptual process described above asymptotically slows down and is never guaranteed to produce a maximal set in finite time, /// this is implemented with an algorithm that produces a maximal set in O(n) time. The slowest part is actually checking if points are inside the subpath shape. - pub fn poisson_disk_points(&self, separation_disk_diameter: f64, rng: impl FnMut() -> f64) -> Vec { + pub fn poisson_disk_points(&self, separation_disk_diameter: f64, rng: impl FnMut() -> f64, subpaths: &[(Self, [DVec2; 2])], subpath_index: usize) -> Vec { let Some(bounding_box) = self.bounding_box() else { return Vec::new() }; let (offset_x, offset_y) = bounding_box[0].into(); let (width, height) = (bounding_box[1] - bounding_box[0]).into(); @@ -400,7 +400,23 @@ impl Subpath { shape.set_closed(true); shape.apply_transform(DAffine2::from_translation((-offset_x, -offset_y).into())); - let point_in_shape_checker = |point: DVec2| shape.winding_order(point) != 0; + let point_in_shape_checker = |point: DVec2| { + // Check against all paths the point is contained in to compute the correct winding number + let mut number = 0; + for (i, (shape, bb)) in subpaths.iter().enumerate() { + let point = point + bounding_box[0]; + if bb[0].x > point.x || bb[0].y > point.y || bb[1].x < point.x || bb[1].y < point.y { + continue; + } + let winding = shape.winding_order(point); + + if i == subpath_index && winding == 0 { + return false; + } + number += winding; + } + number != 0 + }; let square_edges_intersect_shape_checker = |corner1: DVec2, size: f64| { let corner2 = corner1 + DVec2::splat(size); diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml index e9c9180f..fa9abbec 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -67,7 +67,7 @@ serde = { workspace = true, optional = true, features = ["derive"] } ctor = { workspace = true, optional = true } log = { workspace = true, optional = true } rand_chacha = { workspace = true, optional = true } -bezier-rs = { workspace = true, optional = true } +bezier-rs = { workspace = true, optional = true, features = ["log"] } kurbo = { workspace = true, optional = true } base64 = { workspace = true, optional = true } vello = { workspace = true, optional = true } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 19f31c00..daf84882 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1245,17 +1245,23 @@ async fn poisson_disk_points( if separation_disk_diameter <= 0.01 { return VectorDataTable::new(result); } + let path_with_bounding_boxes: Vec<_> = vector_data + .stroke_bezier_paths() + .filter_map(|mut subpath| { + // TODO: apply transform to points instead of modifying the paths + subpath.apply_transform(vector_data_transform); + subpath.loose_bounding_box().map(|bb| (subpath, bb)) + }) + .collect(); - for mut subpath in vector_data.stroke_bezier_paths() { + for (i, (subpath, _)) in path_with_bounding_boxes.iter().enumerate() { if subpath.manipulator_groups().len() < 3 { continue; } - subpath.apply_transform(vector_data_transform); - let mut previous_point_index: Option = None; - for point in subpath.poisson_disk_points(separation_disk_diameter, || rng.random::()) { + for point in subpath.poisson_disk_points(separation_disk_diameter, || rng.random::(), &path_with_bounding_boxes, i) { let point_id = PointId::generate(); result.point_domain.push(point_id, point); diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 39b545ac..54f87c6c 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -137,7 +137,9 @@ impl WasmSubpath { let r = separation_disk_diameter / 2.; let subpath_svg = self.to_default_svg(); - let points = self.0.poisson_disk_points(separation_disk_diameter, Math::random); + let points = self + .0 + .poisson_disk_points(separation_disk_diameter, Math::random, &[(self.0.clone(), self.0.bounding_box().unwrap())], 0); let points_style = format!(""); let content = points