From e0df79ae05f73f26c7158863fa1207798cfb5dc8 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 20 Nov 2025 05:30:34 -0800 Subject: [PATCH 1/4] New nodes: 'Morph' and 'Multi-Morph' --- .../node_graph/document_node_definitions.rs | 199 +++++++++++++- .../document/node_graph/node_properties.rs | 46 ++++ .../messages/portfolio/document_migration.rs | 47 ++++ frontend/src/components/views/Graph.svelte | 4 +- node-graph/graph-craft/src/document.rs | 2 +- node-graph/libraries/core-types/src/ops.rs | 2 + .../libraries/graphic-types/src/graphic.rs | 66 +++++ .../libraries/no-std-types/src/registry.rs | 4 + node-graph/nodes/graphic/src/graphic.rs | 73 ++--- node-graph/nodes/vector/src/vector_nodes.rs | 259 ++++++++++-------- 10 files changed, 533 insertions(+), 169 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index eae239ff85..026f0f84ce 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -416,6 +416,201 @@ fn static_nodes() -> Vec { description: Cow::Borrowed("Creates a new Artboard which can be used as a working surface."), properties: None, }, + DocumentNodeDefinition { + identifier: "Multi-Morph", + category: "Vector", + // import[0] -> 0[0] + // import[1] -> 3[0] + // 0: Flatten Vector -> 1[0] + // -> 8[0] + // 1: Count Elements -> 2[0] + // 2: Subtract -> 7[1] + // 3: Floor -> 4[0] + // -> 9[1] + // 4: Subtract -> 6[1] + // 5: Instance Index -> 6[0] + // 6: Divide -> 7[0] + // 7: Multiply -> 8[1] + // 8: Morph -> 9[0] + // 9: Instance Repeat -> export[0] + node_template: NodeTemplate { + document_node: DocumentNode { + implementation: DocumentNodeImplementation::Network(NodeNetwork { + exports: vec![NodeInput::node(NodeId(9), 0)], + nodes: [ + // 0: Flatten Vector + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(graphic_nodes::graphic::flatten_vector::IDENTIFIER), + inputs: vec![NodeInput::import(generic!(T), 0)], + ..Default::default() + }, + // 1: Count Elements + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector::count_elements::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(0), 0)], + ..Default::default() + }, + // 2: Subtract + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::subtract::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::value(TaggedValue::F64(1.), false)], + ..Default::default() + }, + // 3: Floor + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::floor::IDENTIFIER), + inputs: vec![NodeInput::import(concrete!(f64), 1)], + ..Default::default() + }, + // 4: Subtract + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::subtract::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(3), 0), NodeInput::value(TaggedValue::F64(1.), false)], + ..Default::default() + }, + // 5: Instance Index + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_index::IDENTIFIER), + inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::value(TaggedValue::U32(0), false)], + ..Default::default() + }, + // 6: Divide + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::divide::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(5), 0), NodeInput::node(NodeId(4), 0)], + ..Default::default() + }, + // 7: Multiply + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::multiply::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(6), 0), NodeInput::node(NodeId(2), 0)], + ..Default::default() + }, + // 8: Morph + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector::morph::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(7), 0)], + ..Default::default() + }, + // 9: Instance Repeat + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_repeat::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(8), 0), NodeInput::node(NodeId(3), 0), NodeInput::value(TaggedValue::Bool(false), false)], + call_argument: generic!(T), + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), + ..Default::default() + }), + inputs: vec![NodeInput::value(TaggedValue::Vector(Default::default()), true), NodeInput::value(TaggedValue::F64(2.), false)], + ..Default::default() + }, + persistent_node_metadata: DocumentNodePersistentMetadata { + input_metadata: vec![("Content", "TODO").into(), ("Count", "TODO").into()], + output_names: vec!["Out".to_string()], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), + network_metadata: Some(NodeNetworkMetadata { + persistent_metadata: NodeNetworkPersistentMetadata { + node_metadata: [ + // 0: Flatten Vector + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)), + ..Default::default() + }, + ..Default::default() + }, + // 1: Count Elements + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)), + ..Default::default() + }, + ..Default::default() + }, + // 2: Subtract + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)), + ..Default::default() + }, + ..Default::default() + }, + // 3: Floor + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 6)), + ..Default::default() + }, + ..Default::default() + }, + // 4: Subtract + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 4)), + ..Default::default() + }, + ..Default::default() + }, + // 5: Instance Index + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 3)), + ..Default::default() + }, + ..Default::default() + }, + // 6: Divide + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 3)), + ..Default::default() + }, + ..Default::default() + }, + // 7: Multiply + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 3)), + ..Default::default() + }, + ..Default::default() + }, + // 8: Morph + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 2)), + ..Default::default() + }, + ..Default::default() + }, + // 9: Instance Repeat + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(35, 5)), + ..Default::default() + }, + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), + ..Default::default() + }, + ..Default::default() + }), + ..Default::default() + }, + }, + description: Cow::Borrowed("TODO"), + properties: None, + }, DocumentNodeDefinition { identifier: "Load Image", category: "Web Request", @@ -745,13 +940,13 @@ fn static_nodes() -> Vec { exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)], nodes: [ DocumentNode { - inputs: vec![NodeInput::import(concrete!(Table>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)], + inputs: vec![NodeInput::import(concrete!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::X), false)], implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER), call_argument: generic!(T), ..Default::default() }, DocumentNode { - inputs: vec![NodeInput::import(concrete!(Table>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)], + inputs: vec![NodeInput::import(concrete!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)], implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER), call_argument: generic!(T), ..Default::default() diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 6371d8d0e8..a0def2ebf3 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -154,6 +154,8 @@ pub(crate) fn property_from_type( Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(), Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(), Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(), + Some("Progression") => progression_widget(default_info, number_input.min(min(0.))).into(), + Some("SignedInteger") => number_widget(default_info, number_input.int()).into(), Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(), Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(), Some("PixelSize") => vec2_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false), @@ -793,6 +795,50 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec Vec { + let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; + + let mut widgets = start_widgets(parameter_widgets_info); + + let Some(document_node) = document_node else { return Vec::new() }; + let Some(input) = document_node.inputs.get(index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return vec![]; + }; + if let Some(&TaggedValue::F64(x)) = input.as_non_exposed_value() { + let whole_part = x.trunc(); + let fractional_part = x.fract(); + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + number_props + .clone() + .label("Progress") + .mode_range() + .min(0.) + .max(0.99999) + .value(Some(fractional_part)) + .on_update(update_value(move |input: &NumberInput| TaggedValue::F64(whole_part + input.value.unwrap()), node_id, index)) + .on_commit(commit_value) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + TextLabel::new("+").widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + number_props + .label("Element #") + .mode_increment() + .min(0.) + .is_integer(true) + .value(Some(whole_part)) + .on_update(update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap() + fractional_part), node_id, index)) + .on_commit(commit_value) + .widget_holder(), + ]) + } + widgets +} + pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: NumberInput) -> Vec { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 9b5d8dd86f..3ffe8a943e 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1582,6 +1582,53 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], } } + // Migrate from the old source/target "Morph" node to the new vector table based "Morph" node. + // This doesn't produce exactly equivalent results in cases involving input vector tables with multiple rows. + // The old version would zip the source and target table rows, interpoleating each pair together. + // The migrated version will instead deeply flatten both merged tables and morph sequentially between all source vectors and all target vector elements. + // This migration assumes most usages didn't involve multiple parallel vector elements, and instead morphed from a single source to a single target vector element. + if reference == "Morph" && inputs_count == 3 { + // Old signature: + // async fn morph(_: impl Ctx, source: Table, #[expose] target: Table, #[default(0.5)] time: Fraction) -> Table { ... } + // + // New signature: + // async fn morph(_: impl Ctx, content: #[implementations(Table, Table)] content: I, progression: Progression) -> Table { ... } + + let mut node_template = resolve_document_node_type(reference)?.default_node_template(); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + + // Create a new Merge node + let Some(merge_node_type) = resolve_document_node_type("Merge") else { + log::error!("Could not get merge node from definition when upgrading morph"); + return None; + }; + let merge_template = merge_node_type.default_node_template(); + let merge_node_id = NodeId::new(); + + // Decide on the placement position of the new Merge node + let Some(morph_position) = document.network_interface.position_from_downstream_node(node_id, network_path) else { + log::error!("Could not get position for morph node {node_id}"); + return None; + }; + let merge_position = morph_position + IVec2::new(-7, 0); + + // Insert the new Merge node into the network + document.network_interface.insert_node(merge_node_id, merge_template, network_path); + document.network_interface.set_to_node_or_layer(&merge_node_id, network_path, false); + document.network_interface.shift_absolute_node_position(&merge_node_id, merge_position, network_path); + + // Connect the old 'source' and 'target' inputs to the new Merge node + document.network_interface.set_input(&InputConnector::node(merge_node_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(merge_node_id, 1), old_inputs[1].clone(), network_path); + + // Connect the new Merge node to the 'content' input of the Morph node + document + .network_interface + .set_input(&InputConnector::node(*node_id, 0), NodeInput::node(merge_node_id, 0), network_path); + // Connect the old 'progression' input to the new 'progression' input of the Morph node + document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path); + } + // Add context features to nodes that don't have them (fine-grained context caching migration) if node.context_features == graphene_std::ContextDependencies::default() && let Some(reference) = document.network_interface.reference(node_id, network_path).cloned().flatten() diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 819d309229..99c509c4e1 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -505,7 +505,7 @@ style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} style:--layer-area-width={layerAreaWidth} style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0} - title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")} + title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}, Position: (${node.position.x}, ${node.position.y})` : "")} data-node={node.id} >
@@ -650,7 +650,7 @@ style:--clip-path-id={`url(#${clipPathId})`} style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`} style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} - title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")} + title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}, Position: (${node.position.x}, ${node.position.y})` : "")} data-node={node.id} > diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 4f84913fd4..722a81795d 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -848,7 +848,7 @@ impl NodeNetwork { // If the input to self is a node, connect the corresponding output of the inner network to it NodeInput::Node { node_id, output_index } => { nested_node.populate_first_network_input(node_id, output_index, nested_input_index, node.original_location.inputs(*import_index), 1); - let input_node = self.nodes.get_mut(&node_id).unwrap_or_else(|| panic!("unable find input node {node_id:?}")); + let input_node = self.nodes.get_mut(&node_id).unwrap_or_else(|| panic!("Unable to find input node {node_id:?}")); input_node.original_location.dependants[output_index].push(nested_node_id); } NodeInput::Import { import_index, .. } => { diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index d09d8194a0..9221f6dc6d 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -79,6 +79,8 @@ impl Convert for DVec2 { } } +// TODO: Add a DVec2 to Table anchor point conversion implementation to replace the 'Vec2 to Point' node + /// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types. macro_rules! impl_convert { ($from:ty, $to:ty) => { diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 45764287fd..d5db099745 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -119,6 +119,55 @@ impl From> for Graphic { // Local trait to convert types to Table (avoids orphan rule issues) pub trait IntoGraphicTable { fn into_graphic_table(self) -> Table; + + /// Deeply flattens any vector content within a graphic table, discarding non-vector content, and returning a table of only vector elements. + fn into_flattened_vector_table(self) -> Table + where + Self: std::marker::Sized, + { + let content = self.into_graphic_table(); + + // TODO: Avoid mutable reference, instead return a new Table? + fn flatten_table(output_vector_table: &mut Table, current_graphic_table: Table) { + for current_graphic_row in current_graphic_table.iter() { + let current_graphic = current_graphic_row.element.clone(); + let source_node_id = *current_graphic_row.source_node_id; + + match current_graphic { + // If we're allowed to recurse, flatten any tables we encounter + Graphic::Graphic(mut current_graphic_table) => { + // Apply the parent graphic's transform to all child elements + for graphic in current_graphic_table.iter_mut() { + *graphic.transform = *current_graphic_row.transform * *graphic.transform; + } + + flatten_table(output_vector_table, current_graphic_table); + } + // Push any leaf Vector elements we encounter + Graphic::Vector(vector_table) => { + for current_vector_row in vector_table.iter() { + output_vector_table.push(TableRow { + element: current_vector_row.element.clone(), + transform: *current_graphic_row.transform * *current_vector_row.transform, + alpha_blending: AlphaBlending { + blend_mode: current_vector_row.alpha_blending.blend_mode, + opacity: current_graphic_row.alpha_blending.opacity * current_vector_row.alpha_blending.opacity, + fill: current_vector_row.alpha_blending.fill, + clip: current_vector_row.alpha_blending.clip, + }, + source_node_id, + }); + } + } + _ => {} + } + } + } + + let mut output = Table::new(); + flatten_table(&mut output, content); + output + } } impl IntoGraphicTable for Table { @@ -284,6 +333,7 @@ impl RenderComplexity for Graphic { pub trait AtIndex { type Output; fn at_index(&self, index: usize) -> Option; + fn at_index_from_end(&self, index: usize) -> Option; } impl AtIndex for Vec { type Output = T; @@ -291,6 +341,10 @@ impl AtIndex for Vec { fn at_index(&self, index: usize) -> Option { self.get(index).cloned() } + + fn at_index_from_end(&self, index: usize) -> Option { + if index == 0 || index > self.len() { None } else { self.get(self.len() - index).cloned() } + } } impl AtIndex for Table { type Output = Table; @@ -304,6 +358,18 @@ impl AtIndex for Table { None } } + + fn at_index_from_end(&self, index: usize) -> Option { + let mut result_table = Self::default(); + if index == 0 || index > self.len() { + None + } else if let Some(row) = self.iter().nth(self.len() - index) { + result_table.push(row.into_cloned()); + Some(result_table) + } else { + None + } + } } // TODO: Eventually remove this migration document upgrade code diff --git a/node-graph/libraries/no-std-types/src/registry.rs b/node-graph/libraries/no-std-types/src/registry.rs index d1957c8d29..9fde16ff77 100644 --- a/node-graph/libraries/no-std-types/src/registry.rs +++ b/node-graph/libraries/no-std-types/src/registry.rs @@ -19,6 +19,10 @@ pub mod types { pub type Length = f64; /// 0 to 1 pub type Fraction = f64; + /// Non-negative number broken into whole and fractional parts + pub type Progression = f64; + /// Signed integer that's actually a float because we don't handle type conversions very well yet + pub type SignedInteger = f64; /// Unsigned integer pub type IntegerCount = u32; /// Unsigned integer to be used for random seeds diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index effbe25586..e193fc0eed 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -1,15 +1,11 @@ use core_types::Color; -use core_types::{ - Ctx, - blending::AlphaBlending, - table::{Table, TableRow}, - uuid::NodeId, -}; +use core_types::Ctx; +use core_types::registry::types::SignedInteger; +use core_types::table::{Table, TableRow}; +use core_types::uuid::NodeId; use glam::{DAffine2, DVec2}; -use graphic_types::{ - Artboard, Vector, - graphic::{Graphic, IntoGraphicTable}, -}; +use graphic_types::graphic::{Graphic, IntoGraphicTable}; +use graphic_types::{Artboard, Vector}; use raster_types::{CPU, GPU, Raster}; use vector_types::GradientStops; @@ -164,48 +160,8 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table, fully_flatten /// Converts a graphic table into a vector table by deeply flattening any vector content it contains, and discarding any non-vector content. #[node_macro::node(category("Vector"))] -pub async fn flatten_vector(_: impl Ctx, content: Table) -> Table { - // TODO: Avoid mutable reference, instead return a new Table? - fn flatten_table(output_vector_table: &mut Table, current_graphic_table: Table) { - for current_graphic_row in current_graphic_table.iter() { - let current_graphic = current_graphic_row.element.clone(); - let source_node_id = *current_graphic_row.source_node_id; - - match current_graphic { - // If we're allowed to recurse, flatten any tables we encounter - Graphic::Graphic(mut current_graphic_table) => { - // Apply the parent graphic's transform to all child elements - for graphic in current_graphic_table.iter_mut() { - *graphic.transform = *current_graphic_row.transform * *graphic.transform; - } - - flatten_table(output_vector_table, current_graphic_table); - } - // Push any leaf Vector elements we encounter - Graphic::Vector(vector_table) => { - for current_vector_row in vector_table.iter() { - output_vector_table.push(TableRow { - element: current_vector_row.element.clone(), - transform: *current_graphic_row.transform * *current_vector_row.transform, - alpha_blending: AlphaBlending { - blend_mode: current_vector_row.alpha_blending.blend_mode, - opacity: current_graphic_row.alpha_blending.opacity * current_vector_row.alpha_blending.opacity, - fill: current_vector_row.alpha_blending.fill, - clip: current_vector_row.alpha_blending.clip, - }, - source_node_id, - }); - } - } - _ => {} - } - } - } - - let mut output = Table::new(); - flatten_table(&mut output, content); - - output +pub async fn flatten_vector(_: impl Ctx, #[implementations(Table, Table)] content: I) -> Table { + content.into_flattened_vector_table() } /// Returns the value at the specified index in the collection. @@ -229,11 +185,18 @@ pub fn index_elements( Table, )] collection: T, - /// The index of the item to retrieve, starting from 0 for the first item. - index: u32, + /// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the collection, starting from -1 for the last item. + index: SignedInteger, ) -> T::Output where T::Output: Clone + Default, { - collection.at_index(index as usize).unwrap_or_default() + let index = index as i32; + + if index < 0 { + collection.at_index_from_end(-index as usize) + } else { + collection.at_index(index as usize) + } + .unwrap_or_default() } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 7b889cd2b2..0e069cb21b 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1,7 +1,7 @@ use core::f64::consts::PI; use core::hash::{Hash, Hasher}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; -use core_types::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue}; +use core_types::registry::types::{Angle, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, Progression, SeedValue}; use core_types::table::{Table, TableRow, TableRowMut}; use core_types::transform::{Footprint, Transform}; use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl}; @@ -825,6 +825,7 @@ async fn dimensions(_: impl Ctx, content: Table) -> DVec2 { .unwrap_or_default() } +// TODO: Replace this node with an automatic type conversion implementation of the `Convert` trait /// Converts a vec2 value into a vector path composed of a single anchor point. /// /// This is useful in conjunction with nodes that repeat it, followed by the "Points to Polyline" node to string together a path of the points. @@ -1146,7 +1147,17 @@ async fn sample_polyline( /// /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] -async fn cut_path(_: impl Ctx, mut content: Table, progression: Fraction, parameterized_distance: bool, reverse: bool) -> Table { +async fn cut_path( + _: impl Ctx, + /// The path to insert a cut into. + mut content: Table, + /// The factor from the start to the end of the path, 0–1 for one subpath, 1–2 for a second subpath, and so on. + progression: Progression, + /// Swap the direction of the path. + reverse: bool, + /// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances. + parameterized_distance: bool, +) -> Table { let euclidian = !parameterized_distance; let bezpaths = content @@ -1251,7 +1262,7 @@ async fn position_on_path( /// The path to traverse. content: Table, /// The factor from the start to the end of the path, 0–1 for one subpath, 1–2 for a second subpath, and so on. - progression: Fraction, + progression: Progression, /// Swap the direction of the path. reverse: bool, /// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances. @@ -1290,7 +1301,7 @@ async fn tangent_on_path( /// The path to traverse. content: Table, /// The factor from the start to the end of the path, 0–1 for one subpath, 1–2 for a second subpath, and so on. - progression: Fraction, + progression: Progression, /// Swap the direction of the path. reverse: bool, /// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances. @@ -1512,8 +1523,18 @@ async fn jitter_points( .collect() } +/// Interpolates the geometry and styles between multiple vector layers, producing a single morphed vector shape. +/// +/// Based on the progression value, adjacent vector elements are blended together. From 0 until 1, the first element (bottom layer) morphs into the second element (next layer up). From 1 until 2, it then morphs into the third element, and so on until progression is capped at the last element (top layer). #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn morph(_: impl Ctx, source: Table, #[expose] target: Table, #[default(0.5)] time: Fraction) -> Table { +async fn morph( + _: impl Ctx, + /// The vector elements to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements. + #[implementations(Table, Table)] + content: I, + /// The factor from one vector element to the next in sequence. The whole number part selects the source element, and the decimal part determines the interpolation amount towards the next element. + progression: Progression, +) -> Table { /// Subdivides the last segment of the bezpath to until it appends 'count' number of segments. fn make_new_segments(bezpath: &mut BezPath, count: usize) { let bezpath_segment_count = bezpath.segments().count(); @@ -1557,127 +1578,147 @@ async fn morph(_: impl Ctx, source: Table, #[expose] target: Table, we convert it into one by flattening any Table content. + let content = content.into_flattened_vector_table(); - // Lerp styles - let vector_alpha_blending = source_row.alpha_blending.lerp(&target_row.alpha_blending, time as f32); - vector.style = source_row.element.style.lerp(&target_row.element.style, time); + // Determine source and target indices and interpolation time fraction + let progression = progression.max(0.); + let source_index = progression.floor() as usize; + let time = progression.fract(); - // Before and after transforms - let source_transform = source_row.transform; - let target_transform = target_row.transform; + // Not enough elements to interpolate between, so we return the input as-is + if content.len() <= 1 { + return content; + } + // Progression is at or past the last element, so we return the last element without interpolation + if source_index >= content.len() - 1 { + return content.into_iter().last().into_iter().collect(); + } - // Before and after paths - let source_bezpaths = source_row.element.stroke_bezpath_iter(); - let target_bezpaths = target_row.element.stroke_bezpath_iter(); + // Interpolation between two elements + let mut content_iter = content.into_iter(); + let source_row = content_iter.nth(source_index).unwrap(); + let target_row = content_iter.next().unwrap(); - for (mut source_bezpath, mut target_bezpath) in source_bezpaths.zip(target_bezpaths) { - if source_bezpath.elements().is_empty() || target_bezpath.elements().is_empty() { - continue; - } + let mut vector = Vector { + upstream_data: Some(graphic_table_content), + ..Default::default() + }; - source_bezpath.apply_affine(Affine::new(source_transform.to_cols_array())); - target_bezpath.apply_affine(Affine::new(target_transform.to_cols_array())); - - let target_segment_len = target_bezpath.segments().count(); - let source_segment_len = source_bezpath.segments().count(); - - // Insert new segments to align the number of segments in sorce_bezpath and target_bezpath. - make_new_segments(&mut source_bezpath, target_segment_len.max(source_segment_len) - source_segment_len); - make_new_segments(&mut target_bezpath, source_segment_len.max(target_segment_len) - target_segment_len); - - let source_segments = source_bezpath.segments().collect::>(); - let target_segments = target_bezpath.segments().collect::>(); - - // Interpolate anchors and handles - for (i, (source_element, target_element)) in source_bezpath.elements_mut().iter_mut().zip(target_bezpath.elements_mut().iter_mut()).enumerate() { - match source_element { - PathEl::MoveTo(point) => *point = point.lerp(target_element.end_point().unwrap(), time), - PathEl::ClosePath => {} - elm => { - let mut source_segment = source_segments.get(i - 1).unwrap().to_cubic(); - let target_segment = target_segments.get(i - 1).unwrap().to_cubic(); - source_segment.p0 = source_segment.p0.lerp(target_segment.p0, time); - source_segment.p1 = source_segment.p1.lerp(target_segment.p1, time); - source_segment.p2 = source_segment.p2.lerp(target_segment.p2, time); - source_segment.p3 = source_segment.p3.lerp(target_segment.p3, time); - *elm = PathSeg::Cubic(source_segment).as_path_el(); - } - } - } + // Lerp styles + let vector_alpha_blending = source_row.alpha_blending.lerp(&target_row.alpha_blending, time as f32); + vector.style = source_row.element.style.lerp(&target_row.element.style, time); - vector.append_bezpath(source_bezpath.clone()); + // Before and after transforms + let source_transform = source_row.transform; + let target_transform = target_row.transform; + + // Before and after paths + let source_bezpaths = source_row.element.stroke_bezpath_iter(); + let target_bezpaths = target_row.element.stroke_bezpath_iter(); + + for (mut source_bezpath, mut target_bezpath) in source_bezpaths.zip(target_bezpaths) { + if source_bezpath.elements().is_empty() || target_bezpath.elements().is_empty() { + continue; + } + + source_bezpath.apply_affine(Affine::new(source_transform.to_cols_array())); + target_bezpath.apply_affine(Affine::new(target_transform.to_cols_array())); + + let target_segment_len = target_bezpath.segments().count(); + let source_segment_len = source_bezpath.segments().count(); + + // Insert new segments to align the number of segments in sorce_bezpath and target_bezpath. + make_new_segments(&mut source_bezpath, target_segment_len.max(source_segment_len) - source_segment_len); + make_new_segments(&mut target_bezpath, source_segment_len.max(target_segment_len) - target_segment_len); + + let source_segments = source_bezpath.segments().collect::>(); + let target_segments = target_bezpath.segments().collect::>(); + + // Interpolate anchors and handles + for (i, (source_element, target_element)) in source_bezpath.elements_mut().iter_mut().zip(target_bezpath.elements_mut().iter_mut()).enumerate() { + match source_element { + PathEl::MoveTo(point) => *point = point.lerp(target_element.end_point().unwrap(), time), + PathEl::ClosePath => {} + elm => { + let mut source_segment = source_segments.get(i - 1).unwrap().to_cubic(); + let target_segment = target_segments.get(i - 1).unwrap().to_cubic(); + source_segment.p0 = source_segment.p0.lerp(target_segment.p0, time); + source_segment.p1 = source_segment.p1.lerp(target_segment.p1, time); + source_segment.p2 = source_segment.p2.lerp(target_segment.p2, time); + source_segment.p3 = source_segment.p3.lerp(target_segment.p3, time); + *elm = PathSeg::Cubic(source_segment).as_path_el(); + } } + } - // Deal with unmatched extra paths by collapsing them - let source_paths_count = source_row.element.stroke_bezpath_iter().count(); - let target_paths_count = target_row.element.stroke_bezpath_iter().count(); - let source_paths = source_row.element.stroke_bezpath_iter().skip(target_paths_count); - let target_paths = target_row.element.stroke_bezpath_iter().skip(source_paths_count); - - for mut source_path in source_paths { - source_path.apply_affine(Affine::new(source_transform.to_cols_array())); - - // Skip if the path has no segments else get the point at the end of the path. - let Some(end) = source_path.segments().last().map(|element| element.end()) else { continue }; - - for element in source_path.elements_mut() { - match element { - PathEl::MoveTo(point) => *point = point.lerp(end, time), - PathEl::LineTo(point) => *point = point.lerp(end, time), - PathEl::QuadTo(point, point1) => { - *point = point.lerp(end, time); - *point1 = point1.lerp(end, time); - } - PathEl::CurveTo(point, point1, point2) => { - *point = point.lerp(end, time); - *point1 = point1.lerp(end, time); - *point2 = point2.lerp(end, time); - } - PathEl::ClosePath => {} - } + vector.append_bezpath(source_bezpath.clone()); + } + + // Deal with unmatched extra paths by collapsing them + let source_paths_count = source_row.element.stroke_bezpath_iter().count(); + let target_paths_count = target_row.element.stroke_bezpath_iter().count(); + let source_paths = source_row.element.stroke_bezpath_iter().skip(target_paths_count); + let target_paths = target_row.element.stroke_bezpath_iter().skip(source_paths_count); + + for mut source_path in source_paths { + source_path.apply_affine(Affine::new(source_transform.to_cols_array())); + + // Skip if the path has no segments else get the point at the end of the path. + let Some(end) = source_path.segments().last().map(|element| element.end()) else { continue }; + + for element in source_path.elements_mut() { + match element { + PathEl::MoveTo(point) => *point = point.lerp(end, time), + PathEl::LineTo(point) => *point = point.lerp(end, time), + PathEl::QuadTo(point, point1) => { + *point = point.lerp(end, time); + *point1 = point1.lerp(end, time); + } + PathEl::CurveTo(point, point1, point2) => { + *point = point.lerp(end, time); + *point1 = point1.lerp(end, time); + *point2 = point2.lerp(end, time); } - vector.append_bezpath(source_path); + PathEl::ClosePath => {} } + } + vector.append_bezpath(source_path); + } - for mut target_path in target_paths { - target_path.apply_affine(Affine::new(source_transform.to_cols_array())); + for mut target_path in target_paths { + target_path.apply_affine(Affine::new(source_transform.to_cols_array())); - // Skip if the path has no segments else get the point at the start of the path. - let Some(start) = target_path.segments().next().map(|element| element.start()) else { continue }; + // Skip if the path has no segments else get the point at the start of the path. + let Some(start) = target_path.segments().next().map(|element| element.start()) else { continue }; - for element in target_path.elements_mut() { - match element { - PathEl::MoveTo(point) => *point = start.lerp(*point, time), - PathEl::LineTo(point) => *point = start.lerp(*point, time), - PathEl::QuadTo(point, point1) => { - *point = start.lerp(*point, time); - *point1 = start.lerp(*point1, time); - } - PathEl::CurveTo(point, point1, point2) => { - *point = start.lerp(*point, time); - *point1 = start.lerp(*point1, time); - *point2 = start.lerp(*point2, time); - } - PathEl::ClosePath => {} - } + for element in target_path.elements_mut() { + match element { + PathEl::MoveTo(point) => *point = start.lerp(*point, time), + PathEl::LineTo(point) => *point = start.lerp(*point, time), + PathEl::QuadTo(point, point1) => { + *point = start.lerp(*point, time); + *point1 = start.lerp(*point1, time); + } + PathEl::CurveTo(point, point1, point2) => { + *point = start.lerp(*point, time); + *point1 = start.lerp(*point1, time); + *point2 = start.lerp(*point2, time); } - vector.append_bezpath(target_path); + PathEl::ClosePath => {} } + } + vector.append_bezpath(target_path); + } - TableRow { - element: vector, - alpha_blending: vector_alpha_blending, - ..Default::default() - } - }) - .collect() + Table::new_from_row(TableRow { + element: vector, + alpha_blending: vector_alpha_blending, + ..Default::default() + }) } fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector { From 0932265f57d4e7923abbf97b25800717a7340b53 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 23 Nov 2025 12:55:24 -0800 Subject: [PATCH 2/4] Blend Shapes node --- .../data_panel/data_panel_message_handler.rs | 11 +- .../node_graph/document_node_definitions.rs | 468 +++++++++++++++--- .../libraries/core-types/src/context.rs | 2 +- .../nodes/transform/src/transform_nodes.rs | 79 ++- node-graph/nodes/vector/src/vector_nodes.rs | 11 +- 5 files changed, 486 insertions(+), 85 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 66dd9f025d..c1a7dfddf9 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -502,10 +502,17 @@ impl TableRowLayout for Raster { format!("Raster ({}x{})", self.width, self.height) } fn element_page(&self, _data: &mut LayoutData) -> Vec { - let base64_string = self.data().base64_string.clone().unwrap_or_else(|| { + let raster = self.data(); + + if raster.width == 0 || raster.height == 0 { + let widgets = vec![TextLabel::new("Image has no area").widget_holder()]; + return vec![LayoutGroup::Row { widgets }]; + } + + let base64_string = raster.base64_string.clone().unwrap_or_else(|| { use base64::Engine; - let output = self.data().to_png(); + let output = raster.to_png(); let preamble = "data:image/png;base64,"; let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4); base64_string.push_str(preamble); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 026f0f84ce..bc1e9d3af1 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -417,86 +417,184 @@ fn static_nodes() -> Vec { properties: None, }, DocumentNodeDefinition { - identifier: "Multi-Morph", + identifier: "Blend Shapes", category: "Vector", - // import[0] -> 0[0] - // import[1] -> 3[0] - // 0: Flatten Vector -> 1[0] - // -> 8[0] - // 1: Count Elements -> 2[0] - // 2: Subtract -> 7[1] - // 3: Floor -> 4[0] - // -> 9[1] - // 4: Subtract -> 6[1] - // 5: Instance Index -> 6[0] - // 6: Divide -> 7[0] - // 7: Multiply -> 8[1] - // 8: Morph -> 9[0] - // 9: Instance Repeat -> export[0] + // [IMPORTS]2 -> 0[0:Floor] + // [0:Floor]0 -> 0[1:Subtract] + // "1: f64" -> 1[1:Subtract] + // "(): ()" -> 0[2:Instance Index] + // "0: u32" -> 1[2:Instance Index] + // [2:Instance Index]0 -> 0[3:Divide] + // [1:Subtract]0 -> 1[3:Divide] + // [IMPORTS]1 -> 0[4:Position on Path] + // [3:Divide]0 -> 1[4:Position on Path] + // "false: bool" -> 2[4:Position on Path] + // "false: bool" -> 3[4:Position on Path] + // "(): ()" -> 0[5:Instance Vector] + // [5:Instance Vector]0 -> 0[6:Reset Transform] + // "true: bool" -> 1[6:Reset Transform] + // "false: bool" -> 2[6:Reset Transform] + // "false: bool" -> 3[6:Reset Transform] + // [12:Flatten Vector]0 -> 0[7:Instance Map] + // [6:Reset Transform]0 -> 1[7:Instance Map] + // [7:Instance Map]0 -> 0[8:Morph] + // [15:Multiply]0 -> 1[8:Morph] + // [8:Morph]0 -> 0[9:Transform] + // [4:Position on Path]0 -> 1[9:Transform] + // "0: f64" -> 2[9:Transform] + // "(0, 0): DVec2" -> 3[9:Transform] + // "(0, 0): DVec2" -> 4[9:Transform] + // [IMPORTS]1 -> 0[10:Count Points] + // [10:Count Points]0 -> 0[11:Equals] + // [13:Count Elements]0 -> 1[11:Equals] + // [IMPORTS]0 -> 0[12:Flatten Vector] + // [12:Flatten Vector]0 -> 0[13:Count Elements] + // [13:Count Elements]0 -> 0[14:Subtract] + // "1: f64" -> 1[14:Subtract] + // [3:Divide]0 -> 0[15:Multiply] + // [14:Subtract]0 -> 1[15:Multiply] + // [12:Flatten Vector]0 -> 0[16:Morph] + // [15:Multiply]0 -> 1[16:Morph] + // [11:Equals]0 -> 0[17:Switch] + // [9:Transform]0 -> 1[17:Switch] + // [16:Morph]0 -> 2[17:Switch] + // [17:Switch]0 -> 0[18:Instance Repeat] + // [0:Floor]0 -> 1[18:Instance Repeat] + // [IMPORTS]3 -> 2[18:Instance Repeat] + // [18:Instance Repeat]0 -> 0[EXPORTS] node_template: NodeTemplate { document_node: DocumentNode { implementation: DocumentNodeImplementation::Network(NodeNetwork { - exports: vec![NodeInput::node(NodeId(9), 0)], + exports: vec![NodeInput::node(NodeId(18), 0)], nodes: [ - // 0: Flatten Vector + // 0: Floor DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(graphic_nodes::graphic::flatten_vector::IDENTIFIER), - inputs: vec![NodeInput::import(generic!(T), 0)], + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::floor::IDENTIFIER), + inputs: vec![NodeInput::import(concrete!(f64), 2)], ..Default::default() }, - // 1: Count Elements + // 1: Subtract DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(vector::count_elements::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(0), 0)], + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::subtract::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(1.), false)], ..Default::default() }, - // 2: Subtract + // 2: Instance Index DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(math_nodes::subtract::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::value(TaggedValue::F64(1.), false)], + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_index::IDENTIFIER), + inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::value(TaggedValue::U32(0), false)], ..Default::default() }, - // 3: Floor + // 3: Divide DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(math_nodes::floor::IDENTIFIER), - inputs: vec![NodeInput::import(concrete!(f64), 1)], + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::divide::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(1), 0)], ..Default::default() }, - // 4: Subtract + // 4: Position on Path DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(math_nodes::subtract::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(3), 0), NodeInput::value(TaggedValue::F64(1.), false)], + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::position_on_path::IDENTIFIER), + inputs: vec![ + NodeInput::import(generic!(T), 1), + NodeInput::node(NodeId(3), 0), + NodeInput::value(TaggedValue::Bool(false), false), + NodeInput::value(TaggedValue::Bool(false), false), + ], ..Default::default() }, - // 5: Instance Index + // 5: Instance Vector DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_index::IDENTIFIER), - inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::value(TaggedValue::U32(0), false)], + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_vector::IDENTIFIER), + inputs: vec![NodeInput::value(TaggedValue::None, false)], ..Default::default() }, - // 6: Divide + // 6: Reset Transform DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(math_nodes::divide::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(5), 0), NodeInput::node(NodeId(4), 0)], + implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::reset_transform::IDENTIFIER), + inputs: vec![ + NodeInput::node(NodeId(5), 0), + NodeInput::value(TaggedValue::Bool(true), false), + NodeInput::value(TaggedValue::Bool(false), false), + NodeInput::value(TaggedValue::Bool(false), false), + ], ..Default::default() }, - // 7: Multiply + // 7: Instance Map DocumentNode { - implementation: DocumentNodeImplementation::ProtoNode(math_nodes::multiply::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(6), 0), NodeInput::node(NodeId(2), 0)], + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_map::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(12), 0), NodeInput::node(NodeId(6), 0)], ..Default::default() }, // 8: Morph DocumentNode { implementation: DocumentNodeImplementation::ProtoNode(vector::morph::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(7), 0)], + inputs: vec![NodeInput::node(NodeId(7), 0), NodeInput::node(NodeId(15), 0)], + ..Default::default() + }, + // 9: Transform + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::transform::IDENTIFIER), + inputs: vec![ + NodeInput::node(NodeId(8), 0), + NodeInput::node(NodeId(4), 0), + NodeInput::value(TaggedValue::F64(0.), false), + NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false), + NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false), + ], + ..Default::default() + }, + // 10: Count Points + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::count_points::IDENTIFIER), + inputs: vec![NodeInput::import(generic!(T), 1)], + ..Default::default() + }, + // 11: Equals + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::equals::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(10), 0), NodeInput::node(NodeId(13), 0)], + ..Default::default() + }, + // 12: Flatten Vector + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(graphic_nodes::graphic::flatten_vector::IDENTIFIER), + inputs: vec![NodeInput::import(generic!(T), 0)], + ..Default::default() + }, + // 13: Count Elements + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector::count_elements::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(12), 0)], + ..Default::default() + }, + // 14: Subtract + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::subtract::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(13), 0), NodeInput::value(TaggedValue::F64(1.), false)], + ..Default::default() + }, + // 15: Multiply + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(math_nodes::multiply::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(3), 0), NodeInput::node(NodeId(14), 0)], + ..Default::default() + }, + // 16: Morph + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector::morph::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(12), 0), NodeInput::node(NodeId(15), 0)], + ..Default::default() + }, + // 17: Switch + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(logic::switch::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(11), 0), NodeInput::node(NodeId(9), 0), NodeInput::node(NodeId(16), 0)], ..Default::default() }, - // 9: Instance Repeat + // 18: Instance Repeat DocumentNode { implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_repeat::IDENTIFIER), - inputs: vec![NodeInput::node(NodeId(8), 0), NodeInput::node(NodeId(3), 0), NodeInput::value(TaggedValue::Bool(false), false)], - call_argument: generic!(T), + inputs: vec![NodeInput::node(NodeId(17), 0), NodeInput::node(NodeId(0), 0), NodeInput::import(generic!(T), 3)], ..Default::default() }, ] @@ -506,76 +604,81 @@ fn static_nodes() -> Vec { .collect(), ..Default::default() }), - inputs: vec![NodeInput::value(TaggedValue::Vector(Default::default()), true), NodeInput::value(TaggedValue::F64(2.), false)], + inputs: vec![ + NodeInput::value(TaggedValue::Vector(Default::default()), true), + NodeInput::value(TaggedValue::Vector(Default::default()), true), + NodeInput::value(TaggedValue::F64(10.), false), + NodeInput::value(TaggedValue::Bool(Default::default()), false), + ], ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_metadata: vec![("Content", "TODO").into(), ("Count", "TODO").into()], + input_metadata: vec![("Content", "TODO").into(), ("Path", "TODO").into(), ("Count", "TODO").into(), ("Reverse", "TODO").into()], output_names: vec!["Out".to_string()], node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { node_metadata: [ - // 0: Flatten Vector + // 0: Floor DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), ..Default::default() }, ..Default::default() }, - // 1: Count Elements + // 1: Subtract DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, -1)), ..Default::default() }, ..Default::default() }, - // 2: Subtract + // 2: Instance Index DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, -2)), ..Default::default() }, ..Default::default() }, - // 3: Floor + // 3: Divide DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 6)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, -2)), ..Default::default() }, ..Default::default() }, - // 4: Subtract + // 4: Position on Path DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 4)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, -3)), ..Default::default() }, ..Default::default() }, - // 5: Instance Index + // 5: Instance Vector DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 3)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 2)), ..Default::default() }, ..Default::default() }, - // 6: Divide + // 6: Reset Transform DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 3)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 2)), ..Default::default() }, ..Default::default() }, - // 7: Multiply + // 7: Instance Map DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 3)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 1)), ..Default::default() }, ..Default::default() @@ -583,15 +686,250 @@ fn static_nodes() -> Vec { // 8: Morph DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 2)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 1)), + ..Default::default() + }, + ..Default::default() + }, + // 9: Transform + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(35, 1)), + ..Default::default() + }, + ..Default::default() + }, + // 10: Count Points + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 4)), + ..Default::default() + }, + ..Default::default() + }, + // 11: Equals + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 4)), + ..Default::default() + }, + ..Default::default() + }, + // 12: Flatten Vector + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 6)), + ..Default::default() + }, + ..Default::default() + }, + // 13: Count Elements + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 8)), + ..Default::default() + }, + ..Default::default() + }, + // 14: Subtract + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 8)), + ..Default::default() + }, + ..Default::default() + }, + // 15: Multiply + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 7)), + ..Default::default() + }, + ..Default::default() + }, + // 16: Morph + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 6)), + ..Default::default() + }, + ..Default::default() + }, + // 17: Switch + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(42, 4)), + ..Default::default() + }, + ..Default::default() + }, + // 18: Instance Repeat + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(49, -1)), + ..Default::default() + }, + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), + ..Default::default() + }, + ..Default::default() + }), + ..Default::default() + }, + }, + description: Cow::Borrowed("TODO"), + properties: None, + }, + DocumentNodeDefinition { + identifier: "Origins to Polyline", + category: "Vector", + // "(): ()" -> 0[0:Instance Vector] + // [0:Instance Vector]0 -> 0[1:Extract Transform] + // [1:Extract Transform]0 -> 0[2:Decompose Translation] + // [2:Decompose Translation]0 -> 0[3:Vec2 to Point] + // [IMPORTS]0 -> 0[4:Flatten Vector] + // [4:Flatten Vector]0 -> 0[5:Instance Map] + // [3:Vec2 to Point]0 -> 1[5:Instance Map] + // [5:Instance Map]0 -> 0[6: Flatten Path] + // [6:Flatten Path]0 -> 0[7:Points to Polyline] + // "false: bool" -> 1[7:Points to Polyline] + // [7:Points to Polyline]0 -> 0[EXPORTS] + node_template: NodeTemplate { + document_node: DocumentNode { + implementation: DocumentNodeImplementation::Network(NodeNetwork { + exports: vec![NodeInput::node(NodeId(7), 0)], + nodes: [ + // 0: Instance Vector + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_vector::IDENTIFIER), + inputs: vec![NodeInput::value(TaggedValue::None, false)], + ..Default::default() + }, + // 1: Extract Transform + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::extract_transform::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(0), 0)], + ..Default::default() + }, + // 2: Decompose Translation + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::decompose_translation::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(1), 0)], + ..Default::default() + }, + // 3: Vec2 to Point + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::vec_2_to_point::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(2), 0)], + ..Default::default() + }, + // 4: Flatten Vector + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(graphic_nodes::graphic::flatten_vector::IDENTIFIER), + inputs: vec![NodeInput::import(generic!(T), 0)], + ..Default::default() + }, + // 5: Instance Map + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_map::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(4), 0), NodeInput::node(NodeId(3), 0)], + ..Default::default() + }, + // 6: Flatten Path + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector::flatten_path::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(5), 0)], + ..Default::default() + }, + // 7: Points to Polyline + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(vector::points_to_polyline::IDENTIFIER), + inputs: vec![NodeInput::node(NodeId(6), 0), NodeInput::value(TaggedValue::Bool(false), false)], + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), + ..Default::default() + }), + inputs: vec![NodeInput::value(TaggedValue::Vector(Default::default()), true)], + ..Default::default() + }, + persistent_node_metadata: DocumentNodePersistentMetadata { + input_metadata: vec![("Vector", "TODO").into()], + output_names: vec!["Vector".to_string()], + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), + network_metadata: Some(NodeNetworkMetadata { + persistent_metadata: NodeNetworkPersistentMetadata { + node_metadata: [ + // 0: Instance Vector + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 1)), + ..Default::default() + }, + ..Default::default() + }, + // 1: Extract Transform + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 1)), + ..Default::default() + }, + ..Default::default() + }, + // 2: Decompose Transform + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 1)), + ..Default::default() + }, + ..Default::default() + }, + // 3: Vec2 to Point + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 1)), + ..Default::default() + }, + ..Default::default() + }, + // 4: Flatten Vector + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 0)), + ..Default::default() + }, + ..Default::default() + }, + // 5: Instance Map + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 0)), + ..Default::default() + }, + ..Default::default() + }, + // 6: Flatten Path + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(35, 0)), ..Default::default() }, ..Default::default() }, - // 9: Instance Repeat + // 7: Points to Polyline DocumentNodeMetadata { persistent_metadata: DocumentNodePersistentMetadata { - node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(35, 5)), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(42, 0)), ..Default::default() }, ..Default::default() diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index f9891a9f37..7918c9750a 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -305,7 +305,7 @@ impl ExtractAnimationTime for OwnedContextImpl { } impl ExtractIndex for OwnedContextImpl { fn try_index(&self) -> Option> { - self.index.clone().map(|x| x.into_iter()) + self.index.clone().map(|x| x.into_iter().rev()) } } impl ExtractVarArgs for OwnedContextImpl { diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 431bd2ea32..9cfeab5e16 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -3,7 +3,7 @@ use core_types::color::Color; use core_types::table::Table; use core_types::transform::{ApplyTransform, Transform}; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl}; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Graphic; use graphic_types::Vector; use graphic_types::raster_types::{CPU, GPU, Raster}; @@ -16,14 +16,14 @@ async fn transform( #[implementations( Context -> DAffine2, Context -> DVec2, - Context -> Table, Context -> Table, + Context -> Table, Context -> Table>, Context -> Table>, Context -> Table, Context -> Table, )] - value: impl Node, Output = T>, + content: impl Node, Output = T>, translation: DVec2, rotation: f64, scale: DVec2, @@ -41,24 +41,75 @@ async fn transform( ctx = ctx.with_footprint(footprint); } - let mut transform_target = value.eval(ctx.into_context()).await; + let mut transform_target = content.eval(ctx.into_context()).await; transform_target.left_apply_transform(&matrix); transform_target } +/// Resets the desired components of the input transform to their default values. If all components are reset, the output will be set to the identity transform. +/// Shear is represented jointly by rotation and scale, so resetting both will also remove any shear. +#[node_macro::node(category("Math: Transform"))] +fn reset_transform( + _: impl Ctx, + #[implementations( + Table, + Table, + Table>, + Table>, + Table, + Table, + )] + mut content: Table, + #[default(true)] reset_translation: bool, + reset_rotation: bool, + reset_scale: bool, +) -> Table { + for row in content.iter_mut() { + // Translation + if reset_translation { + row.transform.translation = DVec2::ZERO; + } + // (Rotation, Scale) + match (reset_rotation, reset_scale) { + (true, true) => { + row.transform.matrix2 = DMat2::IDENTITY; + } + (true, false) => { + let scale = row.transform.decompose_scale(); + row.transform.matrix2 = DMat2::from_diagonal(scale); + } + (false, true) => { + let rotation = row.transform.decompose_rotation(); + let rotation_matrix = DMat2::from_angle(rotation); + row.transform.matrix2 = rotation_matrix; + } + (false, false) => {} + } + } + content +} + /// Overwrites the transform of each element in the input table with the specified transform. -#[node_macro::node(category(""))] -fn replace_transform( +#[node_macro::node(category("Math: Transform"))] +fn replace_transform( _: impl Ctx + InjectFootprint, - #[implementations(Table, Table>, Table, Table, Table)] mut data: Table, - #[implementations(DAffine2)] transform: TransformInput, -) -> Table { - for data_transform in data.iter_mut() { - *data_transform.transform = transform.transform(); + #[implementations( + Table, + Table, + Table>, + Table>, + Table, + Table, + )] + mut content: Table, + transform: DAffine2, +) -> Table { + for row in content.iter_mut() { + *row.transform = transform.transform(); } - data + content } // TODO: Figure out how this node should behave once #2982 is implemented. @@ -74,9 +125,9 @@ async fn extract_transform( Table, Table, )] - vector: Table, + content: Table, ) -> DAffine2 { - vector.iter().next().map(|row| *row.transform).unwrap_or_default() + content.iter().next().map(|row| *row.transform).unwrap_or_default() } /// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform. diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 0e069cb21b..664a0660a2 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -234,7 +234,7 @@ async fn repeat( // TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed. direction: PixelSize, angle: Angle, - #[default(4)] count: IntegerCount, + #[default(5)] count: IntegerCount, ) -> Table { let angle = angle.to_radians(); let count = count.max(1); @@ -849,12 +849,12 @@ async fn points_to_polyline(_: impl Ctx, mut points: Table, #[default(tr let points_count = row.element.point_domain.ids().len(); - if points_count > 2 { + if points_count >= 2 { (0..points_count - 1).for_each(|i| { segment_domain.push(next_id.next_id(), i, i + 1, BezierHandles::Linear, StrokeId::generate()); }); - if closed { + if closed && points_count != 2 { segment_domain.push(next_id.next_id(), points_count - 1, 0, BezierHandles::Linear, StrokeId::generate()); row.element @@ -2053,6 +2053,11 @@ async fn count_elements( source.count() as f64 } +#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] +async fn count_points(_: impl Ctx, source: Table) -> f64 { + source.into_iter().map(|row| row.element.point_domain.positions().len() as f64).sum() +} + #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] async fn path_length(_: impl Ctx, source: Table) -> f64 { source From a4f47cc6601a2b9b0bae3eec33dd9e1babf819b5 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 23 Nov 2025 13:48:01 -0800 Subject: [PATCH 3/4] Add the 'Index Points' node --- node-graph/nodes/vector/src/vector_nodes.rs | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 664a0660a2..edf465a4c6 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -2058,6 +2058,31 @@ async fn count_points(_: impl Ctx, source: Table) -> f64 { source.into_iter().map(|row| row.element.point_domain.positions().len() as f64).sum() } +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn index_points(_: impl Ctx, source: Table, index: f64) -> DVec2 { + let points_count = source.iter().map(|row| row.element.point_domain.positions().len()).sum::(); + + // Clamp and allow negative indexing from the end + let index = index as isize; + let index = if index < 0 { + (points_count as isize + index).max(0) as usize + } else { + (index as usize).min(points_count - 1) + }; + + // Find the point at the given index across all vector elements + let mut accumulated = 0; + for row in source.iter() { + let row_point_count = row.element.point_domain.positions().len(); + if index - accumulated < row_point_count { + return row.element.point_domain.positions()[index - accumulated]; + } + accumulated += row_point_count; + } + + DVec2::ZERO +} + #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] async fn path_length(_: impl Ctx, source: Table) -> f64 { source From 38020ad33115eaf5e6fbb8a5b42ebeecda20388b Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 25 Nov 2025 19:25:52 -0800 Subject: [PATCH 4/4] Fix failing test --- node-graph/nodes/vector/src/vector_nodes.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index edf465a4c6..1bc5837429 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -2375,12 +2375,12 @@ mod test { } #[tokio::test] async fn morph() { - let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY); - let target = Rect::new(-100., -100., 0., 0.).to_path(DEFAULT_ACCURACY); - let morphed = super::morph(Footprint::default(), vector_node_from_bezpath(source), vector_node_from_bezpath(target), 0.5).await; - let morphed = morphed.iter().next().unwrap().element; + let rectangle = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); + let rectangles = super::repeat(Footprint::default(), rectangle, DVec2::new(-100., -100.), 0., 2).await; + let morphed = super::morph(Footprint::default(), rectangles, 0.5).await; + let element = morphed.iter().next().unwrap().element; assert_eq!( - &morphed.point_domain.positions()[..4], + &element.point_domain.positions()[..4], vec![DVec2::new(-50., -50.), DVec2::new(50., -50.), DVec2::new(50., 50.), DVec2::new(-50., 50.)] ); }