diff --git a/Cargo.lock b/Cargo.lock index f8d6a25aa5..554750f6f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2129,6 +2129,7 @@ dependencies = [ "text-nodes", "tokio", "url", + "vector-nodes", "wasm-bindgen", "web-sys", "wgpu-executor", 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 bc1e9d3af1..8e95c40bfd 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 @@ -2175,6 +2175,8 @@ fn static_node_properties() -> NodeProperties { map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties)); map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties)); map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties)); + map.insert("repeat_properties".to_string(), Box::new(node_properties::repeat_properties)); + map.insert("circular_repeat_properties".to_string(), Box::new(node_properties::circular_repeat_properties)); map.insert( "monitor_properties".to_string(), Box::new(|_node_id, _context| node_properties::string_properties("Used internally by the editor to obtain a layer thumbnail.")), 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 9c35d4a529..cfaa130f8b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -25,6 +25,7 @@ use graphene_std::text::{Font, TextAlign}; use graphene_std::transform::{Footprint, ReferencePoint, Transform}; use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::{AngularSpacingMethod, RepeatSpacingMethod}; pub(crate) fn string_properties(text: &str) -> Vec { let widget = TextLabel::new(text).widget_holder(); @@ -221,6 +222,8 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), @@ -1340,6 +1343,46 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte widgets } +pub(crate) fn repeat_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + const DIRECTION_INDEX: usize = 1; + const ANGLE_INDEX: usize = 2; + const COUNT_INDEX: usize = 3; + const SPACING_METHOD_INDEX: usize = 4; + + let direction = vec2_widget(ParameterWidgetsInfo::new(node_id, DIRECTION_INDEX, true, context), "X", "Y", " px", None, false); + let angle = number_widget(ParameterWidgetsInfo::new(node_id, ANGLE_INDEX, true, context), NumberInput::default().unit("°")); + let count = number_widget(ParameterWidgetsInfo::new(node_id, COUNT_INDEX, true, context), NumberInput::default().min(1.).int()); + let spacing_method = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(node_id, SPACING_METHOD_INDEX, true, context)) + .property_row(); + + vec![direction, LayoutGroup::Row { widgets: angle }, LayoutGroup::Row { widgets: count }, spacing_method] +} + +pub(crate) fn circular_repeat_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + const START_ANGLE_INDEX: usize = 1; + const END_ANGLE_INDEX: usize = 2; + const RADIUS_INDEX: usize = 3; + const COUNT_INDEX: usize = 4; + const ANGULAR_SPACING_METHOD_INDEX: usize = 5; + + let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, START_ANGLE_INDEX, true, context), NumberInput::default().unit("°")); + let end_angle = number_widget(ParameterWidgetsInfo::new(node_id, END_ANGLE_INDEX, true, context), NumberInput::default().unit("°")); + let radius = number_widget(ParameterWidgetsInfo::new(node_id, RADIUS_INDEX, true, context), NumberInput::default().min(0.).unit(" px")); + let count = number_widget(ParameterWidgetsInfo::new(node_id, COUNT_INDEX, true, context), NumberInput::default().min(1.).int()); + let angular_spacing_method = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(node_id, ANGULAR_SPACING_METHOD_INDEX, true, context)) + .property_row(); + + vec![ + LayoutGroup::Row { widgets: start_angle }, + LayoutGroup::Row { widgets: end_angle }, + LayoutGroup::Row { widgets: radius }, + LayoutGroup::Row { widgets: count }, + angular_spacing_method, + ] +} + pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { use graphene_std::vector::generator_nodes::spiral::*; diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index f13c57b6cf..a31eea4256 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -21,6 +21,7 @@ graphene-core = { workspace = true } graphene-application-io = { workspace = true } rendering = { workspace = true } raster-nodes = { workspace = true } +vector-nodes = { workspace = true } graphic-types = { workspace = true } text-nodes = { workspace = true } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 48f15c0422..caa54b6ff5 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -26,6 +26,7 @@ use std::hash::Hash; use std::marker::PhantomData; use std::str::FromStr; pub use std::sync::Arc; +use vector_nodes::{AngularSpacingMethod, RepeatSpacingMethod}; pub struct TaggedValueTypeError; @@ -248,6 +249,8 @@ tagged_value! { ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm), PointSpacingType(vector::misc::PointSpacingType), SpiralType(vector::misc::SpiralType), + RepeatSpacingMethod(RepeatSpacingMethod), + AngularSpacingMethod(AngularSpacingMethod), #[serde(alias = "LineCap")] StrokeCap(vector::style::StrokeCap), #[serde(alias = "LineJoin")] diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 8ec8855e69..dd5d25cab2 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -5,6 +5,7 @@ use core_types::registry::types::{Angle, IntegerCount, Length, Multiplier, Perce use core_types::table::{Table, TableRow, TableRowMut}; use core_types::transform::{Footprint, Transform}; use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl}; +use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use graphic_types::Vector; use graphic_types::raster_types::{CPU, GPU, Raster}; @@ -225,8 +226,32 @@ where content } -#[node_macro::node(category("Instancing"), path(core_types::vector))] -async fn repeat( +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum RepeatSpacingMethod { + #[default] + #[serde(rename = "span")] + Span, + #[serde(rename = "envelope")] + Envelope, + #[serde(rename = "pitch")] + Pitch, + #[serde(rename = "gap")] + Gap, +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum AngularSpacingMethod { + #[default] + #[serde(rename = "span")] + Span, + #[serde(rename = "pitch")] + Pitch, +} + +#[node_macro::node(category("Instancing"), path(graphene_core::vector), properties("repeat_properties"))] +async fn repeat( _: impl Ctx, // TODO: Implement other graphical types. #[implementations(Table, Table, Table>, Table, Table)] instance: Table, @@ -235,16 +260,38 @@ async fn repeat( direction: PixelSize, angle: Angle, #[default(5)] count: IntegerCount, + #[default(RepeatSpacingMethod::Span)] spacing_method: RepeatSpacingMethod, ) -> Table { let angle = angle.to_radians(); let count = count.max(1); let total = (count - 1) as f64; + let direction_normalized = direction.normalize(); + + let width = if matches!(spacing_method, RepeatSpacingMethod::Envelope | RepeatSpacingMethod::Gap) { + match instance.bounding_box(DAffine2::IDENTITY, false) { + RenderBoundingBox::Rectangle([min, max]) => { + let size = max - min; + let dir_abs = direction_normalized.abs(); + size.x * dir_abs.x + size.y * dir_abs.y + } + _ => 0.0, + } + } else { + 0.0 + }; + + let (pitch, offset) = match spacing_method { + RepeatSpacingMethod::Span => (direction.length() / total.max(1.), DVec2::ZERO), + RepeatSpacingMethod::Envelope => ((direction.length() - width) / total.max(1.), width / 2. * direction_normalized), + RepeatSpacingMethod::Pitch => (direction.length(), DVec2::ZERO), + RepeatSpacingMethod::Gap => (direction.length() + width, DVec2::ZERO), + }; let mut result_table = Table::new(); for index in 0..count { - let angle = index as f64 * angle / total; - let translation = index as f64 * direction / total; + let angle = index as f64 * angle / total.max(1.); + let translation = offset + index as f64 * pitch * direction_normalized; let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation); for row in instance.iter() { @@ -261,22 +308,34 @@ async fn repeat( result_table } -#[node_macro::node(category("Instancing"), path(core_types::vector))] +#[node_macro::node(category("Instancing"), path(graphene_core::vector), properties("circular_repeat_properties"))] async fn circular_repeat( _: impl Ctx, #[implementations(Table, Table, Table>, Table, Table)] instance: Table, - start_angle: Angle, + #[default(0.)] start_angle: Angle, + #[default(360.)] end_angle: Angle, #[unit(" px")] #[default(5)] radius: f64, #[default(5)] count: IntegerCount, + #[default(AngularSpacingMethod::Span)] angular_spacing_method: AngularSpacingMethod, ) -> Table { let count = count.max(1); + let start_rad = start_angle.to_radians(); + let end_rad = end_angle.to_radians(); + let total_angle = end_rad - start_rad; + let total = (count - 1) as f64; + + let angular_pitch = match angular_spacing_method { + AngularSpacingMethod::Span => total_angle / total.max(1.), + AngularSpacingMethod::Pitch => total_angle / count as f64, + }; let mut result_table = Table::new(); for index in 0..count { - let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + start_angle.to_radians()); + let angle_rad = start_rad + index as f64 * angular_pitch; + let angle = DAffine2::from_angle(angle_rad); let translation = DAffine2::from_translation(radius * DVec2::Y); let transform = angle * translation; @@ -2417,6 +2476,7 @@ mod test { direction, 0., count, + super::RepeatSpacingMethod::Span, ) .await; let vector_table = super::flatten_path(Footprint::default(), repeated).await; @@ -2436,6 +2496,7 @@ mod test { direction, 0., count, + super::RepeatSpacingMethod::Span, ) .await; let vector_table = super::flatten_path(Footprint::default(), repeated).await; @@ -2447,7 +2508,16 @@ mod test { } #[tokio::test] async fn circular_repeat() { - let repeated = super::circular_repeat(Footprint::default(), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)), 45., 4., 8).await; + let repeated = super::circular_repeat( + Footprint::default(), + vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)), + 45., + 360., + 4., + 8, + super::AngularSpacingMethod::Span, + ) + .await; let vector_table = super::flatten_path(Footprint::default(), repeated).await; let vector = vector_table.iter().next().unwrap().element; assert_eq!(vector.region_manipulator_groups().count(), 8); @@ -2588,7 +2658,7 @@ mod test { #[tokio::test] async fn morph() { 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 rectangles = super::repeat(Footprint::default(), rectangle, DVec2::new(-100., -100.), 0., 2, super::RepeatSpacingMethod::Span).await; let morphed = super::morph(Footprint::default(), rectangles, 0.5).await; let element = morphed.iter().next().unwrap().element; assert_eq!(