mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Make Copy to Points and (Circular) Repeat and nodes output group data, and add flattening nodes (#2011)
* Output group from repeat, add flatten vector elements * Fix tests * Fix demo artwork * Output group from copy to points, add repeat for graphic groups, fix editor freeze on render fail * Restore painted dreams * WIP: Fix demo artwork * Fix demo artwork, add ungroup node * Incorrect scaling * fix test * Fix demo art --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -315,6 +315,48 @@ async fn to_group<F: 'n + Send, Data: Into<GraphicGroup> + 'n>(
|
||||
element.eval(footprint).await.into()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("General"))]
|
||||
async fn flatten_group<F: 'n + Send>(
|
||||
#[implementations(
|
||||
(),
|
||||
Footprint,
|
||||
)]
|
||||
footprint: F,
|
||||
#[implementations(
|
||||
() -> GraphicGroup,
|
||||
Footprint -> GraphicGroup,
|
||||
)]
|
||||
group: impl Node<F, Output = GraphicGroup>,
|
||||
fully_flatten: bool,
|
||||
) -> GraphicGroup {
|
||||
let nested_group = group.eval(footprint).await;
|
||||
let mut flat_group = GraphicGroup::EMPTY;
|
||||
fn flatten_group(result_group: &mut GraphicGroup, current_group: GraphicGroup, fully_flatten: bool) {
|
||||
let mut collection_group = GraphicGroup::EMPTY;
|
||||
for (element, reference) in current_group.elements {
|
||||
if let GraphicElement::GraphicGroup(mut nested_group) = element {
|
||||
nested_group.transform *= current_group.transform;
|
||||
let mut sub_group = GraphicGroup::EMPTY;
|
||||
if fully_flatten {
|
||||
flatten_group(&mut sub_group, nested_group, fully_flatten);
|
||||
} else {
|
||||
for (collection_element, _) in &mut nested_group.elements {
|
||||
*collection_element.transform_mut() = nested_group.transform * collection_element.transform();
|
||||
}
|
||||
sub_group = nested_group;
|
||||
}
|
||||
collection_group.append(&mut sub_group.elements);
|
||||
} else {
|
||||
collection_group.push((element, reference));
|
||||
}
|
||||
}
|
||||
|
||||
result_group.append(&mut collection_group.elements);
|
||||
}
|
||||
flatten_group(&mut flat_group, nested_group, fully_flatten);
|
||||
flat_group
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn to_artboard<F: 'n + Send + ApplyTransform, Data: Into<GraphicGroup> + 'n>(
|
||||
#[implementations(
|
||||
|
||||
@@ -266,7 +266,7 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
|
||||
|
||||
// TODO: Click targets can be removed from the render output, since the vector data is available in the vector modify data from Monitor nodes.
|
||||
// This will require that the transform for child layers into that layer space be calculated, or it could be returned from the RenderOutput instead of click targets.
|
||||
#[derive(Debug, Clone, PartialEq, DynAny)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct RenderMetadata {
|
||||
pub footprints: HashMap<NodeId, (Footprint, DAffine2)>,
|
||||
@@ -301,6 +301,12 @@ pub trait GraphicElementRendered {
|
||||
fn contains_artboard(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {}
|
||||
|
||||
fn to_graphic_element(&self) -> GraphicElement {
|
||||
GraphicElement::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphicElementRendered for GraphicGroup {
|
||||
@@ -393,6 +399,16 @@ impl GraphicElementRendered for GraphicGroup {
|
||||
fn contains_artboard(&self) -> bool {
|
||||
self.iter().any(|(element, _)| element.contains_artboard())
|
||||
}
|
||||
|
||||
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
|
||||
for (element, node_id) in self.elements.iter_mut() {
|
||||
element.new_ids_from_hash(*node_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn to_graphic_element(&self) -> GraphicElement {
|
||||
GraphicElement::GraphicGroup(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphicElementRendered for VectorData {
|
||||
@@ -582,6 +598,14 @@ impl GraphicElementRendered for VectorData {
|
||||
scene.pop_layer();
|
||||
}
|
||||
}
|
||||
|
||||
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
|
||||
self.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default());
|
||||
}
|
||||
|
||||
fn to_graphic_element(&self) -> GraphicElement {
|
||||
GraphicElement::VectorData(Box::new(self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphicElementRendered for Artboard {
|
||||
@@ -948,6 +972,14 @@ impl GraphicElementRendered for GraphicElement {
|
||||
GraphicElement::Raster(raster) => raster.contains_artboard(),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
|
||||
match self {
|
||||
GraphicElement::VectorData(vector_data) => vector_data.new_ids_from_hash(reference),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.new_ids_from_hash(reference),
|
||||
GraphicElement::Raster(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to stop rust complaining about upstream traits adding display implementations to `Option<Color>`. This would not be an issue as we control that crate.
|
||||
|
||||
@@ -114,7 +114,6 @@ fn spline<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, _primar
|
||||
}
|
||||
|
||||
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
fn path<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, path_data: Vec<Subpath<PointId>>, colinear_manipulators: Vec<PointId>) -> super::VectorData {
|
||||
let mut vector_data = super::VectorData::from_subpaths(path_data, false);
|
||||
|
||||
@@ -4,6 +4,7 @@ use dyn_any::DynAny;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// A simple macro for creating strongly typed ids (to avoid confusion when passing around ids).
|
||||
macro_rules! create_ids {
|
||||
@@ -22,6 +23,14 @@ macro_rules! create_ids {
|
||||
Self(crate::uuid::generate_uuid())
|
||||
}
|
||||
|
||||
pub fn generate_from_hash(self, node_id: u64) -> Self {
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
node_id.hash(&mut hasher);
|
||||
self.hash(&mut hasher);
|
||||
let hash_value = hasher.finish();
|
||||
Self(hash_value)
|
||||
}
|
||||
|
||||
/// Gets the inner raw value.
|
||||
pub fn inner(self) -> u64 {
|
||||
self.0
|
||||
@@ -161,6 +170,10 @@ impl PointDomain {
|
||||
self.positions.extend(other.positions.iter().map(|&pos| transform.transform_point2(pos)));
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.id.iter_mut().for_each(|id| *id = *id_map.point_map.get(id).unwrap_or(id));
|
||||
}
|
||||
|
||||
fn transform(&mut self, transform: DAffine2) {
|
||||
for pos in &mut self.positions {
|
||||
*pos = transform.transform_point2(*pos);
|
||||
@@ -353,6 +366,10 @@ impl SegmentDomain {
|
||||
self.stroke.extend(&other.stroke);
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.ids.iter_mut().for_each(|id| *id = *id_map.segment_map.get(id).unwrap_or(id));
|
||||
}
|
||||
|
||||
fn transform(&mut self, transform: DAffine2) {
|
||||
for handles in &mut self.handles {
|
||||
*handles = handles.apply_transformation(|p| transform.transform_point2(p));
|
||||
@@ -460,6 +477,13 @@ impl RegionDomain {
|
||||
);
|
||||
self.fill.extend(&other.fill);
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.ids.iter_mut().for_each(|id| *id = *id_map.region_map.get(id).unwrap_or(id));
|
||||
self.segment_range
|
||||
.iter_mut()
|
||||
.for_each(|range| *range = *id_map.segment_map.get(range.start()).unwrap_or(range.start())..=*id_map.segment_map.get(range.end()).unwrap_or(range.end()));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::VectorData {
|
||||
@@ -590,6 +614,23 @@ impl super::VectorData {
|
||||
self.point_domain.transform(transform);
|
||||
self.segment_domain.transform(transform);
|
||||
}
|
||||
|
||||
pub fn vector_new_ids_from_hash(&mut self, node_id: u64) {
|
||||
let point_map = self.point_domain.ids().iter().map(|&old| (old, old.generate_from_hash(node_id))).collect::<HashMap<_, _>>();
|
||||
let segment_map = self.segment_domain.ids().iter().map(|&old| (old, old.generate_from_hash(node_id))).collect::<HashMap<_, _>>();
|
||||
let region_map = self.region_domain.ids().iter().map(|&old| (old, old.generate_from_hash(node_id))).collect::<HashMap<_, _>>();
|
||||
|
||||
let id_map = IdMap {
|
||||
point_offset: self.point_domain.ids().len(),
|
||||
point_map,
|
||||
segment_map,
|
||||
region_map,
|
||||
};
|
||||
|
||||
self.point_domain.map_ids(&id_map);
|
||||
self.segment_domain.map_ids(&id_map);
|
||||
self.region_domain.map_ids(&id_map);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
|
||||
use crate::renderer::GraphicElementRendered;
|
||||
use crate::transform::{Footprint, Transform, TransformMut};
|
||||
use crate::vector::style::LineJoin;
|
||||
use crate::{Color, GraphicGroup};
|
||||
use crate::{Color, GraphicElement, GraphicGroup};
|
||||
|
||||
use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue};
|
||||
use glam::{DAffine2, DVec2};
|
||||
@@ -164,95 +164,188 @@ async fn stroke<F: 'n + Send, T: Into<Option<Color>> + 'n + Send>(
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn repeat<F: 'n + Send>(
|
||||
async fn repeat<F: 'n + Send + Copy, I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
|
||||
#[implementations(
|
||||
(),
|
||||
(),
|
||||
Footprint,
|
||||
Footprint,
|
||||
)]
|
||||
footprint: F,
|
||||
// TODO: Implement other GraphicElementRendered types.
|
||||
#[implementations(
|
||||
() -> VectorData,
|
||||
() -> GraphicGroup,
|
||||
Footprint -> VectorData,
|
||||
Footprint -> GraphicGroup,
|
||||
)]
|
||||
instance: impl Node<F, Output = VectorData>,
|
||||
instance: impl Node<F, Output = I>,
|
||||
#[default(100., 100.)]
|
||||
// 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: DVec2,
|
||||
angle: Angle,
|
||||
#[default(4)] instances: IntegerCount,
|
||||
) -> VectorData {
|
||||
) -> GraphicGroup {
|
||||
let instance = instance.eval(footprint).await;
|
||||
let first_vector_transform = instance.transform();
|
||||
|
||||
let angle = angle.to_radians();
|
||||
let instances = instances.max(1);
|
||||
let total = (instances - 1) as f64;
|
||||
|
||||
if instances == 1 {
|
||||
return instance;
|
||||
}
|
||||
let mut result = GraphicGroup::EMPTY;
|
||||
|
||||
// Repeat the vector data
|
||||
let mut result = VectorData::empty();
|
||||
|
||||
let Some(bounding_box) = instance.bounding_box_with_transform(instance.transform) else {
|
||||
return instance;
|
||||
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else {
|
||||
return result;
|
||||
};
|
||||
|
||||
let center = (bounding_box[0] + bounding_box[1]) / 2.;
|
||||
|
||||
for i in 0..instances {
|
||||
let translation = i as f64 * direction / total;
|
||||
let angle = i as f64 * angle / total;
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
let modification = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
|
||||
|
||||
let transform = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
|
||||
|
||||
result.concat(&instance, transform);
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = modification * first_vector_transform;
|
||||
result.push((new_instance, None));
|
||||
}
|
||||
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn circular_repeat<F: 'n + Send>(
|
||||
async fn circular_repeat<F: 'n + Send + Copy, I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
|
||||
#[implementations(
|
||||
(),
|
||||
(),
|
||||
Footprint,
|
||||
Footprint,
|
||||
)]
|
||||
footprint: F,
|
||||
// TODO: Implement other GraphicElementRendered types.
|
||||
#[implementations(
|
||||
() -> VectorData,
|
||||
() -> GraphicGroup,
|
||||
Footprint -> VectorData,
|
||||
Footprint -> GraphicGroup,
|
||||
)]
|
||||
instance: impl Node<F, Output = I>,
|
||||
angle_offset: Angle,
|
||||
#[default(5)] radius: Length,
|
||||
#[default(5)] instances: IntegerCount,
|
||||
) -> GraphicGroup {
|
||||
let instance = instance.eval(footprint).await;
|
||||
let first_vector_transform = instance.transform();
|
||||
let instances = instances.max(1);
|
||||
|
||||
let mut result = GraphicGroup::EMPTY;
|
||||
|
||||
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else {
|
||||
return result;
|
||||
};
|
||||
|
||||
let center = (bounding_box[0] + bounding_box[1]) / 2.;
|
||||
let base_transform = DVec2::new(0., radius) - center;
|
||||
|
||||
for i in 0..instances {
|
||||
let angle = (std::f64::consts::TAU / instances as f64) * i as f64 + angle_offset.to_radians();
|
||||
let rotation = DAffine2::from_angle(angle);
|
||||
let modification = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = modification * first_vector_transform;
|
||||
result.push((new_instance, None));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn copy_to_points<F: 'n + Send + Copy, I: GraphicElementRendered + ConcatElement + TransformMut + Send + 'n>(
|
||||
#[implementations(
|
||||
(),
|
||||
(),
|
||||
Footprint,
|
||||
)]
|
||||
footprint: F,
|
||||
#[implementations(
|
||||
() -> VectorData,
|
||||
() -> VectorData,
|
||||
Footprint -> VectorData,
|
||||
)]
|
||||
instance: impl Node<F, Output = VectorData>,
|
||||
angle_offset: Angle,
|
||||
#[default(5)] radius: Length,
|
||||
#[default(5)] instances: IntegerCount,
|
||||
) -> VectorData {
|
||||
points: impl Node<F, Output = VectorData>,
|
||||
#[expose]
|
||||
#[implementations(
|
||||
() -> VectorData,
|
||||
() -> GraphicGroup,
|
||||
Footprint -> VectorData,
|
||||
Footprint -> GraphicGroup,
|
||||
)]
|
||||
instance: impl Node<F, Output = I>,
|
||||
#[default(1)] random_scale_min: f64,
|
||||
#[default(1)] random_scale_max: f64,
|
||||
random_scale_bias: f64,
|
||||
random_scale_seed: SeedValue,
|
||||
random_rotation: Angle,
|
||||
random_rotation_seed: SeedValue,
|
||||
) -> GraphicGroup {
|
||||
let points = points.eval(footprint).await;
|
||||
let instance = instance.eval(footprint).await;
|
||||
let instances = instances.max(1);
|
||||
let instance_transform = instance.transform();
|
||||
|
||||
if instances == 1 {
|
||||
return instance;
|
||||
let random_scale_difference = random_scale_max - random_scale_min;
|
||||
|
||||
let points_list = points.point_domain.positions();
|
||||
|
||||
let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default();
|
||||
let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]);
|
||||
|
||||
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 do_scale = random_scale_difference.abs() > 1e-6;
|
||||
let do_rotation = random_rotation.abs() > 1e-6;
|
||||
|
||||
let mut result = GraphicGroup::default();
|
||||
for &point in points_list {
|
||||
let center_transform = DAffine2::from_translation(instance_center);
|
||||
|
||||
let translation = points.transform.transform_point2(point);
|
||||
|
||||
let rotation = if do_rotation {
|
||||
let degrees = (rotation_rng.gen::<f64>() - 0.5) * random_rotation;
|
||||
degrees / 360. * std::f64::consts::TAU
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
let scale = if do_scale {
|
||||
if random_scale_bias.abs() < 1e-6 {
|
||||
// Linear
|
||||
random_scale_min + scale_rng.gen::<f64>() * random_scale_difference
|
||||
} else {
|
||||
// Weighted (see <https://www.desmos.com/calculator/gmavd3m9bd>)
|
||||
let horizontal_scale_factor = 1. - 2_f64.powf(random_scale_bias);
|
||||
let scale_factor = (1. - scale_rng.gen::<f64>() * horizontal_scale_factor).log2() / random_scale_bias;
|
||||
random_scale_min + scale_factor * random_scale_difference
|
||||
}
|
||||
} else {
|
||||
random_scale_min
|
||||
};
|
||||
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform * instance_transform;
|
||||
result.push((new_instance, None));
|
||||
}
|
||||
|
||||
let mut result = VectorData::empty();
|
||||
|
||||
let Some(bounding_box) = instance.bounding_box_with_transform(instance.transform) else {
|
||||
return instance;
|
||||
};
|
||||
let center = (bounding_box[0] + bounding_box[1]) / 2.;
|
||||
|
||||
let base_transform = DVec2::new(0., radius) - center;
|
||||
|
||||
for i in 0..instances {
|
||||
let angle = (std::f64::consts::TAU / instances as f64) * i as f64 + angle_offset.to_radians();
|
||||
let rotation = DAffine2::from_angle(angle);
|
||||
let transform = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
|
||||
result.concat(&instance, transform);
|
||||
}
|
||||
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -381,6 +474,42 @@ async fn solidify_stroke<F: 'n + Send>(
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn flatten_vector_elements<F: 'n + Send>(
|
||||
#[implementations(
|
||||
(),
|
||||
Footprint,
|
||||
)]
|
||||
footprint: F,
|
||||
#[implementations(
|
||||
() -> GraphicGroup,
|
||||
Footprint -> GraphicGroup,
|
||||
)]
|
||||
graphic_group_input: impl Node<F, Output = GraphicGroup>,
|
||||
) -> VectorData {
|
||||
let graphic_group = graphic_group_input.eval(footprint).await;
|
||||
|
||||
fn concat_group(graphic_group: &GraphicGroup, current_transform: DAffine2, result: &mut VectorData) {
|
||||
for (element, _) in graphic_group.iter() {
|
||||
match element {
|
||||
GraphicElement::VectorData(vector_data) => {
|
||||
result.concat(vector_data, current_transform);
|
||||
}
|
||||
GraphicElement::GraphicGroup(graphic_group) => {
|
||||
concat_group(graphic_group, current_transform * graphic_group.transform, result);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = VectorData::empty();
|
||||
concat_group(&graphic_group, DAffine2::IDENTITY, &mut result);
|
||||
// TODO: This leads to incorrect stroke widths when flattening groups with different transforms.
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
result
|
||||
}
|
||||
|
||||
pub trait ConcatElement {
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2);
|
||||
}
|
||||
@@ -396,84 +525,6 @@ impl ConcatElement for GraphicGroup {
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn copy_to_points<F: 'n + Send + Copy, I: GraphicElementRendered + Default + ConcatElement + TransformMut + Send>(
|
||||
#[implementations(
|
||||
(),
|
||||
(),
|
||||
Footprint,
|
||||
)]
|
||||
footprint: F,
|
||||
#[implementations(
|
||||
() -> VectorData,
|
||||
() -> VectorData,
|
||||
Footprint -> VectorData,
|
||||
)]
|
||||
points: impl Node<F, Output = VectorData>,
|
||||
#[expose]
|
||||
#[implementations(
|
||||
() -> VectorData,
|
||||
() -> GraphicGroup,
|
||||
Footprint -> VectorData,
|
||||
Footprint -> GraphicGroup,
|
||||
)]
|
||||
instance: impl Node<F, Output = I>,
|
||||
#[default(1)] random_scale_min: f64,
|
||||
#[default(1)] random_scale_max: f64,
|
||||
random_scale_bias: f64,
|
||||
random_scale_seed: SeedValue,
|
||||
random_rotation: Angle,
|
||||
random_rotation_seed: SeedValue,
|
||||
) -> I {
|
||||
let points = points.eval(footprint).await;
|
||||
let instance = instance.eval(footprint).await;
|
||||
|
||||
let random_scale_difference = random_scale_max - random_scale_min;
|
||||
|
||||
let points_list = points.point_domain.positions();
|
||||
|
||||
let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default();
|
||||
let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]);
|
||||
|
||||
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 do_scale = random_scale_difference.abs() > 1e-6;
|
||||
let do_rotation = random_rotation.abs() > 1e-6;
|
||||
|
||||
let mut result = I::default();
|
||||
for &point in points_list {
|
||||
let center_transform = DAffine2::from_translation(instance_center);
|
||||
|
||||
let translation = points.transform.transform_point2(point);
|
||||
|
||||
let rotation = if do_rotation {
|
||||
let degrees = (rotation_rng.gen::<f64>() - 0.5) * random_rotation;
|
||||
degrees / 360. * std::f64::consts::TAU
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
let scale = if do_scale {
|
||||
if random_scale_bias.abs() < 1e-6 {
|
||||
// Linear
|
||||
random_scale_min + scale_rng.gen::<f64>() * random_scale_difference
|
||||
} else {
|
||||
// Weighted (see <https://www.desmos.com/calculator/gmavd3m9bd>)
|
||||
let horizontal_scale_factor = 1. - 2_f64.powf(random_scale_bias);
|
||||
let scale_factor = (1. - scale_rng.gen::<f64>() * horizontal_scale_factor).log2() / random_scale_bias;
|
||||
random_scale_min + scale_factor * random_scale_difference
|
||||
}
|
||||
} else {
|
||||
random_scale_min
|
||||
};
|
||||
|
||||
result.concat(&instance, DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn sample_points<F: 'n + Send + Copy>(
|
||||
#[implementations(
|
||||
@@ -622,7 +673,7 @@ async fn subpath_segment_lengths<F: 'n + Send>(
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Splines from Points"), category(""), path(graphene_core::vector))]
|
||||
#[node_macro::node(name("Splines from Points"), category("Vector"), path(graphene_core::vector))]
|
||||
fn splines_from_points(_: (), mut vector_data: VectorData) -> VectorData {
|
||||
let points = &vector_data.point_domain;
|
||||
|
||||
@@ -838,8 +889,9 @@ mod test {
|
||||
let direction = DVec2::X * 1.5;
|
||||
let instances = 3;
|
||||
let repeated = super::repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
|
||||
assert_eq!(repeated.region_bezier_paths().count(), 3);
|
||||
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 3);
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
|
||||
}
|
||||
}
|
||||
@@ -848,16 +900,18 @@ mod test {
|
||||
let direction = DVec2::new(12., 10.);
|
||||
let instances = 8;
|
||||
let repeated = super::repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
|
||||
assert_eq!(repeated.region_bezier_paths().count(), 8);
|
||||
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 8);
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn circle_repeat() {
|
||||
let repeated = super::circular_repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
|
||||
assert_eq!(repeated.region_bezier_paths().count(), 8);
|
||||
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 8);
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
let expected_angle = (index as f64 + 1.) * 45.;
|
||||
let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
|
||||
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
|
||||
@@ -893,9 +947,10 @@ mod test {
|
||||
let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.);
|
||||
let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE);
|
||||
let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec();
|
||||
let bounding_box = super::copy_to_points(Footprint::default(), &vector_node(points), &vector_node(instance), 1., 1., 0., 0, 0., 0).await;
|
||||
assert_eq!(bounding_box.region_bezier_paths().count(), expected_points.len());
|
||||
for (index, (_, subpath)) in bounding_box.region_bezier_paths().enumerate() {
|
||||
let copy_to_points = super::copy_to_points(Footprint::default(), &vector_node(points), &vector_node(instance), 1., 1., 0., 0, 0., 0).await;
|
||||
let flattened_copy_to_points = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(copy_to_points)).await;
|
||||
assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len());
|
||||
for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() {
|
||||
let offset = expected_points[index];
|
||||
assert_eq!(
|
||||
&subpath.anchors()[..4],
|
||||
|
||||
Reference in New Issue
Block a user