Skip to content

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

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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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/parametric-dunescape.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/procedural-string-lights.graphite

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo-artwork/red-dress.graphite

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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)],
Expand All @@ -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()
},
Expand Down Expand Up @@ -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),
Expand All @@ -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()
},
Expand Down Expand Up @@ -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()),
Expand All @@ -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",
Expand Down Expand Up @@ -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.")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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::*;

Expand Down
27 changes: 26 additions & 1 deletion editor/src/messages/portfolio/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion node-graph/gcore/src/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
45 changes: 32 additions & 13 deletions node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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));
Expand All @@ -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;
}

const SAFETY_MAX_COUNT: f64 = 10_000. - 1.;

// 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.

// 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
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.
Expand Down
11 changes: 11 additions & 0 deletions node-graph/gcore/src/vector/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
Loading
Loading