Rename the repeat nodes to replace "Instance" terminology with "Repeat" (#3794)

* WIP

* Move the Mirror node from the module 'vector' to 'graphic'

* Update demo art

* Fix failing tests

Fix tests
This commit is contained in:
Keavon Chambers 2026-02-20 22:10:59 -08:00 committed by GitHub
parent 5a1503fc98
commit 7ca6470656
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 482 additions and 373 deletions

22
Cargo.lock generated
View File

@ -2295,6 +2295,7 @@ dependencies = [
"path-bool-nodes",
"raster-nodes",
"rendering",
"repeat-nodes",
"reqwest",
"text-nodes",
"tokio",
@ -5078,6 +5079,25 @@ dependencies = [
"vello",
]
[[package]]
name = "repeat-nodes"
version = "0.1.0"
dependencies = [
"core-types",
"dyn-any",
"glam",
"graphene-core",
"graphic-types",
"kurbo 0.12.0",
"log",
"node-macro",
"raster-types",
"serde",
"tokio",
"vector-nodes",
"vector-types",
]
[[package]]
name = "reqwest"
version = "0.12.23"
@ -6431,6 +6451,7 @@ dependencies = [
"glam",
"graphic-types",
"node-macro",
"rand 0.9.2",
"serde",
"vector-types",
]
@ -6734,6 +6755,7 @@ dependencies = [
"node-macro",
"qrcodegen",
"rand 0.9.2",
"repeat-nodes",
"rustc-hash 2.1.1",
"serde",
"tokio",

View File

@ -31,6 +31,7 @@ members = [
"node-graph/nodes/raster/shaders/entrypoint",
"node-graph/nodes/text",
"node-graph/nodes/transform",
"node-graph/nodes/repeat",
"node-graph/nodes/vector",
"node-graph/graph-craft",
"node-graph/graphene-cli",
@ -116,6 +117,7 @@ graphic-nodes = { path = "node-graph/nodes/graphic" }
text-nodes = { path = "node-graph/nodes/text" }
transform-nodes = { path = "node-graph/nodes/transform" }
vector-nodes = { path = "node-graph/nodes/vector" }
repeat-nodes = { path = "node-graph/nodes/repeat" }
math-nodes = { path = "node-graph/nodes/math" }
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
graph-craft = { path = "node-graph/graph-craft" }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -502,10 +502,10 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
// [11:Equals]0 -> 0[17:Switch]
// [9:Transform]0 -> 1[17:Switch]
// [16:Morph]0 -> 2[17:Switch]
// [17:Switch]0 -> 0[18:Instance Repeat]
// [0:Floor]0 -> 1[18:Instance Repeat]
// [IMPORTS]3 -> 2[18:Instance Repeat]
// [18:Instance Repeat]0 -> 0[EXPORTS]
// [17:Switch]0 -> 0[18:Repeat]
// [0:Floor]0 -> 1[18:Repeat]
// [IMPORTS]3 -> 2[18:Repeat]
// [18:Repeat]0 -> 0[EXPORTS]
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
@ -635,9 +635,9 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
inputs: vec![NodeInput::node(NodeId(11), 0), NodeInput::node(NodeId(9), 0), NodeInput::node(NodeId(16), 0)],
..Default::default()
},
// 18: Instance Repeat
// 18: Repeat
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_repeat::IDENTIFIER),
implementation: DocumentNodeImplementation::ProtoNode(repeat_nodes::repeat::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(17), 0), NodeInput::node(NodeId(0), 0), NodeInput::import(generic!(T), 3)],
..Default::default()
},
@ -807,7 +807,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
},
..Default::default()
},
// 18: Instance Repeat
// 18: Repeat
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(49, -1)),

View File

@ -753,18 +753,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::vector::centroid::IDENTIFIER,
aliases: &["graphene_core::vector::CentroidNode"],
},
NodeReplacement {
node: graphene_std::vector::circular_repeat::IDENTIFIER,
aliases: &["graphene_core::vector::CircularRepeatNode"],
},
NodeReplacement {
node: graphene_std::vector::close_path::IDENTIFIER,
aliases: &["graphene_core::vector::ClosePathNode"],
},
NodeReplacement {
node: graphene_std::vector::copy_to_points::IDENTIFIER,
aliases: &["graphene_core::vector::CopyToPointsNode"],
},
NodeReplacement {
node: graphene_std::vector::count_elements::IDENTIFIER,
aliases: &["graphene_core::vector::CountElementsNode"],
@ -841,22 +833,30 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::graphic::map::IDENTIFIER,
aliases: &["graphene_core::vector::InstanceMapNode"],
},
NodeReplacement {
node: graphene_std::vector::instance_on_points::IDENTIFIER,
aliases: &["graphene_core::vector::InstanceOnPointsNode"],
},
NodeReplacement {
node: graphene_std::context::read_position::IDENTIFIER,
aliases: &["graphene_core::vector::InstancePositionNode", "core_types::vector::InstancePositionNode"],
},
NodeReplacement {
node: graphene_std::vector::instance_repeat::IDENTIFIER,
aliases: &["graphene_core::vector::InstanceRepeatNode"],
},
NodeReplacement {
node: graphene_std::context::read_vector::IDENTIFIER,
aliases: &["graphene_core::vector::InstanceVectorNode"],
},
NodeReplacement {
node: graphene_std::repeat::repeat::IDENTIFIER,
aliases: &["graphene_core::vector::InstanceRepeatNode", "core_types::vector::InstanceRepeatNode"],
},
NodeReplacement {
node: graphene_std::repeat::repeat_array::IDENTIFIER,
aliases: &["graphene_core::vector::RepeatNode", "core_types::vector::RepeatNode"],
},
NodeReplacement {
node: graphene_std::repeat::repeat_radial::IDENTIFIER,
aliases: &["graphene_core::vector::CircularRepeatNode", "core_types::vector::CircularRepeatNode"],
},
NodeReplacement {
node: graphene_std::repeat::repeat_on_points::IDENTIFIER,
aliases: &["graphene_core::vector::InstanceOnPointsNode", "core_types::vector::InstanceOnPointsNode"],
},
NodeReplacement {
node: graphene_std::vector::jitter_points::IDENTIFIER,
aliases: &["graphene_core::vector::JitterPointsNode"],
@ -866,8 +866,8 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
aliases: &["graphene_core::vector::MergeByDistanceNode"],
},
NodeReplacement {
node: graphene_std::vector::mirror::IDENTIFIER,
aliases: &["graphene_core::vector::MirrorNode"],
node: graphene_std::graphic::mirror::IDENTIFIER,
aliases: &["graphene_core::vector::MirrorNode", "core_types::vector::MirrorNode"],
},
NodeReplacement {
node: graphene_std::vector::morph::IDENTIFIER,
@ -905,10 +905,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::vector::position_on_path::IDENTIFIER,
aliases: &["graphene_core::vector::PositionOnPathNode"],
},
NodeReplacement {
node: graphene_std::vector::repeat::IDENTIFIER,
aliases: &["graphene_core::vector::RepeatNode"],
},
NodeReplacement {
node: graphene_std::vector::round_corners::IDENTIFIER,
aliases: &["graphene_core::vector::RoundCornersNode"],
@ -1337,7 +1333,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
}
// Upgrade the Mirror node to add the `keep_original` boolean input
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::mirror::IDENTIFIER) && inputs_count == 3 {
if reference == DefinitionIdentifier::ProtoNode(graphene_std::graphic::mirror::IDENTIFIER) && inputs_count == 3 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
@ -1352,7 +1348,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
}
// Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64`
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::mirror::IDENTIFIER) && inputs_count == 4 {
if reference == DefinitionIdentifier::ProtoNode(graphene_std::graphic::mirror::IDENTIFIER) && inputs_count == 4 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
@ -1404,7 +1400,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
}
}
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::instance_on_points::IDENTIFIER) && inputs_count == 2 {
if reference == DefinitionIdentifier::ProtoNode(graphene_std::repeat::repeat_on_points::IDENTIFIER) && inputs_count == 2 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);

View File

@ -1,4 +1,5 @@
use core_types::registry::types::SignedInteger;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::registry::types::{Angle, SignedInteger};
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use core_types::{AnyHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
@ -6,9 +7,9 @@ use glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicTable};
use graphic_types::{Artboard, Vector};
use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
use vector_types::{GradientStops, ReferencePoint};
#[node_macro::node(category("General"), path(graphene_core::vector))]
#[node_macro::node(category("General"))]
async fn map<Item: AnyHash + Send + Sync + std::hash::Hash>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
@ -41,6 +42,70 @@ async fn map<Item: AnyHash + Send + Sync + std::hash::Hash>(
rows
}
#[node_macro::node(category("General"))]
async fn mirror<T: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
)]
content: Table<T>,
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
#[unit(" px")] offset: f64,
#[range((-90., 90.))] angle: Angle,
#[default(true)] keep_original: bool,
) -> Table<T>
where
Table<T>: BoundingBox,
{
// Normalize the direction vector
let normal = DVec2::from_angle(angle.to_radians());
// The mirror reference may be based on the bounding box if an explicit reference point is chosen
let RenderBoundingBox::Rectangle(bounding_box) = content.bounding_box(DAffine2::IDENTITY, false) else {
return content;
};
let reference_point_location = relative_to_bounds.point_in_bounding_box((bounding_box[0], bounding_box[1]).into());
let mirror_reference_point = reference_point_location.map(|point| point + normal * offset);
// Create the reflection matrix
let reflection = DAffine2::from_mat2_translation(
glam::DMat2::from_cols(
DVec2::new(1. - 2. * normal.x * normal.x, -2. * normal.y * normal.x),
DVec2::new(-2. * normal.x * normal.y, 1. - 2. * normal.y * normal.y),
),
DVec2::ZERO,
);
// Apply reflection around the reference point
let reflected_transform = if let Some(mirror_reference_point) = mirror_reference_point {
DAffine2::from_translation(mirror_reference_point) * reflection * DAffine2::from_translation(-mirror_reference_point)
} else {
reflection * DAffine2::from_translation(DVec2::from_angle(angle.to_radians()) * DVec2::splat(-offset))
};
let mut result_table = Table::new();
// Add original instance depending on the keep_original flag
if keep_original {
for instance in content.clone().into_iter() {
result_table.push(instance);
}
}
// Create and add mirrored instance
for mut row in content.into_iter() {
row.transform = reflected_transform * row.transform;
result_table.push(row);
}
result_table
}
/// Performs internal editor record-keeping that enables tools to target this network's layer.
/// This node associates the ID of the network's parent layer to every element of output data.
/// This technical detail may be ignored by users, and will be phased out in the future.

View File

@ -42,6 +42,7 @@ raster-nodes = { workspace = true }
brush-nodes = { workspace = true }
graphene-core = { workspace = true }
graphic-nodes = { workspace = true }
repeat-nodes = { workspace = true }
# Workspace dependencies
log = { workspace = true }

View File

@ -14,6 +14,7 @@ pub use graphic_types::{Artboard, Graphic, Vector};
pub use math_nodes;
pub use path_bool_nodes as path_bool;
pub use raster_nodes;
pub use repeat_nodes;
pub use text_nodes;
pub use transform_nodes;
pub use vector_nodes;
@ -64,6 +65,10 @@ pub mod transform {
pub use vector_types::ReferencePoint;
}
pub mod repeat {
pub use repeat_nodes::repeat_nodes::*;
}
pub mod math {
pub use core_types::math::quad;

View File

@ -0,0 +1,32 @@
[package]
name = "repeat-nodes"
version = "0.1.0"
edition = "2024"
description = "Repeat operation nodes for Graphene"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
[dependencies]
# Local dependencies
core-types = { workspace = true }
vector-types = { workspace = true }
raster-types = { workspace = true }
node-macro = { workspace = true }
graphic-types = { workspace = true }
# Workspace dependencies
dyn-any = { workspace = true }
glam = { workspace = true }
log = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }
[dev-dependencies]
graphene-core = { workspace = true }
vector-nodes = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] }
kurbo = { workspace = true }

View File

@ -0,0 +1,8 @@
pub mod repeat_nodes;
// Re-export for convenience
pub use core_types as gcore;
pub use graphic_types;
pub use raster_types;
pub use repeat_nodes::*;
pub use vector_types;

View File

@ -0,0 +1,291 @@
use crate::gcore::Context;
use core::f64::consts::TAU;
use core_types::registry::types::{Angle, IntegerCount, PixelSize};
use core_types::table::{Table, TableRowRef};
use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::{Graphic, Vector};
use raster_types::{CPU, Raster};
use vector_types::GradientStops;
#[node_macro::node(category("Repeat"))]
async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
#[default(1)] count: u64,
reverse: bool,
) -> Table<T> {
// Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`).
let count = count.max(1) as usize;
let mut result_table = Table::new();
for index in 0..count {
let index = if reverse { count - index - 1 } else { index };
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for generated_row in generated_instance.into_iter() {
result_table.push(generated_row);
}
}
result_table
}
#[node_macro::node(category("Repeat"))]
pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
#[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: PixelSize,
angle: Angle,
#[default(5)] count: IntegerCount,
) -> Table<T> {
let angle = angle.to_radians();
let count = count.max(1);
let total = (count - 1) as f64;
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 transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation);
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for row in generated_instance.iter() {
let mut row = row.into_cloned();
let local_translation = DAffine2::from_translation(row.transform.translation);
let local_matrix = DAffine2::from_mat2(row.transform.matrix2);
row.transform = local_translation * transform * local_matrix;
result_table.push(row);
}
}
result_table
}
#[node_macro::node(category("Repeat"))]
async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
start_angle: Angle,
#[unit(" px")]
#[default(5)]
radius: f64,
#[default(5)] count: IntegerCount,
) -> Table<T> {
let count = count.max(1);
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 translation = DAffine2::from_translation(radius * DVec2::Y);
let transform = angle * translation;
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for row in generated_instance.iter() {
let mut row = row.into_cloned();
let local_translation = DAffine2::from_translation(row.transform.translation);
let local_matrix = DAffine2::from_mat2(row.transform.matrix2);
row.transform = local_translation * transform * local_matrix;
result_table.push(row);
}
}
result_table
}
#[node_macro::node(category("Repeat"), name("Repeat on Points"))]
async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs,
points: Table<Vector>,
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
reverse: bool,
) -> Table<T> {
let mut result_table = Table::new();
for TableRowRef { element: points, transform, .. } in points.iter() {
let mut iteration = async |index, point| {
let transformed_point = transform.transform_point2(point);
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(transformed_point);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for mut generated_row in generated_instance.into_iter() {
generated_row.transform.translation = transformed_point;
result_table.push(generated_row);
}
};
let range = points.point_domain.positions().iter().enumerate();
if reverse {
for (index, &point) in range.rev() {
iteration(index, point).await;
}
} else {
for (index, &point) in range {
iteration(index, point).await;
}
}
}
result_table
}
#[cfg(test)]
mod test {
use super::*;
use core_types::Ctx;
use core_types::Node;
use core_types::transform::Footprint;
use glam::DVec2;
use graphene_core::ReadPositionNode;
use graphene_core::extract_xy::{ExtractXyNode, XY};
use graphic_types::Vector;
use kurbo::Shape;
use kurbo::{BezPath, DEFAULT_ACCURACY, Rect};
use std::future::Future;
use std::pin::Pin;
use vector_nodes::generator_nodes::RectangleNode;
use vector_types::subpath::Subpath;
fn vector_node_from_bezpath(bezpath: BezPath) -> Table<Vector> {
Table::new_from_element(Vector::from_bezpath(bezpath))
}
#[derive(Clone)]
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: I) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
}
}
#[tokio::test]
async fn repeat_on_points_test() {
let context = OwnedContextImpl::default().into_context();
let rect = RectangleNode::new(
FutureWrapperNode(()),
ExtractXyNode::new(ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(0)), FutureWrapperNode(XY::Y)),
FutureWrapperNode(2_f64),
FutureWrapperNode(false),
FutureWrapperNode(0_f64),
FutureWrapperNode(false),
);
let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)];
let points = Table::new_from_element(Vector::from_subpath(Subpath::from_anchors(positions, false)));
let generated = super::repeat_on_points(context, points, &rect, false).await;
assert_eq!(generated.len(), positions.len());
for (position, generated_row) in positions.into_iter().zip(generated.iter()) {
let bounds = generated_row.element.bounding_box_with_transform(*generated_row.transform).unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y);
}
}
#[tokio::test]
async fn repeat() {
let direction = DVec2::X * 1.5;
let count = 3;
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_array(
context,
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
direction,
0.,
count,
)
.await;
let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_table.iter().next().unwrap().element;
assert_eq!(vector.region_manipulator_groups().count(), 3);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn repeat_transform_position() {
let direction = DVec2::new(12., 10.);
let count = 8;
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_array(
context,
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
direction,
0.,
count,
)
.await;
let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_table.iter().next().unwrap().element;
assert_eq!(vector.region_manipulator_groups().count(), 8);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn repeat_radial() {
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_radial(context, &FutureWrapperNode(vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))), 45., 4., 8).await;
let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_table.iter().next().unwrap().element;
assert_eq!(vector.region_manipulator_groups().count(), 8);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
let center = (manipulator_groups[0].anchor + manipulator_groups[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
}
}
}

View File

@ -18,6 +18,7 @@ node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
rand = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true }

View File

@ -15,6 +15,7 @@ core-types = { workspace = true }
vector-types = { workspace = true }
graphic-types = { workspace = true }
node-macro = { workspace = true }
repeat-nodes = { workspace = true }
# Workspace dependencies
dyn-any = { workspace = true }

View File

@ -1,132 +0,0 @@
use core_types::Color;
use core_types::table::{Table, TableRowRef};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
use graphic_types::Graphic;
use graphic_types::Vector;
use graphic_types::raster_types::{CPU, Raster};
use vector_types::GradientStops;
#[node_macro::node(name("Instance on Points"), category("Instancing"), path(core_types::vector))]
async fn instance_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx,
points: Table<Vector>,
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
reverse: bool,
) -> Table<T> {
let mut result_table = Table::new();
for TableRowRef { element: points, transform, .. } in points.iter() {
let mut iteration = async |index, point| {
let transformed_point = transform.transform_point2(point);
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(transformed_point);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for mut generated_row in generated_instance.into_iter() {
generated_row.transform.translation = transformed_point;
result_table.push(generated_row);
}
};
let range = points.point_domain.positions().iter().enumerate();
if reverse {
for (index, &point) in range.rev() {
iteration(index, point).await;
}
} else {
for (index, &point) in range {
iteration(index, point).await;
}
}
}
result_table
}
#[node_macro::node(category("Instancing"), path(core_types::vector))]
async fn instance_repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
instance: impl Node<'n, Context<'static>, Output = Table<T>>,
#[default(1)] count: u64,
reverse: bool,
) -> Table<T> {
let count = count.max(1) as usize;
let mut result_table = Table::new();
for index in 0..count {
let index = if reverse { count - index - 1 } else { index };
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index);
let generated_instance = instance.eval(new_ctx.into_context()).await;
for generated_row in generated_instance.into_iter() {
result_table.push(generated_row);
}
}
result_table
}
#[cfg(test)]
mod test {
use super::*;
use crate::generator_nodes::RectangleNode;
use core_types::Ctx;
use core_types::Node;
use glam::DVec2;
use graphene_core::ReadPositionNode;
use graphene_core::extract_xy::{ExtractXyNode, XY};
use graphic_types::Vector;
use std::future::Future;
use std::pin::Pin;
use vector_types::subpath::Subpath;
#[derive(Clone)]
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: I) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
}
}
#[tokio::test]
async fn instance_on_points_test() {
let owned = OwnedContextImpl::default().into_context();
let rect = RectangleNode::new(
FutureWrapperNode(()),
ExtractXyNode::new(ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(0)), FutureWrapperNode(XY::Y)),
FutureWrapperNode(2_f64),
FutureWrapperNode(false),
FutureWrapperNode(0_f64),
FutureWrapperNode(false),
);
let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)];
let points = Table::new_from_element(Vector::from_subpath(Subpath::from_anchors(positions, false)));
let generated = super::instance_on_points(owned, points, &rect, false).await;
assert_eq!(generated.len(), positions.len());
for (position, generated_row) in positions.into_iter().zip(generated.iter()) {
let bounds = generated_row.element.bounding_box_with_transform(*generated_row.transform).unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y);
}
}
}

View File

@ -1,5 +1,4 @@
pub mod generator_nodes;
pub mod instance;
pub mod merge_qr_squares;
pub mod vector_modification_nodes;
mod vector_nodes;
@ -11,7 +10,6 @@ extern crate log;
pub use core_types as gcore;
pub use generator_nodes::*;
pub use graphic_types;
pub use instance::*;
pub use vector_modification_nodes::*;
pub use vector_nodes::*;
pub use vector_types;

View File

@ -1,8 +1,8 @@
use core::cmp::Ordering;
use core::f64::consts::PI;
use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher};
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::registry::types::{Angle, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, Progression, SeedValue};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::table::{Table, TableRow, TableRowMut};
use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
@ -13,22 +13,18 @@ use graphic_types::{Graphic, IntoGraphicTable};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, PathEl, PathSeg, Shape};
use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use std::f64::consts::TAU;
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
use vector_types::vector::PointDomain;
use vector_types::vector::ReferencePoint;
use vector_types::vector::algorithms::bezpath_algorithms::eval_pathseg_euclidean;
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use vector_types::vector::misc::{CentroidType, ExtrudeJoiningAlgorithm, RowsOrColumns, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2};
use vector_types::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
use vector_types::vector::misc::{handles_to_segment, segment_to_handles};
use vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
use vector_types::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, RegionId};
use vector_types::vector::{PointId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::vector::misc::{
CentroidType, ExtrudeJoiningAlgorithm, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, handles_to_segment, is_linear,
point_to_dvec2, segment_to_handles,
};
use vector_types::vector::style::{Fill, Gradient, GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
/// Implemented for types that can be converted to an iterator of vector rows.
/// Used for the fill and stroke node so they can be used on `Table<Graphic>` or `Table<Vector>`.
@ -226,75 +222,6 @@ where
content
}
#[node_macro::node(category("Instancing"), path(core_types::vector))]
async fn repeat<I: 'n + Send + Clone>(
_: impl Ctx,
// TODO: Implement other graphical types.
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] instance: Table<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: PixelSize,
angle: Angle,
#[default(5)] count: IntegerCount,
) -> Table<I> {
let angle = angle.to_radians();
let count = count.max(1);
let total = (count - 1) as f64;
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 transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation);
for row in instance.iter() {
let mut row = row.into_cloned();
let local_translation = DAffine2::from_translation(row.transform.translation);
let local_matrix = DAffine2::from_mat2(row.transform.matrix2);
row.transform = local_translation * transform * local_matrix;
result_table.push(row);
}
}
result_table
}
#[node_macro::node(category("Instancing"), path(core_types::vector))]
async fn circular_repeat<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] instance: Table<I>,
start_angle: Angle,
#[unit(" px")]
#[default(5)]
radius: f64,
#[default(5)] count: IntegerCount,
) -> Table<I> {
let count = count.max(1);
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 translation = DAffine2::from_translation(radius * DVec2::Y);
let transform = angle * translation;
for row in instance.iter() {
let mut row = row.into_cloned();
let local_translation = DAffine2::from_translation(row.transform.translation);
let local_matrix = DAffine2::from_mat2(row.transform.matrix2);
row.transform = local_translation * transform * local_matrix;
result_table.push(row);
}
}
result_table
}
#[node_macro::node(name("Copy to Points"), category("Instancing"), path(core_types::vector))]
async fn copy_to_points<I: 'n + Send + Clone>(
_: impl Ctx,
@ -373,63 +300,6 @@ async fn copy_to_points<I: 'n + Send + Clone>(
result_table
}
#[node_macro::node(category("Instancing"), path(core_types::vector))]
async fn mirror<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] content: Table<I>,
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
#[unit(" px")] offset: f64,
#[range((-90., 90.))] angle: Angle,
#[default(true)] keep_original: bool,
) -> Table<I>
where
Table<I>: BoundingBox,
{
// Normalize the direction vector
let normal = DVec2::from_angle(angle.to_radians());
// The mirror reference may be based on the bounding box if an explicit reference point is chosen
let RenderBoundingBox::Rectangle(bounding_box) = content.bounding_box(DAffine2::IDENTITY, false) else {
return content;
};
let reference_point_location = relative_to_bounds.point_in_bounding_box((bounding_box[0], bounding_box[1]).into());
let mirror_reference_point = reference_point_location.map(|point| point + normal * offset);
// Create the reflection matrix
let reflection = DAffine2::from_mat2_translation(
glam::DMat2::from_cols(
DVec2::new(1. - 2. * normal.x * normal.x, -2. * normal.y * normal.x),
DVec2::new(-2. * normal.x * normal.y, 1. - 2. * normal.y * normal.y),
),
DVec2::ZERO,
);
// Apply reflection around the reference point
let reflected_transform = if let Some(mirror_reference_point) = mirror_reference_point {
DAffine2::from_translation(mirror_reference_point) * reflection * DAffine2::from_translation(-mirror_reference_point)
} else {
reflection * DAffine2::from_translation(DVec2::from_angle(angle.to_radians()) * DVec2::splat(-offset))
};
let mut result_table = Table::new();
// Add original instance depending on the keep_original flag
if keep_original {
for instance in content.clone().into_iter() {
result_table.push(instance);
}
}
// Create and add mirrored instance
for mut row in content.into_iter() {
row.transform = reflected_transform * row.transform;
result_table.push(row);
}
result_table
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn round_corners(
_: impl Ctx,
@ -1343,7 +1213,7 @@ async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: Table<Ve
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn flatten_path<T: 'n + Send>(
pub async fn flatten_path<T: 'n + Send>(
_: impl Ctx,
#[implementations(
Table<Graphic>,
@ -2536,60 +2406,6 @@ mod test {
}
}
#[tokio::test]
async fn repeat() {
let direction = DVec2::X * 1.5;
let count = 3;
let repeated = super::repeat(
Footprint::default(),
vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY)),
direction,
0.,
count,
)
.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(), 3);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn repeat_transform_position() {
let direction = DVec2::new(12., 10.);
let count = 8;
let repeated = super::repeat(
Footprint::default(),
vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY)),
direction,
0.,
count,
)
.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);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
}
}
#[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 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);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
let center = (manipulator_groups[0].anchor + manipulator_groups[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
}
}
#[tokio::test]
async fn bounding_box() {
let bounding_box = super::bounding_box((), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))).await;
@ -2716,8 +2532,11 @@ 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 mut rectangles = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
let mut second_rectangle = rectangles.get(0).unwrap().into_cloned();
second_rectangle.transform *= DAffine2::from_translation((-100., -100.).into());
rectangles.push(second_rectangle);
let morphed = super::morph(Footprint::default(), rectangles, 0.5).await;
let element = morphed.iter().next().unwrap().element;
assert_eq!(