Skip to content
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