Skip to content

Refactor the 'Scatter Points' node to use Kurbo instead of Bezier-rs #2634

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

Merged
merged 3 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion demo-artwork/red-dress.graphite

Large diffs are not rendered by default.

68 changes: 50 additions & 18 deletions node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use super::poisson_disk::poisson_disk_sample;
use crate::vector::PointId;
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2};
use kurbo::{BezPath, ParamCurve, ParamCurveDeriv, PathSeg, Point, Shape};
use crate::vector::misc::dvec2_to_point;
use glam::DVec2;
use kurbo::{Affine, BezPath, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, Rect, Shape};

/// Accuracy to find the position on [kurbo::Bezpath].
const POSITION_ACCURACY: f64 = 1e-5;
Expand Down Expand Up @@ -199,26 +198,29 @@ fn bezpath_t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue, pre
///
/// 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(this: &Subpath<PointId>, separation_disk_diameter: f64, rng: impl FnMut() -> f64, subpaths: &[(Subpath<PointId>, [DVec2; 2])], subpath_index: usize) -> Vec<DVec2> {
let Some(bounding_box) = this.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();
pub fn poisson_disk_points(bezpath: &BezPath, separation_disk_diameter: f64, rng: impl FnMut() -> f64, subpaths: &[(BezPath, Rect)], subpath_index: usize) -> Vec<DVec2> {
if bezpath.elements().is_empty() {
return Vec::new();
}
let bbox = bezpath.bounding_box();
let (offset_x, offset_y) = (bbox.x0, bbox.y0);
let (width, height) = (bbox.x1 - bbox.x0, bbox.y1 - bbox.y0);

// TODO: Optimize the following code and make it more robust

let mut shape = this.clone();
shape.set_closed(true);
shape.apply_transform(DAffine2::from_translation((-offset_x, -offset_y).into()));
let mut shape = bezpath.clone();
shape.close_path();
shape.apply_affine(Affine::translate((-offset_x, -offset_y)));

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 {
for (i, (shape, bbox)) in subpaths.iter().enumerate() {
let point = point + DVec2::new(bbox.x0, bbox.y0);
if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y {
continue;
}
let winding = shape.winding_order(point);
let winding = shape.winding(dvec2_to_point(point));

if i == subpath_index && winding == 0 {
return false;
Expand All @@ -228,9 +230,9 @@ pub fn poisson_disk_points(this: &Subpath<PointId>, separation_disk_diameter: f6
number != 0
};

let square_edges_intersect_shape_checker = |corner1: DVec2, size: f64| {
let corner2 = corner1 + DVec2::splat(size);
this.rectangle_intersections_exist(corner1, corner2)
let square_edges_intersect_shape_checker = |position: DVec2, size: f64| {
let rect = Rect::new(position.x, position.y, position.x + size, position.y + size);
bezpath_rectangle_intersections_exist(bezpath, rect)
};

let mut points = poisson_disk_sample(width, height, separation_disk_diameter, point_in_shape_checker, square_edges_intersect_shape_checker, rng);
Expand All @@ -240,3 +242,33 @@ pub fn poisson_disk_points(this: &Subpath<PointId>, separation_disk_diameter: f6
}
points
}

fn bezpath_rectangle_intersections_exist(bezpath: &BezPath, rect: Rect) -> bool {
if !bezpath.bounding_box().overlaps(rect) {
return false;
}

// Top left
let p1 = Point::new(rect.x0, rect.y0);
// Top right
let p2 = Point::new(rect.x1, rect.y0);
// Bottom right
let p3 = Point::new(rect.x1, rect.y1);
// Bottom left
let p4 = Point::new(rect.x0, rect.y1);

let top_line = Line::new((p1.x, p1.y), (p2.x, p2.y));
let right_line = Line::new((p2.x, p2.y), (p3.x, p3.y));
let bottom_line = Line::new((p3.x, p3.y), (p4.x, p4.y));
let left_line = Line::new((p4.x, p4.y), (p1.x, p1.y));

for segment in bezpath.segments() {
for line in [top_line, right_line, bottom_line, left_line] {
if !segment.intersect_line(line).is_empty() {
return true;
}
}
}

false
}
37 changes: 16 additions & 21 deletions node-graph/gcore/src/vector/vector_nodes.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::algorithms::bezpath_algorithms::{PERIMETER_ACCURACY, position_on_bezpath, sample_points_on_bezpath, tangent_on_bezpath};
use super::algorithms::bezpath_algorithms::{self, PERIMETER_ACCURACY, position_on_bezpath, sample_points_on_bezpath, tangent_on_bezpath};
use super::algorithms::offset_subpath::offset_subpath;
use super::misc::{CentroidType, point_to_dvec2};
use super::style::{Fill, Gradient, GradientStops, Stroke};
Expand All @@ -9,13 +9,14 @@ use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier,
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, ReferencePoint, Transform, TransformMut};
use crate::vector::PointDomain;
use crate::vector::misc::dvec2_to_point;
use crate::vector::style::{LineCap, LineJoin};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue};
use core::f64::consts::PI;
use core::hash::{Hash, Hasher};
use glam::{DAffine2, DVec2};
use kurbo::{Affine, Shape};
use kurbo::{Affine, BezPath, Shape};
use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;

Expand Down Expand Up @@ -1364,36 +1365,30 @@ async fn poisson_disk_points(
return VectorDataTable::new(result);
}
let path_with_bounding_boxes: Vec<_> = vector_data
.stroke_bezier_paths()
.filter_map(|mut subpath| {
.stroke_bezpath_iter()
.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))
subpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
let bbox = subpath.bounding_box();
(subpath, bbox)
})
.collect();

for (i, (subpath, _)) in path_with_bounding_boxes.iter().enumerate() {
if subpath.manipulator_groups().len() < 3 {
if subpath.segments().count() < 2 {
continue;
}

let mut previous_point_index: Option<usize> = None;

for point in subpath.poisson_disk_points(separation_disk_diameter, || rng.random::<f64>(), &path_with_bounding_boxes, i) {
let point_id = PointId::generate();
result.point_domain.push(point_id, point);
let mut poisson_disk_bezpath = BezPath::new();

// Get the index of the newly added point.
let point_index = result.point_domain.ids().len() - 1;

// If there is a previous point, connect it with the current point by adding a segment.
if let Some(prev_point_index) = previous_point_index {
let segment_id = SegmentId::generate();
result.segment_domain.push(segment_id, prev_point_index, point_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
for point in bezpath_algorithms::poisson_disk_points(subpath, separation_disk_diameter, || rng.random::<f64>(), &path_with_bounding_boxes, i) {
if poisson_disk_bezpath.elements().is_empty() {
poisson_disk_bezpath.move_to(dvec2_to_point(point));
} else {
poisson_disk_bezpath.line_to(dvec2_to_point(point));
}

previous_point_index = Some(point_index);
}
result.append_bezpath(poisson_disk_bezpath);
}

// Transfer the style from the input vector data to the result.
Expand Down
Loading