mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
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:
parent
4a65ad290c
commit
504af4e68d
12 changed files with 206 additions and 63 deletions
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
|
@ -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.")),
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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.)]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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>]),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue