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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1822,12 +1822,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
properties: None,
},
DocumentNodeDefinition {
identifier: "Sample Points",
identifier: "Sample Polyline",
category: "Vector: Modifier",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(4), 0)], // Taken from output 0 of Sample Points
exports: vec![NodeInput::node(NodeId(4), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0)],
@ -1838,13 +1838,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNode {
inputs: vec![
NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0),
NodeInput::network(concrete!(f64), 1), // From the document node's parameters
NodeInput::network(concrete!(f64), 2), // From the document node's parameters
NodeInput::network(concrete!(f64), 3), // From the document node's parameters
NodeInput::network(concrete!(bool), 4), // From the document node's parameters
NodeInput::node(NodeId(0), 0), // From output 0 of SubpathSegmentLengthsNode
NodeInput::network(concrete!(vector::misc::PointSpacingType), 1),
NodeInput::network(concrete!(f64), 2),
NodeInput::network(concrete!(f64), 3),
NodeInput::network(concrete!(f64), 4),
NodeInput::network(concrete!(f64), 5),
NodeInput::network(concrete!(bool), 6),
NodeInput::node(NodeId(0), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SamplePointsNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SamplePolylineNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
@ -1875,6 +1877,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}),
inputs: vec![
NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true),
NodeInput::value(TaggedValue::PointSpacingType(Default::default()), false),
NodeInput::value(TaggedValue::F64(100.), false),
NodeInput::value(TaggedValue::F64(100.), false),
NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::F64(0.), false),
@ -1889,14 +1893,14 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Subpath Segment Lengths".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 5)),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 7)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Sample Points".to_string(),
display_name: "Sample Polyline".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
..Default::default()
},
@ -1937,18 +1941,28 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}),
input_properties: vec![
("Vector Data", "The shape to be resampled and converted into a polyline.").into(),
Into::<PropertiesRow>::into(("Spacing", node_properties::SAMPLE_POLYLINE_TOOLTIP_SPACING)),
PropertiesRow::with_override(
"Spacing",
"Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).",
"Separation",
node_properties::SAMPLE_POLYLINE_TOOLTIP_SEPARATION,
WidgetOverride::Number(NumberInputSettings {
min: Some(1.),
min: Some(0.),
unit: Some(" px".to_string()),
..Default::default()
}),
),
PropertiesRow::with_override(
"Quantity",
node_properties::SAMPLE_POLYLINE_TOOLTIP_QUANTITY,
WidgetOverride::Number(NumberInputSettings {
min: Some(2.),
is_integer: true,
..Default::default()
}),
),
PropertiesRow::with_override(
"Start Offset",
"Exclude some distance from the start of the path before the first instance.",
node_properties::SAMPLE_POLYLINE_TOOLTIP_START_OFFSET,
WidgetOverride::Number(NumberInputSettings {
min: Some(0.),
unit: Some(" px".to_string()),
@ -1957,21 +1971,21 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
),
PropertiesRow::with_override(
"Stop Offset",
"Exclude some distance from the end of the path after the last instance.",
node_properties::SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET,
WidgetOverride::Number(NumberInputSettings {
min: Some(0.),
unit: Some(" px".to_string()),
..Default::default()
}),
),
Into::<PropertiesRow>::into(("Adaptive Spacing", "Round 'Spacing' to a nearby value that divides into the path length evenly.")),
Into::<PropertiesRow>::into(("Adaptive Spacing", node_properties::SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING)),
],
output_names: vec!["Vector".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("Convert vector geometry into a polyline composed of evenly spaced points."),
properties: None,
properties: Some("sample_polyline_properties"),
},
DocumentNodeDefinition {
identifier: "Scatter Points",
@ -2344,6 +2358,7 @@ fn static_node_properties() -> NodeProperties {
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
map.insert(
"identity_properties".to_string(),
Box::new(|_node_id, _context| node_properties::string_properties("The identity node simply passes its data through.")),

View file

@ -22,9 +22,9 @@ use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::text::Font;
use graphene_std::transform::{Footprint, ReferencePoint};
use graphene_std::vector::VectorDataTable;
use graphene_std::vector::misc::CentroidType;
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
use graphene_std::vector::misc::{BooleanOperation, GridType};
use graphene_std::vector::misc::{CentroidType, PointSpacingType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
@ -238,6 +238,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
@ -1225,6 +1226,64 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
widgets
}
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SPACING: &str = "Use a point sampling density controlled by a distance between, or specific number of, points.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SEPARATION: &str = "Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_START_OFFSET: &str = "Exclude some distance from the start of the path before the first instance.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET: &str = "Exclude some distance from the end of the path after the last instance.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING: &str = "Round 'Separation' to a nearby value that divides into the path length evenly.";
pub(crate) fn sample_polyline_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::sample_polyline::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
log::error!("Could not get document node in sample_polyline_properties: {err}");
return Vec::new();
}
};
let current_spacing = document_node.inputs.get(SpacingInput::INDEX).and_then(|input| input.as_value()).cloned();
let is_quantity = matches!(current_spacing, Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)));
let spacing = enum_choice::<PointSpacingType>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, SpacingInput::INDEX, true, context))
.property_row();
let separation = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, SeparationInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let quantity = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, QuantityInput::INDEX, true, context),
NumberInput::default().min(2.).int(),
);
let start_offset = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, StartOffsetInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let stop_offset = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, StopOffsetInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let adaptive_spacing = bool_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, AdaptiveSpacingInput::INDEX, true, context),
CheckboxInput::default().disabled(is_quantity),
);
vec![
spacing.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_SPACING),
match current_spacing {
Some(TaggedValue::PointSpacingType(PointSpacingType::Separation)) => LayoutGroup::Row { widgets: separation }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_SEPARATION),
Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)) => LayoutGroup::Row { widgets: quantity }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_QUANTITY),
_ => LayoutGroup::Row { widgets: vec![] },
},
LayoutGroup::Row { widgets: start_offset }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_START_OFFSET),
LayoutGroup::Row { widgets: stop_offset }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET),
LayoutGroup::Row { widgets: adaptive_spacing }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING),
]
}
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::exposure::*;

View file

@ -441,7 +441,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
let document_name = document_name.replace("__DO_NOT_UPGRADE__", "");
const TEXT_REPLACEMENTS: [(&str, &str); 2] = [
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePointsNode"),
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"),
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
];
let document_serialized_content = TEXT_REPLACEMENTS
@ -1074,6 +1074,31 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document.network_interface.replace_reference_name(node_id, network_path, "Merge by Distance".to_string());
}
if reference == "Sample Points" && inputs_count == 5 {
// TODO: Rename to "Sample Polyline", also remove segment generation from "Scatter Points"
let node_definition = resolve_document_node_type("Sample Polyline").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let new_spacing_value = NodeInput::value(TaggedValue::PointSpacingType(graphene_std::vector::misc::PointSpacingType::Separation), false);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), new_spacing_value, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[4].clone(), network_path);
// TODO: Rename to "Sample Polyline", also remove segment generation from "Scatter Points"
document.network_interface.replace_reference_name(node_id, network_path, "Sample Polyline".to_string());
}
}
// TODO: Eventually remove this document upgrade code

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>]),