Convert copy to points, sample polyline, scatter, spline, jitter, and assign colors

This commit is contained in:
Dennis Kobert
2026-08-22 20:59:37 +00:00
parent 5f6c18893e
commit 3126a5b278
5 changed files with 379 additions and 291 deletions
@@ -1391,6 +1391,17 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
if reset_node_definitions_on_open && let Some(reference) = document.network_interface.reference(node_id, network_path) { if reset_node_definitions_on_open && let Some(reference) = document.network_interface.reference(node_id, network_path) {
let node_definition = resolve_document_node_type(&reference)?; let node_definition = resolve_document_node_type(&reference)?;
document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template()); document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template());
// The leveled-records flip moved the Copy to Points content wire ahead of the points wire.
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::copy_to_points::IDENTIFIER) {
let mut node_template = node_definition.default_node_template();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[0].clone(), network_path);
for (index, input) in old_inputs.into_iter().enumerate().skip(2) {
document.network_interface.set_input(&InputConnector::node(*node_id, index), input, network_path);
}
}
} }
// Rebuild stale Merge/Artboard subgraphs that still use the removed LegacyLayerExtendNode internally // Rebuild stale Merge/Artboard subgraphs that still use the removed LegacyLayerExtendNode internally
@@ -206,16 +206,16 @@ impl Fsm for FillToolFsmState {
mod test_fill { mod test_fill {
pub use crate::test_utils::test_prelude::*; pub use crate::test_utils::test_prelude::*;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::list::List;
use graphene_std::vector::fill; use graphene_std::vector::fill;
use graphene_std::Graphic;
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<List<Color>> { async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Graphic> {
let instrumented = match editor.eval_graph().await { let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented, Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"), Err(e) => panic!("Failed to evaluate graph: {e}"),
}; };
instrumented.grab_all_input::<fill::FillInput<List<Color>>>(&editor.runtime).collect() instrumented.grab_all_input::<fill::FillInput>(&editor.runtime).collect()
} }
#[tokio::test] #[tokio::test]
@@ -245,7 +245,8 @@ mod test_fill {
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
let fills = get_fills(&mut editor).await; let fills = get_fills(&mut editor).await;
assert_eq!(fills.len(), 1); assert_eq!(fills.len(), 1);
let color = fills.first().unwrap().element(0).expect("Color is stored in the list"); let Some(Graphic::Color(color_list)) = fills.first() else { panic!("the fill paint holds a color") };
let color = color_list.element(0).expect("Color is stored in the list");
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::GREEN)); assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::GREEN));
} }
@@ -258,7 +259,8 @@ mod test_fill {
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await; editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await;
let fills = get_fills(&mut editor).await; let fills = get_fills(&mut editor).await;
assert_eq!(fills.len(), 1); assert_eq!(fills.len(), 1);
let color = fills.first().unwrap().element(0).expect("Color is stored in the list"); let Some(Graphic::Color(color_list)) = fills.first() else { panic!("the fill paint holds a color") };
let color = color_list.element(0).expect("Color is stored in the list");
assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::YELLOW)); assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::YELLOW));
} }
} }
@@ -2013,10 +2013,9 @@ mod test_gradient {
use glam::DAffine2; use glam::DAffine2;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::list::List;
use graphene_std::vector::style::{GradientSpreadMethod, build_transform_with_y_preservation}; use graphene_std::vector::style::{GradientSpreadMethod, build_transform_with_y_preservation};
use graphene_std::vector::{GradientStop, GradientStops, fill}; use graphene_std::vector::{GradientStop, GradientStops, fill};
use graphene_std::{Graphic, NodeInputDecleration}; use graphene_std::NodeInputDecleration;
use super::gradient_space_transform; use super::gradient_space_transform;
@@ -145,6 +145,12 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
.into_iter() .into_iter()
.map(|entry| (graphene_std::vector::solidify_stroke::IDENTIFIER.clone(), entry)), .map(|entry| (graphene_std::vector::solidify_stroke::IDENTIFIER.clone(), entry)),
); );
// The color assignment's graphic-lane rows, served under its identifier.
node_types.extend(
graphene_std::vector::assign_colors_graphic_entries()
.into_iter()
.map(|entry| (graphene_std::vector::assign_colors::IDENTIFIER.clone(), entry)),
);
// Element-wise coercion into `Graphic` for single-typed leveled inputs, // Element-wise coercion into `Graphic` for single-typed leveled inputs,
// served by the to_graphic rows. // served by the to_graphic rows.
node_types.extend( node_types.extend(
+354 -284
View File
@@ -9,7 +9,8 @@ use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLe
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::attribute::{Attr, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill}; use core_types::attribute::{Attr, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill};
use core_types::extent::{ListIn, LevelIn}; use core_types::context::IndexLink;
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
use core_types::gpoll::{Extent, GPoll}; use core_types::gpoll::{Extent, GPoll};
use core_types::gpoll::GraphError; use core_types::gpoll::GraphError;
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex};
@@ -40,46 +41,41 @@ use vector_types::vector::style::{GradientStops, PaintOrder, Stroke, StrokeAlign
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::{GradientSpreadMethod, GradientType}; use vector_types::{GradientSpreadMethod, GradientType};
/// Implemented for types that contain vector items reachable via mutable access. /// The standard row attributes a per-lane re-emission carries from its
/// Used for the fill and stroke nodes so they can apply to either `List<Graphic>` or `List<Vector>`. /// materialized source lane, parked for the fresh output row.
trait VectorListIterMut { fn carried_lane_attrs<'e>(arena: &'e core_types::arena::Arena, lane: core_types::node::RecordLane<'_>) -> Result<(Attr<'e, TransformAttr>, Attr<'e, EditorLayerPath>), Interrupt> {
fn for_each_vector_list_mut(&mut self, f: impl FnMut(&mut List<Vector>)); let layer_path: Vec<NodeId> = lane.attr::<EditorLayerPath>().to_vec();
let (layer_path, _) = arena.alloc(layer_path).ok_or(GraphError {
fn vector_count(&self) -> usize; kind: core_types::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
})?;
Ok((Attr(lane.attr::<TransformAttr>()), Attr(layer_path.as_slice())))
} }
impl VectorListIterMut for List<Graphic> { /// The gradient color for one assign-colors position, replaying the
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) { /// randomized draws up to it.
for graphic in self.iter_element_values_mut() { fn assign_color_at(gradient: &GradientStops, position: usize, length: usize, randomize: bool, seed: SeedValue, repeat_every: u32) -> Color {
if let Some(vector_list) = graphic.as_vector_mut() { let factor = match randomize {
f(vector_list); true => {
}; let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
(0..=position).map(|_| rng.random::<f64>()).next_back().unwrap_or_default()
} }
} false => match repeat_every {
0 => position as f64 / (length - 1).max(1) as f64,
fn vector_count(&self) -> usize { 1 => 0.,
self.iter_element_values().filter_map(|element| element.as_vector()).map(|list| list.len()).sum() _ => position as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
} },
} };
gradient.evaluate(factor)
impl VectorListIterMut for List<Vector> {
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) {
f(self);
}
fn vector_count(&self) -> usize {
self.len()
}
} }
/// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient. /// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), extent(assign_colors_extent))]
fn assign_colors<T>( fn assign_colors<'e>(
_: impl Ctx, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// The content with vector paths to apply the fill and/or stroke style to. /// The content with vector paths to apply the fill and/or stroke style to.
#[implementations(List<Graphic>, List<Vector>)]
#[widget(ParsedWidgetOverride::Hidden)] #[widget(ParsedWidgetOverride::Hidden)]
mut content: T, content: IList<Vector>,
/// Whether to style the fill. /// Whether to style the fill.
#[default(true)] #[default(true)]
fill: bool, fill: bool,
@@ -87,7 +83,7 @@ fn assign_colors<T>(
stroke: bool, stroke: bool,
/// The range of colors to select from. /// The range of colors to select from.
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")] #[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")]
gradient: List<GradientStops>, gradient: IList<GradientStops>,
/// Whether to reverse the gradient. /// Whether to reverse the gradient.
reverse: bool, reverse: bool,
/// Whether to randomize the color selection for each element from throughout the gradient. /// Whether to randomize the color selection for each element from throughout the gradient.
@@ -99,31 +95,108 @@ fn assign_colors<T>(
/// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once. /// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once.
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] #[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")]
repeat_every: u32, repeat_every: u32,
) -> T ) -> Result<IList<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>)>, Interrupt> {
where let lane = ctx.innermost_index() as usize;
T: VectorListIterMut + Send, if lane >= content.len() {
{ return Err(GraphError::past_end().into());
let Some(row) = gradient.into_iter().next() else { return content }; }
let element = content.element_ref(lane).clone();
let park_existing = |paint: Option<&List<Graphic>>| -> Result<Option<&'e List<Graphic>>, Interrupt> {
paint.map(|paint| park_paint(ctx.arena(), paint.clone())).transpose()
};
let existing_fill = park_existing(content.lane(lane).attr::<Fill>())?;
let existing_stroke = park_existing(content.lane(lane).attr::<StrokeAttr>())?;
let carried = carried_lane_attrs(ctx.arena(), content.lane(lane))?;
let (transform, layer_path) = carried;
let length = content.vector_count(); if gradient.len() == 0 {
let element = row.into_element(); return Ok((element, transform, Attr(existing_fill), Attr(existing_stroke), layer_path));
let gradient = if reverse { element.reversed() } else { element }; }
let gradient_element = gradient.element_ref(0);
let reversed;
let gradient_element = match reverse {
true => {
reversed = gradient_element.reversed();
&reversed
}
false => gradient_element,
};
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); let color = assign_color_at(gradient_element, lane, content.len(), randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list();
let parked = park_paint(ctx.arena(), paint)?;
let mut i: usize = 0; let fill_attr = match fill {
content.for_each_vector_list_mut(|vector_list| { true => Some(parked),
false => existing_fill,
};
let stroke_attr = match stroke && element.stroke.is_some() {
true => Some(parked),
false => existing_stroke,
};
Ok((element, transform, Attr(fill_attr), Attr(stroke_attr), layer_path))
}
fn assign_colors_extent(
content: ListIn<'_, Vector>,
_fill: ValueIn<'_, bool>,
_stroke: ValueIn<'_, bool>,
_gradient: ListIn<'_, GradientStops>,
_reverse: ValueIn<'_, bool>,
_randomize: ValueIn<'_, bool>,
_seed: ValueIn<'_, SeedValue>,
_repeat_every: ValueIn<'_, u32>,
level: LevelIn,
) -> GPoll<Extent> {
match level.top() {
true => content.get().map(|content| Extent::Exactly(content.len())),
false => GPoll::Final(Extent::Exactly(1)),
}
}
/// The color assignment over graphic lanes: the running position spans the
/// interior vectors of every lane, as the pre-flip broadcast did. Registered
/// under the assign colors identifier.
#[node_macro::node(category(""), extent(assign_colors_graphic_extent))]
fn assign_colors_graphic<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic>,
#[default(true)]
fill: bool,
stroke: bool,
gradient: IList<GradientStops>,
reverse: bool,
randomize: bool,
seed: SeedValue,
repeat_every: u32,
) -> Result<IList<(Graphic, Attr<'e, TransformAttr>, Attr<'e, EditorLayerPath>)>, Interrupt> {
let lane = ctx.innermost_index() as usize;
if lane >= content.len() {
return Err(GraphError::past_end().into());
}
let mut element = content.element_ref(lane).clone();
let (transform, layer_path) = carried_lane_attrs(ctx.arena(), content.lane(lane))?;
if gradient.len() == 0 {
return Ok((element, transform, layer_path));
}
let gradient_element = gradient.element_ref(0);
let reversed;
let gradient_element = match reverse {
true => {
reversed = gradient_element.reversed();
&reversed
}
false => gradient_element,
};
let interior_count = |graphic: &Graphic| graphic.as_vector().map_or(0, |list| list.len());
let length: usize = (0..content.len()).map(|row| interior_count(content.element_ref(row))).sum();
let mut position: usize = (0..lane).map(|row| interior_count(content.element_ref(row))).sum();
if let Some(vector_list) = element.as_vector_mut() {
for index in 0..vector_list.len() { for index in 0..vector_list.len() {
let factor = match randomize { let color = assign_color_at(gradient_element, position, length, randomize, seed, repeat_every);
true => rng.random::<f64>(),
false => match repeat_every {
0 => i as f64 / (length - 1).max(1) as f64,
1 => 0.,
_ => i as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
},
};
let color = gradient.evaluate(factor);
let paint = List::new_from_element(color).into_graphic_list(); let paint = List::new_from_element(color).into_graphic_list();
if fill { if fill {
@@ -133,13 +206,33 @@ where
set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone()); set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone());
} }
i += 1; position += 1;
} }
}); }
content Ok((element, transform, layer_path))
} }
fn assign_colors_graphic_extent(
content: ListIn<'_, Graphic>,
_fill: ValueIn<'_, bool>,
_stroke: ValueIn<'_, bool>,
_gradient: ListIn<'_, GradientStops>,
_reverse: ValueIn<'_, bool>,
_randomize: ValueIn<'_, bool>,
_seed: ValueIn<'_, SeedValue>,
_repeat_every: ValueIn<'_, u32>,
level: LevelIn,
) -> GPoll<Extent> {
match level.top() {
true => content.get().map(|content| Extent::Exactly(content.len())),
false => GPoll::Final(Extent::Exactly(1)),
}
}
pub use _assign_colors_graphic_mod::assign_colors_graphic_entries;
fn park_paint<'e>(arena: &'e core_types::arena::Arena, paint: List<Graphic>) -> Result<&'e List<Graphic>, Interrupt> { fn park_paint<'e>(arena: &'e core_types::arena::Arena, paint: List<Graphic>) -> Result<&'e List<Graphic>, Interrupt> {
let (parked, _) = arena.alloc(paint).ok_or(GraphError { let (parked, _) = arena.alloc(paint).ok_or(GraphError {
kind: core_types::gpoll::ErrorKind::ArenaExhausted, kind: core_types::gpoll::ErrorKind::ArenaExhausted,
@@ -366,14 +459,17 @@ fn stroke_graphic_leveled<'e>(
pub use _fill_graphic_leveled_mod::fill_graphic_leveled_entries; pub use _fill_graphic_leveled_mod::fill_graphic_leveled_entries;
pub use _stroke_graphic_leveled_mod::stroke_graphic_leveled_entries; pub use _stroke_graphic_leveled_mod::stroke_graphic_leveled_entries;
#[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))] /// Each copy evaluates the content within the copy's index pushed in, placed
fn copy_to_points<I: Send + Clone>( /// at the copy's point with its randomized scale and rotation composed onto
_: impl Ctx, /// the lane transform.
points: List<Vector>, #[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector), extent(copy_to_points_extent))]
fn copy_to_points<T>(
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
/// Artwork to be copied and placed at each point. /// Artwork to be copied and placed at each point.
content: impl Node<Context<'_>, Output = (T, Attr<TransformAttr>)>,
/// The points to place the copies at.
#[expose] #[expose]
#[implementations(List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Color>, List<GradientStops>)] points: IList<Vector>,
content: List<I>,
/// Minimum range of randomized sizes given to each placed copy. /// Minimum range of randomized sizes given to each placed copy.
#[default(1)] #[default(1)]
#[range] #[range]
@@ -398,56 +494,77 @@ fn copy_to_points<I: Send + Clone>(
random_rotation: Angle, random_rotation: Angle,
/// Seed to determine unique variations on all the randomized copy angles. /// Seed to determine unique variations on all the randomized copy angles.
random_rotation_seed: SeedValue, random_rotation_seed: SeedValue,
) -> List<I> { ) -> Result<IList<(T, Attr<TransformAttr>)>, Interrupt> {
let mut result_list = List::new(); let inner = content.inner_extent(ctx)?;
let (copy, rest) = ctx.split_innermost(inner);
let random_scale_difference = random_scale_max - random_scale_min; let random_scale_difference = random_scale_max - random_scale_min;
let do_scale = random_scale_difference.abs() > 1e-6;
let do_rotation = random_rotation.abs() > 1e-6;
for row in points.into_iter() { let mut remaining = copy as usize;
for row in 0..points.len() {
let vector = points.element_ref(row);
let positions = vector.point_domain.positions();
if remaining >= positions.len() {
remaining -= positions.len();
continue;
}
// The randomized parameters replay the row's sequential draws up to
// this copy's point.
let mut scale_rng = rand::rngs::StdRng::seed_from_u64(random_scale_seed.into()); let mut scale_rng = rand::rngs::StdRng::seed_from_u64(random_scale_seed.into());
let mut rotation_rng = rand::rngs::StdRng::seed_from_u64(random_rotation_seed.into()); let mut rotation_rng = rand::rngs::StdRng::seed_from_u64(random_rotation_seed.into());
let mut rotation = 0.;
let do_scale = random_scale_difference.abs() > 1e-6; let mut scale = random_scale_min;
let do_rotation = random_rotation.abs() > 1e-6; for _ in 0..=remaining {
rotation = match do_rotation {
let points_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); true => (rotation_rng.random::<f64>() - 0.5) * random_rotation / 360. * TAU,
for &point in row.element().point_domain.positions() { false => 0.,
let translation = points_transform.transform_point2(point);
let rotation = if do_rotation {
let degrees = (rotation_rng.random::<f64>() - 0.5) * random_rotation;
degrees / 360. * TAU
} else {
0.
}; };
scale = match do_scale {
let scale = if do_scale { false => random_scale_min,
if random_scale_bias.abs() < 1e-6 { // Linear
// Linear true if random_scale_bias.abs() < 1e-6 => random_scale_min + scale_rng.random::<f64>() * random_scale_difference,
random_scale_min + scale_rng.random::<f64>() * random_scale_difference // Weighted (see <https://www.desmos.com/calculator/gmavd3m9bd>)
} else { true => {
// Weighted (see <https://www.desmos.com/calculator/gmavd3m9bd>)
let horizontal_scale_factor = 1. - 2_f64.powf(random_scale_bias); let horizontal_scale_factor = 1. - 2_f64.powf(random_scale_bias);
let scale_factor = (1. - scale_rng.random::<f64>() * horizontal_scale_factor).log2() / random_scale_bias; let scale_factor = (1. - scale_rng.random::<f64>() * horizontal_scale_factor).log2() / random_scale_bias;
random_scale_min + scale_factor * random_scale_difference random_scale_min + scale_factor * random_scale_difference
} }
} else {
random_scale_min
}; };
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation);
for row_index in 0..content.len() {
let Some(mut row) = content.clone_item(row_index) else { continue };
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
row.set_attribute(ATTR_TRANSFORM, transform * row_transform);
result_list.push(row);
}
} }
}
result_list let points_transform: DAffine2 = points.lane(row).attr::<TransformAttr>();
let translation = points_transform.transform_point2(positions[remaining]);
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation);
let mut frame = IndexLink { index: 0, outer: None };
let (element, local_transform) = content.eval(&ctx.push_level(&mut frame, copy, rest))?;
return Ok((element, Attr(transform * *local_transform)));
}
Err(GraphError::past_end().into())
}
/// The pushed level holds one copy per point; inner levels forward to the
/// content, taken uniform across copies.
fn copy_to_points_extent(
content: ExtentIn<'_>,
points: ListIn<'_, Vector>,
_random_scale_min: ValueIn<'_, f64>,
_random_scale_max: ValueIn<'_, f64>,
_random_scale_bias: ValueIn<'_, f64>,
_random_scale_seed: ValueIn<'_, SeedValue>,
_random_rotation: ValueIn<'_, f64>,
_random_rotation_seed: ValueIn<'_, SeedValue>,
level: LevelIn,
) -> GPoll<Extent> {
match level.pushed() {
true => points
.get()
.map(|points| Extent::Exactly((0..points.len()).map(|row| points.element_ref(row).point_domain.positions().len()).sum())),
false => content.at(level),
}
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
@@ -1448,7 +1565,7 @@ fn solidify_stroke<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic>, content: IList<Graphic>,
) -> Result< ) -> Result<
( IList<(
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, Fill>,
@@ -1459,7 +1576,7 @@ fn solidify_stroke<'e>(
Attr<'e, ClippingMask>, Attr<'e, ClippingMask>,
Attr<'e, EditorLayerPath>, Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>, Attr<'e, EditorMergedLayers>,
), )>,
Interrupt, Interrupt,
> { > {
solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.innermost_index() as usize) solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.innermost_index() as usize)
@@ -1485,7 +1602,7 @@ fn solidify_stroke_vector<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Vector>, content: IList<Vector>,
) -> Result< ) -> Result<
( IList<(
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, Fill>,
@@ -1496,7 +1613,7 @@ fn solidify_stroke_vector<'e>(
Attr<'e, ClippingMask>, Attr<'e, ClippingMask>,
Attr<'e, EditorLayerPath>, Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>, Attr<'e, EditorMergedLayers>,
), )>,
Interrupt, Interrupt,
> { > {
solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.innermost_index() as usize) solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.innermost_index() as usize)
@@ -1672,7 +1789,7 @@ pub use _flatten_path_vector_mod::flatten_path_vector_entries;
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)]
fn sample_polyline( fn sample_polyline(
_: impl Ctx, _: impl Ctx,
content: List<Vector>, (element, transform): (Vector, Attr<TransformAttr>),
spacing: PointSpacingType, spacing: PointSpacingType,
#[default(100.)] #[default(100.)]
#[hard(0..)] #[hard(0..)]
@@ -1688,7 +1805,7 @@ fn sample_polyline(
#[unit(" px")] #[unit(" px")]
stop_offset: f64, stop_offset: f64,
adaptive_spacing: bool, adaptive_spacing: bool,
) -> List<Vector> { ) -> (Vector, Attr<TransformAttr>) {
let pathseg_perimeter = |segment: PathSeg| { let pathseg_perimeter = |segment: PathSeg| {
if is_linear(segment) { if is_linear(segment) {
Line::new(segment.start(), segment.end()).perimeter(DEFAULT_ACCURACY) Line::new(segment.start(), segment.end()).perimeter(DEFAULT_ACCURACY)
@@ -1697,61 +1814,55 @@ fn sample_polyline(
} }
}; };
content let mut element = element;
.into_iter() let mut result = Vector {
.map(|mut row| { point_domain: Default::default(),
let mut result = Vector { segment_domain: Default::default(),
point_domain: Default::default(), region_domain: Default::default(),
segment_domain: Default::default(), colinear_manipulators: Default::default(),
region_domain: Default::default(), stroke: std::mem::take(&mut element.stroke),
colinear_manipulators: Default::default(), };
stroke: std::mem::take(&mut row.element_mut().stroke), // Transfer the stroke transform from the input vector content to the result.
}; result.set_stroke_transform(*transform);
// Transfer the stroke transform from the input vector content to the result.
result.set_stroke_transform(row.attribute_cloned_or_default(ATTR_TRANSFORM));
for local_bezpath in row.element().stroke_bezpath_iter() { for local_bezpath in element.stroke_bezpath_iter() {
// Apply the transform to compute sample locations in world space (for correct distance-based spacing) // Apply the transform to compute sample locations in world space (for correct distance-based spacing)
let mut world_bezpath = local_bezpath.clone(); let mut world_bezpath = local_bezpath.clone();
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); world_bezpath.apply_affine(Affine::new(transform.to_cols_array()));
world_bezpath.apply_affine(Affine::new(transform_attribute.to_cols_array()));
// Per-segment perimeter lengths (transform-baked) for distance-based spacing // Per-segment perimeter lengths (transform-baked) for distance-based spacing
let segment_lengths: Vec<f64> = world_bezpath.segments().map(pathseg_perimeter).collect(); let segment_lengths: Vec<f64> = world_bezpath.segments().map(pathseg_perimeter).collect();
let amount = match spacing { let amount = match spacing {
PointSpacingType::Separation => separation, PointSpacingType::Separation => separation,
PointSpacingType::Quantity => quantity as f64, PointSpacingType::Quantity => quantity as f64,
}; };
// Compute sample locations using world-space distances, then evaluate positions on the untransformed bezpath. // Compute sample locations using world-space distances, then evaluate positions on the untransformed bezpath.
// This avoids needing to invert the transform (which fails when the transform is singular, e.g. zero scale). // This avoids needing to invert the transform (which fails when the transform is singular, e.g. zero scale).
let Some((locations, was_closed)) = bezpath_algorithms::compute_sample_locations(&world_bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, &segment_lengths) else { let Some((locations, was_closed)) = bezpath_algorithms::compute_sample_locations(&world_bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, &segment_lengths) else {
continue; continue;
}; };
// Evaluate the sample locations on the untransformed bezpath and append the result // Evaluate the sample locations on the untransformed bezpath and append the result
let mut sample_bezpath = BezPath::new(); let mut sample_bezpath = BezPath::new();
for &(segment_index, t) in &locations { for &(segment_index, t) in &locations {
let segment = local_bezpath.get_seg(segment_index + 1).unwrap(); let segment = local_bezpath.get_seg(segment_index + 1).unwrap();
let point = segment.eval(t); let point = segment.eval(t);
if sample_bezpath.elements().is_empty() { if sample_bezpath.elements().is_empty() {
sample_bezpath.move_to(point); sample_bezpath.move_to(point);
} else { } else {
sample_bezpath.line_to(point); sample_bezpath.line_to(point);
}
}
if was_closed {
sample_bezpath.close_path();
}
result.append_bezpath(sample_bezpath);
} }
}
if was_closed {
sample_bezpath.close_path();
}
result.append_bezpath(sample_bezpath);
}
*row.element_mut() = result; (result, Attr(*transform))
row
})
.collect()
} }
/// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance. /// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance.
@@ -2126,7 +2237,7 @@ fn tangent_on_path(
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)]
fn scatter_points( fn scatter_points(
_: impl Ctx, _: impl Ctx,
content: List<Vector>, element: Vector,
#[unit(" px")] #[unit(" px")]
#[default(10.)] #[default(10.)]
#[range] #[range]
@@ -2134,89 +2245,78 @@ fn scatter_points(
#[soft(1..100)] #[soft(1..100)]
separation: f64, separation: f64,
seed: SeedValue, seed: SeedValue,
) -> List<Vector> { ) -> Vector {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
content let mut result = Vector::default();
.into_iter()
.map(|mut row| {
let mut result = Vector::default();
let path_with_bounding_boxes: Vec<_> = row let path_with_bounding_boxes: Vec<_> = element
.element() .stroke_bezpath_iter()
.stroke_bezpath_iter() .map(|mut bezpath| {
.map(|mut bezpath| { // TODO: apply transform to points instead of modifying the paths
// TODO: apply transform to points instead of modifying the paths bezpath.close_path();
bezpath.close_path(); let bbox = bezpath.bounding_box();
let bbox = bezpath.bounding_box(); (bezpath, bbox)
(bezpath, bbox)
})
.collect();
for (i, (subpath, _)) in path_with_bounding_boxes.iter().enumerate() {
if subpath.segments().count() < 2 {
continue;
}
for point in bezpath_algorithms::poisson_disk_points(i, &path_with_bounding_boxes, separation, || rng.random::<f64>()) {
result.point_domain.push(PointId::generate(), point);
}
}
// Transfer the style from the input vector content to the result.
result.stroke = row.element().stroke.clone();
result.set_stroke_transform(DAffine2::IDENTITY);
*row.element_mut() = result;
row
}) })
.collect() .collect();
for (i, (subpath, _)) in path_with_bounding_boxes.iter().enumerate() {
if subpath.segments().count() < 2 {
continue;
}
for point in bezpath_algorithms::poisson_disk_points(i, &path_with_bounding_boxes, separation, || rng.random::<f64>()) {
result.point_domain.push(PointId::generate(), point);
}
}
// Transfer the style from the input vector content to the result.
result.stroke = element.stroke.clone();
result.set_stroke_transform(DAffine2::IDENTITY);
result
} }
#[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))]
fn spline(_: impl Ctx, content: List<Vector>) -> List<Vector> { fn spline(_: impl Ctx, element: Vector) -> Vector {
content // Exit early if there are no points to generate splines from.
.into_iter() if element.point_domain.positions().is_empty() {
.filter_map(|mut row| { return element;
// Exit early if there are no points to generate splines from. }
if row.element().point_domain.positions().is_empty() {
return None;
}
let mut segment_domain = SegmentDomain::default(); let mut segment_domain = SegmentDomain::default();
let mut next_id = SegmentId::ZERO; let mut next_id = SegmentId::ZERO;
for (manipulator_groups, closed) in row.element().stroke_manipulator_groups() { for (manipulator_groups, closed) in element.stroke_manipulator_groups() {
let positions = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::<Vec<_>>(); let positions = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::<Vec<_>>();
let closed = closed && positions.len() > 2; let closed = closed && positions.len() > 2;
// Compute control point handles for Bezier spline. // Compute control point handles for Bezier spline.
let first_handles = if closed { let first_handles = if closed {
solve_spline_first_handle_closed(&positions) solve_spline_first_handle_closed(&positions)
} else { } else {
solve_spline_first_handle_open(&positions) solve_spline_first_handle_open(&positions)
}; };
let stroke_id = StrokeId::ZERO; let stroke_id = StrokeId::ZERO;
// Create segments with computed Bezier handles and add them to the output vector element's segment domain. // Create segments with computed Bezier handles and add them to the output vector element's segment domain.
for i in 0..(positions.len() - if closed { 0 } else { 1 }) { for i in 0..(positions.len() - if closed { 0 } else { 1 }) {
let next_index = (i + 1) % positions.len(); let next_index = (i + 1) % positions.len();
let start_index = row.element().point_domain.resolve_id(manipulator_groups[i].id).unwrap(); let start_index = element.point_domain.resolve_id(manipulator_groups[i].id).unwrap();
let end_index = row.element().point_domain.resolve_id(manipulator_groups[next_index].id).unwrap(); let end_index = element.point_domain.resolve_id(manipulator_groups[next_index].id).unwrap();
let handle_start = first_handles[i]; let handle_start = first_handles[i];
let handle_end = positions[next_index] * 2. - first_handles[next_index]; let handle_end = positions[next_index] * 2. - first_handles[next_index];
let handles = BezierHandles::Cubic { handle_start, handle_end }; let handles = BezierHandles::Cubic { handle_start, handle_end };
segment_domain.push(next_id.next_id(), start_index, end_index, handles, stroke_id); segment_domain.push(next_id.next_id(), start_index, end_index, handles, stroke_id);
} }
} }
row.element_mut().segment_domain = segment_domain; let mut element = element;
Some(row) element.segment_domain = segment_domain;
}) element
.collect()
} }
/// Computes the inverse of a transform's linear (matrix2) part, handling singular transforms /// Computes the inverse of a transform's linear (matrix2) part, handling singular transforms
@@ -2277,7 +2377,7 @@ fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine
fn jitter_points( fn jitter_points(
_: impl Ctx, _: impl Ctx,
/// The vector geometry with points to be jittered. /// The vector geometry with points to be jittered.
content: List<Vector>, (element, transform): (Vector, Attr<TransformAttr>),
/// The maximum extent of the random distance each point can be offset. /// The maximum extent of the random distance each point can be offset.
#[default(5.)] #[default(5.)]
#[unit(" px")] #[unit(" px")]
@@ -2287,38 +2387,32 @@ fn jitter_points(
/// Whether to offset anchor points along their normal direction (perpendicular to the path) or in a random direction. Free-floating and branching points have no normal direction, so they receive a random-angled offset regardless of this setting. /// Whether to offset anchor points along their normal direction (perpendicular to the path) or in a random direction. Free-floating and branching points have no normal direction, so they receive a random-angled offset regardless of this setting.
#[default(true)] #[default(true)]
along_normals: bool, along_normals: bool,
) -> List<Vector> { ) -> (Vector, Attr<TransformAttr>) {
content let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
.into_iter() let inverse_linear = inverse_linear_or_repair(transform.matrix2);
.map(|mut row| {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2);
let deltas: Vec<_> = (0..row.element().point_domain.positions().len()) let deltas: Vec<_> = (0..element.point_domain.positions().len())
.map(|point_index| { .map(|point_index| {
let normal = if along_normals { let normal = if along_normals {
row.element().segment_domain.point_tangent(point_index, row.element().point_domain.positions()).map(|t| -t.perp()) element.segment_domain.point_tangent(point_index, element.point_domain.positions()).map(|t| -t.perp())
} else { } else {
None None
}; };
let offset = if let Some(normal) = normal { let offset = if let Some(normal) = normal {
normal * (rng.random::<f64>() * 2. - 1.) normal * (rng.random::<f64>() * 2. - 1.)
} else { } else {
DVec2::from_angle(rng.random::<f64>() * TAU) * rng.random::<f64>() DVec2::from_angle(rng.random::<f64>() * TAU) * rng.random::<f64>()
}; };
inverse_linear * offset * max_distance inverse_linear * offset * max_distance
})
.collect();
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
apply_point_deltas(row.element_mut(), &deltas, transform);
row
}) })
.collect() .collect();
let mut element = element;
apply_point_deltas(&mut element, &deltas, *transform);
(element, Attr(*transform))
} }
/// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance. /// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance.
@@ -3495,34 +3589,10 @@ mod test {
assert_eq!(manipulator_groups_anchors[i], expected_bounding_box[i]); assert_eq!(manipulator_groups_anchors[i], expected_bounding_box[i]);
} }
} }
#[test]
fn copy_to_points() {
let points = Rect::new(-10., -10., 10., 10.).to_path(DEFAULT_ACCURACY);
let element = Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY);
let expected_points = Vector::from_bezpath(points.clone()).point_domain.positions().to_vec();
let copy_to_points = super::copy_to_points(&Footprint::default(), vector_node_from_bezpath(points), vector_node_from_bezpath(element), 1., 1., 0., 0, 0., 0);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let (flattened_copy_to_points, ..) = super::flatten_path_core(&arena, copy_to_points.into_graphic_list()).unwrap();
assert_eq!(flattened_copy_to_points.region_manipulator_groups().count(), expected_points.len());
for (index, (_, manipulator_groups)) in flattened_copy_to_points.region_manipulator_groups().enumerate() {
let offset = expected_points[index];
let manipulator_groups_anchors = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::<Vec<DVec2>>();
assert_eq!(
&manipulator_groups_anchors,
&[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),]
);
}
}
#[test]
fn sample_polyline() { fn sample_polyline() {
let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
let sample_polyline = super::sample_polyline(&Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 30., 0, 0., 0., false); let (sample_polyline, _) = super::sample_polyline(&Footprint::default(), (Vector::from_bezpath(path), Attr(DAffine2::IDENTITY)), PointSpacingType::Separation, 30., 0, 0., 0., false);
let sample_polyline = sample_polyline.element(0).unwrap(); let sample_polyline = &sample_polyline;
assert_eq!(sample_polyline.point_domain.positions().len(), 4); assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
@@ -3531,8 +3601,8 @@ mod test {
#[test] #[test]
fn sample_polyline_adaptive_spacing() { fn sample_polyline_adaptive_spacing() {
let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
let sample_polyline = super::sample_polyline(&Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 18., 0, 45., 10., true); let (sample_polyline, _) = super::sample_polyline(&Footprint::default(), (Vector::from_bezpath(path), Attr(DAffine2::IDENTITY)), PointSpacingType::Separation, 18., 0, 45., 10., true);
let sample_polyline = sample_polyline.element(0).unwrap(); let sample_polyline = &sample_polyline;
assert_eq!(sample_polyline.point_domain.positions().len(), 4); assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
@@ -3542,11 +3612,11 @@ mod test {
fn poisson() { fn poisson() {
let poisson_points = super::scatter_points( let poisson_points = super::scatter_points(
&Footprint::default(), &Footprint::default(),
vector_node_from_bezpath(Ellipse::from_rect(Rect::new(-50., -50., 50., 50.)).to_path(DEFAULT_ACCURACY)), Vector::from_bezpath(Ellipse::from_rect(Rect::new(-50., -50., 50., 50.)).to_path(DEFAULT_ACCURACY)),
10. * std::f64::consts::SQRT_2, 10. * std::f64::consts::SQRT_2,
0, 0,
); );
let poisson_points = poisson_points.element(0).unwrap(); let poisson_points = &poisson_points;
assert!( assert!(
(20..=40).contains(&poisson_points.point_domain.positions().len()), (20..=40).contains(&poisson_points.point_domain.positions().len()),
"actual len {}", "actual len {}",
@@ -3570,8 +3640,8 @@ mod test {
} }
#[test] #[test]
fn spline() { fn spline() {
let spline = super::spline(&Footprint::default(), vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))); let spline = super::spline(&Footprint::default(), Vector::from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)));
let spline = spline.element(0).unwrap(); let spline = &spline;
assert_eq!(spline.stroke_bezpath_iter().count(), 1); assert_eq!(spline.stroke_bezpath_iter().count(), 1);
assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]);
} }