Skip to content

Commit fbef888

Browse files
authored
Migrate 'Sample Polylines' from a subgraph to a proto node (#4063)
1 parent 7f48e17 commit fbef888

5 files changed

Lines changed: 60 additions & 205 deletions

File tree

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 0 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1938,141 +1938,6 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
19381938
description: Cow::Borrowed("TODO"),
19391939
properties: None,
19401940
},
1941-
DocumentNodeDefinition {
1942-
identifier: "Sample Polyline",
1943-
category: "Vector: Modifier",
1944-
node_template: NodeTemplate {
1945-
document_node: DocumentNode {
1946-
implementation: DocumentNodeImplementation::Network(NodeNetwork {
1947-
exports: vec![NodeInput::node(NodeId(2), 0)],
1948-
nodes: [
1949-
DocumentNode {
1950-
inputs: vec![NodeInput::import(concrete!(Table<Vector>), 0)],
1951-
implementation: DocumentNodeImplementation::ProtoNode(vector::subpath_segment_lengths::IDENTIFIER),
1952-
call_argument: generic!(T),
1953-
..Default::default()
1954-
},
1955-
DocumentNode {
1956-
inputs: vec![
1957-
NodeInput::import(concrete!(Table<Vector>), 0),
1958-
NodeInput::import(concrete!(vector::misc::PointSpacingType), 1),
1959-
NodeInput::import(concrete!(f64), 2),
1960-
NodeInput::import(concrete!(u32), 3),
1961-
NodeInput::import(concrete!(f64), 4),
1962-
NodeInput::import(concrete!(f64), 5),
1963-
NodeInput::import(concrete!(bool), 6),
1964-
NodeInput::node(NodeId(0), 0),
1965-
],
1966-
implementation: DocumentNodeImplementation::ProtoNode(vector::sample_polyline::IDENTIFIER),
1967-
call_argument: generic!(T),
1968-
..Default::default()
1969-
},
1970-
DocumentNode {
1971-
inputs: vec![NodeInput::node(NodeId(1), 0)],
1972-
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
1973-
call_argument: generic!(T),
1974-
..Default::default()
1975-
},
1976-
]
1977-
.into_iter()
1978-
.enumerate()
1979-
.map(|(id, node)| (NodeId(id as u64), node))
1980-
.collect(),
1981-
..Default::default()
1982-
}),
1983-
inputs: vec![
1984-
NodeInput::value(TaggedValue::Vector(Default::default()), true),
1985-
NodeInput::value(TaggedValue::PointSpacingType(Default::default()), false),
1986-
NodeInput::value(TaggedValue::F64(100.), false),
1987-
NodeInput::value(TaggedValue::U32(100), false),
1988-
NodeInput::value(TaggedValue::F64(0.), false),
1989-
NodeInput::value(TaggedValue::F64(0.), false),
1990-
NodeInput::value(TaggedValue::Bool(false), false),
1991-
],
1992-
..Default::default()
1993-
},
1994-
persistent_node_metadata: DocumentNodePersistentMetadata {
1995-
network_metadata: Some(NodeNetworkMetadata {
1996-
persistent_metadata: NodeNetworkPersistentMetadata {
1997-
node_metadata: [
1998-
DocumentNodeMetadata {
1999-
persistent_metadata: DocumentNodePersistentMetadata {
2000-
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 7)),
2001-
..Default::default()
2002-
},
2003-
..Default::default()
2004-
},
2005-
DocumentNodeMetadata {
2006-
persistent_metadata: DocumentNodePersistentMetadata {
2007-
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
2008-
..Default::default()
2009-
},
2010-
..Default::default()
2011-
},
2012-
DocumentNodeMetadata {
2013-
persistent_metadata: DocumentNodePersistentMetadata {
2014-
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)),
2015-
..Default::default()
2016-
},
2017-
..Default::default()
2018-
},
2019-
]
2020-
.into_iter()
2021-
.enumerate()
2022-
.map(|(id, node)| (NodeId(id as u64), node))
2023-
.collect(),
2024-
..Default::default()
2025-
},
2026-
..Default::default()
2027-
}),
2028-
input_metadata: vec![
2029-
("Content", "The shape to be resampled and converted into a polyline.").into(),
2030-
("Spacing", node_properties::SAMPLE_POLYLINE_DESCRIPTION_SPACING).into(),
2031-
InputMetadata::with_name_description_override(
2032-
"Separation",
2033-
node_properties::SAMPLE_POLYLINE_DESCRIPTION_SEPARATION,
2034-
WidgetOverride::Number(NumberInputSettings {
2035-
min: Some(0.),
2036-
unit: Some(" px".to_string()),
2037-
..Default::default()
2038-
}),
2039-
),
2040-
InputMetadata::with_name_description_override(
2041-
"Quantity",
2042-
node_properties::SAMPLE_POLYLINE_DESCRIPTION_QUANTITY,
2043-
WidgetOverride::Number(NumberInputSettings {
2044-
min: Some(2.),
2045-
is_integer: true,
2046-
..Default::default()
2047-
}),
2048-
),
2049-
InputMetadata::with_name_description_override(
2050-
"Start Offset",
2051-
node_properties::SAMPLE_POLYLINE_DESCRIPTION_START_OFFSET,
2052-
WidgetOverride::Number(NumberInputSettings {
2053-
min: Some(0.),
2054-
unit: Some(" px".to_string()),
2055-
..Default::default()
2056-
}),
2057-
),
2058-
InputMetadata::with_name_description_override(
2059-
"Stop Offset",
2060-
node_properties::SAMPLE_POLYLINE_DESCRIPTION_STOP_OFFSET,
2061-
WidgetOverride::Number(NumberInputSettings {
2062-
min: Some(0.),
2063-
unit: Some(" px".to_string()),
2064-
..Default::default()
2065-
}),
2066-
),
2067-
("Adaptive Spacing", node_properties::SAMPLE_POLYLINE_DESCRIPTION_ADAPTIVE_SPACING).into(),
2068-
],
2069-
output_names: vec!["Vector".to_string()],
2070-
..Default::default()
2071-
},
2072-
},
2073-
description: Cow::Borrowed("Convert vector geometry into a polyline composed of evenly spaced points."),
2074-
properties: Some("sample_polyline_properties"),
2075-
},
20761941
DocumentNodeDefinition {
20771942
identifier: "Scatter Points",
20781943
category: "Vector: Modifier",

editor/src/messages/portfolio/document_migration.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
9494
"graphene_core::transform::FreezeRealTimeNode",
9595
"graphene_core::transform_nodes::BoundlessFootprintNode",
9696
"graphene_core::transform_nodes::FreezeRealTimeNode",
97+
// `subpath_segment_lengths` was inlined into the `sample_polyline` proto; old "Sample Polyline" subnetworks pass through unchanged.
98+
"graphene_core::vector::SubpathSegmentLengthsNode",
99+
"core_types::vector::SubpathSegmentLengthsNode",
97100
],
98101
},
99102
NodeReplacement {
@@ -946,10 +949,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
946949
node: graphene_std::vector::stroke::IDENTIFIER,
947950
aliases: &["graphene_core::vector::StrokeNode"],
948951
},
949-
NodeReplacement {
950-
node: graphene_std::vector::subpath_segment_lengths::IDENTIFIER,
951-
aliases: &["graphene_core::vector::SubpathSegmentLengthsNode"],
952-
},
953952
NodeReplacement {
954953
node: graphene_std::vector::tangent_on_path::IDENTIFIER,
955954
aliases: &["graphene_core::vector::TangentOnPathNode"],
@@ -1075,6 +1074,10 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
10751074
}
10761075

10771076
fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) -> Option<()> {
1077+
// Must run before the reset block below: a node referencing a removed catalog entry would otherwise abort
1078+
// `migrate_node` via the `?` on `resolve_document_node_type`, preventing subsequent migration blocks from running.
1079+
migrate_removed_catalog_definitions(node_id, node, network_path, document);
1080+
10781081
if reset_node_definitions_on_open && let Some(reference) = document.network_interface.reference(node_id, network_path) {
10791082
let node_definition = resolve_document_node_type(&reference)?;
10801083
document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template());
@@ -2026,6 +2029,32 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
20262029
Some(())
20272030
}
20282031

2032+
/// Migrates document nodes whose catalog definitions have been removed.
2033+
///
2034+
/// This is called from `migrate_node` BEFORE its standard reset/migration logic, since that logic aborts when it can't
2035+
/// resolve the node's `reference` against the current catalog. Each block here detects a specific decommissioned
2036+
/// definition by its old reference name, swaps it to a still-supported implementation, and preserves the user's inputs.
2037+
/// After this runs, the node's reference resolves cleanly so the rest of `migrate_node` proceeds normally.
2038+
fn migrate_removed_catalog_definitions(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler) -> Option<()> {
2039+
// Collapse the legacy "Sample Polyline" wrapper network into the standalone `sample_polyline` proto.
2040+
// The proto now computes per-bezpath segment lengths inline, so the wrapper's separate `subpath_segment_lengths`
2041+
// and `Memo` nodes are no longer needed. The 7 user-facing inputs are positionally identical between the
2042+
// old wrapper and the new proto.
2043+
if let Some(DefinitionIdentifier::Network(name)) = document.network_interface.reference(node_id, network_path)
2044+
&& name == "Sample Polyline"
2045+
&& node.inputs.len() == 7
2046+
{
2047+
let mut node_template = resolve_proto_node_type(graphene_std::vector::sample_polyline::IDENTIFIER)?.default_node_template();
2048+
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
2049+
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
2050+
for (index, input) in old_inputs.iter().take(7).enumerate() {
2051+
document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path);
2052+
}
2053+
}
2054+
2055+
Some(())
2056+
}
2057+
20292058
#[cfg(test)]
20302059
mod tests {
20312060
use super::*;

node-graph/nodes/gcore/src/animation.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use core_types::table::Table;
22
use core_types::transform::Footprint;
3-
use core_types::uuid::NodeId;
43
use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
54
use glam::{DAffine2, DVec2};
65
use graphic_types::vector_types::GradientStops;
@@ -74,10 +73,7 @@ async fn quantize_real_time<T>(
7473
Context -> DAffine2,
7574
Context -> Footprint,
7675
Context -> DVec2,
77-
Context -> Vec<DVec2>,
78-
Context -> Vec<NodeId>,
7976
Context -> Vec<f64>,
80-
Context -> Vec<f32>,
8177
Context -> Vec<String>,
8278
Context -> Table<Vector>,
8379
Context -> Table<Graphic>,
@@ -117,10 +113,7 @@ async fn quantize_animation_time<T>(
117113
Context -> DAffine2,
118114
Context -> Footprint,
119115
Context -> DVec2,
120-
Context -> Vec<DVec2>,
121-
Context -> Vec<NodeId>,
122116
Context -> Vec<f64>,
123-
Context -> Vec<f32>,
124117
Context -> Vec<String>,
125118
Context -> Table<Vector>,
126119
Context -> Table<Graphic>,

node-graph/nodes/gcore/src/context_modification.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ async fn context_modification<T>(
3030
Context -> Option<NodeId>,
3131
Context -> Vec<NodeId>,
3232
Context -> Vec<f64>,
33-
Context -> Vec<f32>,
3433
Context -> Vec<String>,
3534
Context -> Table<Vector>,
3635
Context -> Table<Graphic>,

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 27 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,18 +1326,34 @@ pub async fn flatten_path<T: IntoGraphicTable + 'n + Send>(_: impl Ctx, #[implem
13261326
}
13271327

13281328
/// Convert vector geometry into a polyline composed of evenly spaced points.
1329-
#[node_macro::node(category(""), path(core_types::vector))]
1329+
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"))]
13301330
async fn sample_polyline(
13311331
_: impl Ctx,
13321332
content: Table<Vector>,
13331333
spacing: PointSpacingType,
1334-
#[unit(" px")] separation: f64,
1334+
#[default(100.)]
1335+
#[hard_min(0.)]
1336+
#[unit(" px")]
1337+
separation: f64,
1338+
#[default(100)]
1339+
#[hard_min(2)]
13351340
quantity: u32,
1336-
#[unit(" px")] start_offset: f64,
1337-
#[unit(" px")] stop_offset: f64,
1341+
#[hard_min(0.)]
1342+
#[unit(" px")]
1343+
start_offset: f64,
1344+
#[hard_min(0.)]
1345+
#[unit(" px")]
1346+
stop_offset: f64,
13381347
adaptive_spacing: bool,
1339-
subpath_segment_lengths: Vec<f64>,
13401348
) -> Table<Vector> {
1349+
let pathseg_perimeter = |segment: PathSeg| {
1350+
if is_linear(segment) {
1351+
Line::new(segment.start(), segment.end()).perimeter(DEFAULT_ACCURACY)
1352+
} else {
1353+
segment.perimeter(DEFAULT_ACCURACY)
1354+
}
1355+
};
1356+
13411357
content
13421358
.into_iter()
13431359
.map(|mut row| {
@@ -1351,27 +1367,14 @@ async fn sample_polyline(
13511367
// Transfer the stroke transform from the input vector content to the result.
13521368
result.style.set_stroke_transform(row.attribute_cloned_or_default("transform"));
13531369

1354-
// Using `stroke_bezpath_iter` so that the `subpath_segment_lengths` is aligned to the segments of each bezpath.
1355-
// So we can index into `subpath_segment_lengths` to get the length of the segments.
1356-
// NOTE: `subpath_segment_lengths` has precalulated lengths with transformation applied.
1357-
let bezpaths = row.element().stroke_bezpath_iter();
1358-
1359-
// Keeps track of the index of the first segment of the next bezpath in order to get lengths of all segments.
1360-
let mut next_segment_index = 0;
1361-
1362-
for local_bezpath in bezpaths {
1370+
for local_bezpath in row.element().stroke_bezpath_iter() {
13631371
// Apply the transform to compute sample locations in world space (for correct distance-based spacing)
13641372
let mut world_bezpath = local_bezpath.clone();
13651373
let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform");
13661374
world_bezpath.apply_affine(Affine::new(transform_attribute.to_cols_array()));
13671375

1368-
let segment_count = world_bezpath.segments().count();
1369-
1370-
// For the current bezpath we get its segment's length by calculating the start index and end index.
1371-
let current_bezpath_segments_length = &subpath_segment_lengths[next_segment_index..next_segment_index + segment_count];
1372-
1373-
// Increment the segment index by the number of segments in the current bezpath to calculate the next bezpath segment's length.
1374-
next_segment_index += segment_count;
1376+
// Per-segment perimeter lengths (transform-baked) for distance-based spacing
1377+
let segment_lengths: Vec<f64> = world_bezpath.segments().map(pathseg_perimeter).collect();
13751378

13761379
let amount = match spacing {
13771380
PointSpacingType::Separation => separation,
@@ -1380,9 +1383,7 @@ async fn sample_polyline(
13801383

13811384
// Compute sample locations using world-space distances, then evaluate positions on the untransformed bezpath.
13821385
// This avoids needing to invert the transform (which fails when the transform is singular, e.g. zero scale).
1383-
let Some((locations, was_closed)) =
1384-
bezpath_algorithms::compute_sample_locations(&world_bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length)
1385-
else {
1386+
let Some((locations, was_closed)) = bezpath_algorithms::compute_sample_locations(&world_bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, &segment_lengths) else {
13861387
continue;
13871388
};
13881389

@@ -1827,32 +1828,6 @@ async fn poisson_disk_points(
18271828
.collect()
18281829
}
18291830

1830-
#[node_macro::node(category(""), path(core_types::vector))]
1831-
async fn subpath_segment_lengths(_: impl Ctx, content: Table<Vector>) -> Vec<f64> {
1832-
let pathseg_perimeter = |segment: PathSeg| {
1833-
if is_linear(segment) {
1834-
Line::new(segment.start(), segment.end()).perimeter(DEFAULT_ACCURACY)
1835-
} else {
1836-
segment.perimeter(DEFAULT_ACCURACY)
1837-
}
1838-
};
1839-
1840-
content
1841-
.into_iter()
1842-
.flat_map(|vector| {
1843-
let transform: DAffine2 = vector.attribute_cloned_or_default("transform");
1844-
vector
1845-
.element()
1846-
.stroke_bezpath_iter()
1847-
.flat_map(|mut bezpath| {
1848-
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
1849-
bezpath.segments().map(pathseg_perimeter).collect::<Vec<f64>>()
1850-
})
1851-
.collect::<Vec<f64>>()
1852-
})
1853-
.collect()
1854-
}
1855-
18561831
#[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))]
18571832
async fn spline(_: impl Ctx, content: Table<Vector>) -> Table<Vector> {
18581833
content
@@ -3126,7 +3101,7 @@ mod test {
31263101
#[tokio::test]
31273102
async fn sample_polyline() {
31283103
let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
3129-
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 30., 0, 0., 0., false, vec![100.]).await;
3104+
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 30., 0, 0., 0., false).await;
31303105
let sample_polyline = sample_polyline.element(0).unwrap();
31313106
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
31323107
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
@@ -3136,7 +3111,7 @@ mod test {
31363111
#[tokio::test]
31373112
async fn sample_polyline_adaptive_spacing() {
31383113
let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
3139-
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 18., 0, 45., 10., true, vec![100.]).await;
3114+
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 18., 0, 45., 10., true).await;
31403115
let sample_polyline = sample_polyline.element(0).unwrap();
31413116
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
31423117
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
@@ -3163,12 +3138,6 @@ mod test {
31633138
}
31643139
}
31653140
#[tokio::test]
3166-
async fn segment_lengths() {
3167-
let bezpath = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
3168-
let lengths = subpath_segment_lengths(Footprint::default(), vector_node_from_bezpath(bezpath)).await;
3169-
assert_eq!(lengths, vec![100.]);
3170-
}
3171-
#[tokio::test]
31723141
async fn path_length() {
31733142
let bezpath = Rect::new(100., 100., 201., 201.).to_path(DEFAULT_ACCURACY);
31743143
let transform = DAffine2::from_scale(DVec2::new(2., 2.));

0 commit comments

Comments
 (0)