Rename 'Sample Points' node to 'Sample Polyline' and add a parameter spacing based on separation or quantity (#2727)

* Added Count point Radio button to property pannel

* Implemented on Count radio button functionality

* Fixed linting and Title case problem

* Fixing more linting problem

* Instance tables refactor part 8: Make repeater nodes use pivot not bbox and output instance type not group; rename 'Flatten Vector Elements' to 'Flatten Path' and add 'Flatten Vector' (#2697)

Make repeater nodes use pivot not bbox and output instance type not group; rename 'Flatten Vector Elements' to 'Flatten Path' and add 'Flatten Vector'

* Refactor the 'Bounding Box' node to use Kurbo instead of Bezier-rs (#2662)

* use kurbo's default accuracy constant

* fix append_bezpath() method

* refactor bounding box node

* fix append bezpath implementation.

* comments

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>

* Add overlays for free-floating anchors on hovered/selected vector layers (#2630)

* Add selection overlay for free-floating anchors

* Add hover overlay for free-floating anchors

* Refactor outline_free_floating anchor

* Add single-anchor click targets on VectorData

* Modify ClickTarget to adapt for Subpath and PointGroup

* Fix Rust formatting

* Remove debug statements

* Add point groups support in VectorDataTable::add_upstream_click_targets

* Improve overlay for free floating anchors

* Remove datatype for nodes_to_shift

* Fix formatting in select_tool.rs

* Lints

* Code review

* Remove references to point_group

* Refactor ManipulatorGroup for FreePoint in ClickTargetGroup

* Rename ClickTargetGroup to ClickTargetType

* Refactor outline_free_floating_anchors into outline

* Adapt TransformCage to disable dragging and rotating on a single anchor layer

* Fix hover on single points

* Fix comments

* Lints

* Code review pass

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>

* Add anchor sliding along adjacent segments in the Path tool (#2682)

* Improved comments

* Add point sliding with approximate t value

* Add similarity calculation

* Numerical approach to fit the curve

* Reliable point sliding for cubic segments

* Fix formatting and clean comments

* Fix cubic with one handle logic

* Cancel on right click and escape

* Two parameter optimization

* Esc/ Right click cancellation

* Code review

* Fix dynamic hints

* Revert selected_points_counts and fix comments

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>

* Fix Sample Points node to avoid duplicating endpoints instead of closing its sampled paths (#2714)

* Skip duplicate endpoint and close sampled paths in Sample Points node
Closes #2713

* Comment

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>

* Implemented on Count radio button functionality

* Fixed linting and Title case problem

* The sample count can now work with adaptive spacing

* Readying for production

* Rename to 'Sample Polyline' and add migration

* Upgrade demo artwork

* Add monomorphization

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Priyanshu <indierusty@gmail.com>
Co-authored-by: seam0s <153828136+seam0s-dev@users.noreply.github.com>
Co-authored-by: Adesh Gupta <148623820+4adex@users.noreply.github.com>
Co-authored-by: Ezbaze <68749104+Ezbaze@users.noreply.github.com>
This commit is contained in:
Ahmed Moharram 2025-06-26 06:14:08 +03:00 committed by GitHub
parent 4a65ad290c
commit 504af4e68d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 206 additions and 63 deletions

View file

@ -2,7 +2,7 @@ use crate::vector::VectorDataTable;
use crate::{Color, Context, Ctx};
use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"))]
#[node_macro::node(category("Debug"), name("Log to Console"))]
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("{:#?}", value);

View file

@ -1,5 +1,5 @@
use super::poisson_disk::poisson_disk_sample;
use crate::vector::misc::dvec2_to_point;
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
use glam::DVec2;
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape};
@ -67,7 +67,15 @@ pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_l
}
}
pub fn sample_points_on_bezpath(bezpath: BezPath, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, segments_length: &[f64]) -> Option<BezPath> {
pub fn sample_polyline_on_bezpath(
bezpath: BezPath,
point_spacing_type: PointSpacingType,
amount: f64,
start_offset: f64,
stop_offset: f64,
adaptive_spacing: bool,
segments_length: &[f64],
) -> Option<BezPath> {
let mut sample_bezpath = BezPath::new();
let was_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
@ -78,22 +86,33 @@ pub fn sample_points_on_bezpath(bezpath: BezPath, spacing: f64, start_offset: f6
// Adjust the usable length by subtracting start and stop offsets.
let mut used_length = total_length - start_offset - stop_offset;
// Sanity check that the usable length is positive.
if used_length <= 0. {
return None;
}
// Determine the number of points to generate along the path.
let sample_count = if adaptive_spacing {
// Calculate point count to evenly distribute points while covering the entire path.
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
(used_length / spacing).round()
} else {
// Calculate point count based on exact spacing, which may not cover the entire path.
const SAFETY_MAX_COUNT: f64 = 10_000. - 1.;
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
let count = (used_length / spacing + f64::EPSILON).floor();
used_length -= used_length % spacing;
count
// Determine the number of points to generate along the path.
let sample_count = match point_spacing_type {
PointSpacingType::Separation => {
let spacing = amount.min(used_length - f64::EPSILON);
if adaptive_spacing {
// Calculate point count to evenly distribute points while covering the entire path.
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
(used_length / spacing).round().min(SAFETY_MAX_COUNT)
} else {
// Calculate point count based on exact spacing, which may not cover the entire path.
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
let count = (used_length / spacing + f64::EPSILON).floor().min(SAFETY_MAX_COUNT);
if count != SAFETY_MAX_COUNT {
used_length -= used_length % spacing;
}
count
}
}
PointSpacingType::Quantity => (amount - 1.).floor().clamp(1., SAFETY_MAX_COUNT),
};
// Skip if there are no points to generate.

View file

@ -103,6 +103,17 @@ pub enum MergeByDistanceAlgorithm {
Topological,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum PointSpacingType {
#[default]
/// The desired spacing distance between points.
Separation,
/// The exact number of points to span the path.
Quantity,
}
pub fn point_to_dvec2(point: Point) -> DVec2 {
DVec2 { x: point.x, y: point.y }
}

View file

@ -1,4 +1,4 @@
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_points_on_bezpath, split_bezpath, tangent_on_bezpath};
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
use super::algorithms::offset_subpath::offset_subpath;
use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use super::misc::{CentroidType, point_to_dvec2};
@ -9,7 +9,7 @@ use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, ReferencePoint, Transform};
use crate::vector::misc::{MergeByDistanceAlgorithm, dvec2_to_point};
use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, dvec2_to_point};
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use crate::vector::{FillId, PointDomain, RegionId};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
@ -1141,11 +1141,19 @@ where
output_table
}
/// Convert vector geometry into a polyline composed of evenly spaced points.
#[node_macro::node(category(""), path(graphene_core::vector))]
async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, subpath_segment_lengths: Vec<f64>) -> VectorDataTable {
// Limit the smallest spacing to something sensible to avoid freezing the application.
let spacing = spacing.max(0.01);
async fn sample_polyline(
_: impl Ctx,
vector_data: VectorDataTable,
spacing: PointSpacingType,
separation: f64,
quantity: f64,
start_offset: f64,
stop_offset: f64,
adaptive_spacing: bool,
subpath_segment_lengths: Vec<f64>,
) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
for mut vector_data_instance in vector_data.instance_iter() {
@ -1180,7 +1188,11 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
// Increment the segment index by the number of segments in the current bezpath to calculate the next bezpath segment's length.
next_segment_index += segment_count;
let Some(mut sample_bezpath) = sample_points_on_bezpath(bezpath, spacing, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
let amount = match spacing {
PointSpacingType::Separation => separation,
PointSpacingType::Quantity => quantity,
};
let Some(mut sample_bezpath) = sample_polyline_on_bezpath(bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
continue;
};
@ -2073,41 +2085,41 @@ mod test {
}
}
#[tokio::test]
async fn sample_points() {
async fn sample_polyline() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 30., 0., 0., false, vec![100.]).await;
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_points.point_domain.positions().len(), 4);
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 30., 0., 0., 0., false, vec![100.]).await;
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
}
}
#[tokio::test]
async fn adaptive_spacing() {
async fn sample_polyline_adaptive_spacing() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 18., 45., 10., true, vec![100.]).await;
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_points.point_domain.positions().len(), 4);
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 18., 0., 45., 10., true, vec![100.]).await;
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
}
}
#[tokio::test]
async fn poisson() {
let sample_points = super::poisson_disk_points(
let poisson_points = super::poisson_disk_points(
Footprint::default(),
vector_node(Subpath::new_ellipse(DVec2::NEG_ONE * 50., DVec2::ONE * 50.)),
10. * std::f64::consts::SQRT_2,
0,
)
.await;
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
let poisson_points = poisson_points.instance_ref_iter().next().unwrap().instance;
assert!(
(20..=40).contains(&sample_points.point_domain.positions().len()),
(20..=40).contains(&poisson_points.point_domain.positions().len()),
"actual len {}",
sample_points.point_domain.positions().len()
poisson_points.point_domain.positions().len()
);
for point in sample_points.point_domain.positions() {
for point in poisson_points.point_domain.positions() {
assert!(point.length() < 50. + 1., "Expected point in circle {point}")
}
}
@ -2126,8 +2138,8 @@ mod test {
let length = super::path_length(Footprint::default(), vector_node_from_instances(instances)).await;
// 4040 equals 101 * 4 (rectangle perimeter) * 2 (scale) * 5 (number of rows)
assert_eq!(length, 4040.);
// 101 (each rectangle edge length) * 4 (rectangle perimeter) * 2 (scale) * 5 (number of rows)
assert_eq!(length, 101. * 4. * 2. * 5.);
}
#[tokio::test]
async fn spline() {
@ -2140,10 +2152,10 @@ mod test {
async fn morph() {
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO);
let sample_points = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5).await;
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
let morphed = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5).await;
let morphed = morphed.instance_ref_iter().next().unwrap().instance;
assert_eq!(
&sample_points.point_domain.positions()[..4],
&morphed.point_domain.positions()[..4],
vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)]
);
}

View file

@ -235,6 +235,7 @@ tagged_value! {
GridType(graphene_core::vector::misc::GridType),
ArcType(graphene_core::vector::misc::ArcType),
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
PointSpacingType(graphene_core::vector::misc::PointSpacingType),
#[serde(alias = "LineCap")]
StrokeCap(graphene_core::vector::style::StrokeCap),
#[serde(alias = "LineJoin")]

View file

@ -70,6 +70,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Color]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_core::vector::VectorModification>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => VectorDataTable]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RasterDataTable<CPU>]),