diff --git a/Cargo.lock b/Cargo.lock index 5aae8b9592..5b7422763b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2842,12 +2842,9 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.7" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "inotify" diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 47d5455e1b..fa74447021 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -5,7 +5,7 @@ use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List, ListDyn}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; -use core_types::transform::{Footprint, Transform}; +use core_types::transform::{ApplyTransform, Footprint, Transform}; use core_types::uuid::NodeId; use core_types::{ ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CloneVarArgs, Color, Context, Ctx, ExtractAll, @@ -38,6 +38,9 @@ trait VectorListIterMut { fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2)); fn vector_count(&self) -> usize; + + /// Iterates over a mutable reference to each vector item's transform, used by the transform-assignment nodes. + fn vector_transforms_iter_mut(&mut self) -> impl DoubleEndedIterator; } impl VectorListIterMut for List { @@ -54,6 +57,13 @@ impl VectorListIterMut for List { fn vector_count(&self) -> usize { self.iter_element_values().filter_map(|element| element.as_vector()).map(|list| list.len()).sum() } + + fn vector_transforms_iter_mut(&mut self) -> impl DoubleEndedIterator { + // Grab only the direct children + self.iter_element_values_mut() + .filter_map(|graphic| graphic.as_vector_mut()) + .flat_map(|vector_list| vector_list.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM)) + } } impl VectorListIterMut for List { @@ -67,6 +77,10 @@ impl VectorListIterMut for List { fn vector_count(&self) -> usize { self.len() } + + fn vector_transforms_iter_mut(&mut self) -> impl DoubleEndedIterator { + self.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) + } } /// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient. @@ -134,6 +148,239 @@ where content } +#[node_macro::node(category("Instancing"), path(core_types::vector))] +async fn assign_rotations( + _: impl Ctx, + #[implementations(List, List)] mut content: T, + #[range((0., 360.))] + #[default(360.)] + angle: Angle, + /// Whether to reverse the order. + reverse: bool, + /// Whether to randomize the rotation selection for each element. + #[default(true)] + randomize: bool, + /// The seed used for randomization. + seed: SeedValue, + #[range((-50., 50.))] bias: f64, + /// The number of elements to span across before repeating. A 0 value will span the entire range once. + repeat_every: u32, +) -> T { + let count = content.vector_count() as u32; + + let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64); + + let apply = |(index, transform): (usize, &mut DAffine2)| { + let factor = match randomize { + true => { + if bias.abs() < 1e-6 { + rng.random::() + } else { + (1. - rng.random::() * (1. - 2_f64.powf(bias))).log2() / bias + } + } + false => match repeat_every { + 0 => index as f64 / (count - 1).max(1) as f64, + 1 => 0., + _ => index as f64 % repeat_every as f64 / (repeat_every - 1) as f64, + }, + }; + let rotation_transform = DAffine2::from_angle(factor * angle.to_radians()); + transform.left_apply_transform(&rotation_transform); + }; + + if reverse { + content.vector_transforms_iter_mut().rev().enumerate().for_each(apply); + } else { + content.vector_transforms_iter_mut().enumerate().for_each(apply); + } + + content +} + +#[node_macro::node(category("Instancing"), path(core_types::vector))] +async fn assign_translations( + _: impl Ctx, + #[implementations(List, List)] mut content: T, + x_min: f64, + x_max: f64, + y_min: f64, + y_max: f64, + /// Whether to reverse the order. + reverse: bool, + /// Whether to randomize the translation selection for each element. + #[default(true)] + randomize: bool, + /// The seed used for randomization. + seed: SeedValue, + #[range((-50., 50.))] x_bias: f64, + #[range((-50., 50.))] y_bias: f64, + /// The number of elements to span across before repeating. A 0 value will span the entire range once. + repeat_every: u32, +) -> T { + let count = content.vector_count() as u32; + + let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64); + + let apply = |(index, transform): (usize, &mut DAffine2)| { + let (factor_x, factor_y) = match randomize { + true => { + let factor_x = if x_bias.abs() < 1e-6 { + rng.random::() + } else { + (1. - rng.random::() * (1. - 2_f64.powf(x_bias))).log2() / x_bias + }; + let factor_y = if y_bias.abs() < 1e-6 { + rng.random::() + } else { + (1. - rng.random::() * (1. - 2_f64.powf(y_bias))).log2() / y_bias + }; + (factor_x, factor_y) + } + false => match repeat_every { + 0 => { + let factor = index as f64 / (count - 1).max(1) as f64; + (factor, factor) + } + 1 => (0., 0.), + _ => { + let factor = index as f64 % repeat_every as f64 / (repeat_every - 1) as f64; + (factor, factor) + } + }, + }; + let translation = DVec2::new(x_min + (x_max - x_min) * factor_x, y_min + (y_max - y_min) * factor_y); + let translation_transform = DAffine2::from_translation(translation); + transform.left_apply_transform(&translation_transform); + }; + + if reverse { + content.vector_transforms_iter_mut().rev().enumerate().for_each(apply); + } else { + content.vector_transforms_iter_mut().enumerate().for_each(apply); + } + + content +} + +// TODO: decide if we want to keep both circular and non-circular versions +#[node_macro::node(category("Instancing"), path(core_types::vector))] +async fn assign_translations_circular( + _: impl Ctx, + #[implementations(List, List)] mut content: T, + radius_min: f64, + radius_max: f64, + #[default(-180.)] angle_min: Angle, + #[default(180.)] angle_max: Angle, + /// Whether to reverse the order. + reverse: bool, + /// Whether to randomize the translation selection for each element. + #[default(true)] + randomize: bool, + /// The seed used for randomization. + seed: SeedValue, + #[range((-50., 50.))] radius_bias: f64, + #[range((-50., 50.))] angle_bias: f64, + /// The number of elements to span across before repeating. A 0 value will span the entire range once. + repeat_every: u32, +) -> T { + let count = content.vector_count() as u32; + + let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64); + + let apply = |(index, transform): (usize, &mut DAffine2)| { + let (factor_angle, factor_radius) = match randomize { + true => { + let factor_angle = if angle_bias.abs() < 1e-6 { + rng.random::() + } else { + (1. - rng.random::() * (1. - 2_f64.powf(angle_bias))).log2() / angle_bias + }; + let factor_radius = if radius_bias.abs() < 1e-6 { + rng.random::() + } else { + (1. - rng.random::() * (1. - 2_f64.powf(radius_bias))).log2() / radius_bias + }; + (factor_angle, factor_radius) + } + false => match repeat_every { + 0 => { + let factor = index as f64 / (count - 1).max(1) as f64; + (factor, factor) + } + 1 => (0., 0.), + _ => { + let factor = index as f64 % repeat_every as f64 / (repeat_every - 1) as f64; + (factor, factor) + } + }, + }; + let angle = factor_angle * (angle_max.to_radians() - angle_min.to_radians()) + angle_min.to_radians(); + let radius = radius_min + (radius_max - radius_min) * factor_radius.sqrt(); + let translation = DVec2::new(angle.cos() * radius, angle.sin() * radius); + let translation_transform = DAffine2::from_translation(translation); + transform.left_apply_transform(&translation_transform); + }; + + if reverse { + content.vector_transforms_iter_mut().rev().enumerate().for_each(apply); + } else { + content.vector_transforms_iter_mut().enumerate().for_each(apply); + } + + content +} + +#[node_macro::node(category("Instancing"), path(core_types::vector))] +async fn assign_scales( + _: impl Ctx, + #[implementations(List, List)] mut content: T, + #[default(0.5)] min_scale: f64, + #[default(2.)] max_scale: f64, + /// Whether to reverse the order. + reverse: bool, + /// Whether to randomize the scale selection for each element. + #[default(true)] + randomize: bool, + /// The seed used for randomization. + seed: SeedValue, + #[range((-50., 50.))] bias: f64, + /// The number of elements to span across before repeating. A 0 value will span the entire range once. + repeat_every: u32, +) -> T { + let count = content.vector_count() as u32; + + let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64); + + let apply = |(index, transform): (usize, &mut DAffine2)| { + let factor = match randomize { + true => { + if bias.abs() < 1e-6 { + rng.random::() + } else { + (1. - rng.random::() * (1. - 2_f64.powf(bias))).log2() / bias + } + } + false => match repeat_every { + 0 => index as f64 / (count - 1).max(1) as f64, + 1 => 0., + _ => index as f64 % repeat_every as f64 / (repeat_every - 1) as f64, + }, + }; + let scale_value = min_scale + (max_scale - min_scale) * factor; + let scale_transform = DAffine2::from_scale(DVec2::splat(scale_value)); + transform.apply_transform(&scale_transform); + }; + + if reverse { + content.vector_transforms_iter_mut().rev().enumerate().for_each(apply); + } else { + content.vector_transforms_iter_mut().enumerate().for_each(apply); + } + + content +} + /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] async fn fill + 'n + Send, V: VectorListIterMut + 'n + Send>(