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/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 6371d8d0e8..7b25d7285b 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,7 @@ 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("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), 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..9b6a2fe929 100644 --- a/node-graph/libraries/no-std-types/src/registry.rs +++ b/node-graph/libraries/no-std-types/src/registry.rs @@ -19,6 +19,8 @@ pub mod types { pub type Length = f64; /// 0 to 1 pub type Fraction = 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..ac2d1ebe93 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/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 7b889cd2b2..877773e6ae 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); @@ -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. @@ -848,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 @@ -2007,9 +2008,47 @@ async fn count_elements( Vec, Vec, )] - source: I, + content: I, ) -> f64 { - source.count() as f64 + content.count() as f64 +} + +#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] +async fn count_points(_: impl Ctx, content: Table) -> f64 { + content.into_iter().map(|row| row.element.point_domain.positions().len() as f64).sum() +} + +/// Retrieves the vec2 position (in local space) of the anchor point at the specified index in table of vector elements. +/// If no value exists at that index, the position (0, 0) is returned. +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn index_points( + _: impl Ctx, + /// The vector element or elements containing the anchor points to be retrieved. + content: Table, + /// The index of the points to retrieve, starting from 0 for the first point. Negative indices count backwards from the end, starting from -1 for the last item. + index: f64, +) -> DVec2 { + let points_count = content.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 content.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))]