mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -2295,6 +2295,7 @@ dependencies = [
|
|||||||
"path-bool-nodes",
|
"path-bool-nodes",
|
||||||
"raster-nodes",
|
"raster-nodes",
|
||||||
"rendering",
|
"rendering",
|
||||||
|
"repeat-nodes",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"text-nodes",
|
"text-nodes",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -5078,6 +5079,25 @@ dependencies = [
|
|||||||
"vello",
|
"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]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.23"
|
version = "0.12.23"
|
||||||
@@ -6431,6 +6451,7 @@ dependencies = [
|
|||||||
"glam",
|
"glam",
|
||||||
"graphic-types",
|
"graphic-types",
|
||||||
"node-macro",
|
"node-macro",
|
||||||
|
"rand 0.9.2",
|
||||||
"serde",
|
"serde",
|
||||||
"vector-types",
|
"vector-types",
|
||||||
]
|
]
|
||||||
@@ -6734,6 +6755,7 @@ dependencies = [
|
|||||||
"node-macro",
|
"node-macro",
|
||||||
"qrcodegen",
|
"qrcodegen",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
|
"repeat-nodes",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ members = [
|
|||||||
"node-graph/nodes/raster/shaders/entrypoint",
|
"node-graph/nodes/raster/shaders/entrypoint",
|
||||||
"node-graph/nodes/text",
|
"node-graph/nodes/text",
|
||||||
"node-graph/nodes/transform",
|
"node-graph/nodes/transform",
|
||||||
|
"node-graph/nodes/repeat",
|
||||||
"node-graph/nodes/vector",
|
"node-graph/nodes/vector",
|
||||||
"node-graph/graph-craft",
|
"node-graph/graph-craft",
|
||||||
"node-graph/graphene-cli",
|
"node-graph/graphene-cli",
|
||||||
@@ -116,6 +117,7 @@ graphic-nodes = { path = "node-graph/nodes/graphic" }
|
|||||||
text-nodes = { path = "node-graph/nodes/text" }
|
text-nodes = { path = "node-graph/nodes/text" }
|
||||||
transform-nodes = { path = "node-graph/nodes/transform" }
|
transform-nodes = { path = "node-graph/nodes/transform" }
|
||||||
vector-nodes = { path = "node-graph/nodes/vector" }
|
vector-nodes = { path = "node-graph/nodes/vector" }
|
||||||
|
repeat-nodes = { path = "node-graph/nodes/repeat" }
|
||||||
math-nodes = { path = "node-graph/nodes/math" }
|
math-nodes = { path = "node-graph/nodes/math" }
|
||||||
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
|
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
|
||||||
graph-craft = { path = "node-graph/graph-craft" }
|
graph-craft = { path = "node-graph/graph-craft" }
|
||||||
|
|||||||
2
demo-artwork/changing-seasons.graphite
generated
2
demo-artwork/changing-seasons.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/isometric-fountain.graphite
generated
2
demo-artwork/isometric-fountain.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/marbled-mandelbrot.graphite
generated
2
demo-artwork/marbled-mandelbrot.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/painted-dreams.graphite
generated
2
demo-artwork/painted-dreams.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/valley-of-spires.graphite
generated
2
demo-artwork/valley-of-spires.graphite
generated
File diff suppressed because one or more lines are too long
@@ -502,10 +502,10 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
// [11:Equals]0 -> 0[17:Switch]
|
// [11:Equals]0 -> 0[17:Switch]
|
||||||
// [9:Transform]0 -> 1[17:Switch]
|
// [9:Transform]0 -> 1[17:Switch]
|
||||||
// [16:Morph]0 -> 2[17:Switch]
|
// [16:Morph]0 -> 2[17:Switch]
|
||||||
// [17:Switch]0 -> 0[18:Instance Repeat]
|
// [17:Switch]0 -> 0[18:Repeat]
|
||||||
// [0:Floor]0 -> 1[18:Instance Repeat]
|
// [0:Floor]0 -> 1[18:Repeat]
|
||||||
// [IMPORTS]3 -> 2[18:Instance Repeat]
|
// [IMPORTS]3 -> 2[18:Repeat]
|
||||||
// [18:Instance Repeat]0 -> 0[EXPORTS]
|
// [18:Repeat]0 -> 0[EXPORTS]
|
||||||
node_template: NodeTemplate {
|
node_template: NodeTemplate {
|
||||||
document_node: DocumentNode {
|
document_node: DocumentNode {
|
||||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
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)],
|
inputs: vec![NodeInput::node(NodeId(11), 0), NodeInput::node(NodeId(9), 0), NodeInput::node(NodeId(16), 0)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
// 18: Instance Repeat
|
// 18: Repeat
|
||||||
DocumentNode {
|
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)],
|
inputs: vec![NodeInput::node(NodeId(17), 0), NodeInput::node(NodeId(0), 0), NodeInput::import(generic!(T), 3)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -807,7 +807,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
// 18: Instance Repeat
|
// 18: Repeat
|
||||||
DocumentNodeMetadata {
|
DocumentNodeMetadata {
|
||||||
persistent_metadata: DocumentNodePersistentMetadata {
|
persistent_metadata: DocumentNodePersistentMetadata {
|
||||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(49, -1)),
|
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(49, -1)),
|
||||||
|
|||||||
@@ -753,18 +753,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::vector::centroid::IDENTIFIER,
|
node: graphene_std::vector::centroid::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::CentroidNode"],
|
aliases: &["graphene_core::vector::CentroidNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::vector::circular_repeat::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::vector::CircularRepeatNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::close_path::IDENTIFIER,
|
node: graphene_std::vector::close_path::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::ClosePathNode"],
|
aliases: &["graphene_core::vector::ClosePathNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::vector::copy_to_points::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::vector::CopyToPointsNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::count_elements::IDENTIFIER,
|
node: graphene_std::vector::count_elements::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::CountElementsNode"],
|
aliases: &["graphene_core::vector::CountElementsNode"],
|
||||||
@@ -841,22 +833,30 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::graphic::map::IDENTIFIER,
|
node: graphene_std::graphic::map::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::InstanceMapNode"],
|
aliases: &["graphene_core::vector::InstanceMapNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::vector::instance_on_points::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::vector::InstanceOnPointsNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::context::read_position::IDENTIFIER,
|
node: graphene_std::context::read_position::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::InstancePositionNode", "core_types::vector::InstancePositionNode"],
|
aliases: &["graphene_core::vector::InstancePositionNode", "core_types::vector::InstancePositionNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::vector::instance_repeat::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::vector::InstanceRepeatNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::context::read_vector::IDENTIFIER,
|
node: graphene_std::context::read_vector::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::InstanceVectorNode"],
|
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 {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::jitter_points::IDENTIFIER,
|
node: graphene_std::vector::jitter_points::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::JitterPointsNode"],
|
aliases: &["graphene_core::vector::JitterPointsNode"],
|
||||||
@@ -866,8 +866,8 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
aliases: &["graphene_core::vector::MergeByDistanceNode"],
|
aliases: &["graphene_core::vector::MergeByDistanceNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::mirror::IDENTIFIER,
|
node: graphene_std::graphic::mirror::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::MirrorNode"],
|
aliases: &["graphene_core::vector::MirrorNode", "core_types::vector::MirrorNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::morph::IDENTIFIER,
|
node: graphene_std::vector::morph::IDENTIFIER,
|
||||||
@@ -905,10 +905,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
|||||||
node: graphene_std::vector::position_on_path::IDENTIFIER,
|
node: graphene_std::vector::position_on_path::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::PositionOnPathNode"],
|
aliases: &["graphene_core::vector::PositionOnPathNode"],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
|
||||||
node: graphene_std::vector::repeat::IDENTIFIER,
|
|
||||||
aliases: &["graphene_core::vector::RepeatNode"],
|
|
||||||
},
|
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::vector::round_corners::IDENTIFIER,
|
node: graphene_std::vector::round_corners::IDENTIFIER,
|
||||||
aliases: &["graphene_core::vector::RoundCornersNode"],
|
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
|
// 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();
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
document.network_interface.replace_implementation(node_id, network_path, &mut 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`
|
// 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();
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
document.network_interface.replace_implementation(node_id, network_path, &mut 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();
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||||
|
|
||||||
|
|||||||
@@ -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::table::{Table, TableRow};
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
use core_types::{AnyHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
|
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::graphic::{Graphic, IntoGraphicTable};
|
||||||
use graphic_types::{Artboard, Vector};
|
use graphic_types::{Artboard, Vector};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
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>(
|
async fn map<Item: AnyHash + Send + Sync + std::hash::Hash>(
|
||||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
@@ -41,6 +42,70 @@ async fn map<Item: AnyHash + Send + Sync + std::hash::Hash>(
|
|||||||
rows
|
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.
|
/// 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 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.
|
/// This technical detail may be ignored by users, and will be phased out in the future.
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ raster-nodes = { workspace = true }
|
|||||||
brush-nodes = { workspace = true }
|
brush-nodes = { workspace = true }
|
||||||
graphene-core = { workspace = true }
|
graphene-core = { workspace = true }
|
||||||
graphic-nodes = { workspace = true }
|
graphic-nodes = { workspace = true }
|
||||||
|
repeat-nodes = { workspace = true }
|
||||||
|
|
||||||
# Workspace dependencies
|
# Workspace dependencies
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub use graphic_types::{Artboard, Graphic, Vector};
|
|||||||
pub use math_nodes;
|
pub use math_nodes;
|
||||||
pub use path_bool_nodes as path_bool;
|
pub use path_bool_nodes as path_bool;
|
||||||
pub use raster_nodes;
|
pub use raster_nodes;
|
||||||
|
pub use repeat_nodes;
|
||||||
pub use text_nodes;
|
pub use text_nodes;
|
||||||
pub use transform_nodes;
|
pub use transform_nodes;
|
||||||
pub use vector_nodes;
|
pub use vector_nodes;
|
||||||
@@ -64,6 +65,10 @@ pub mod transform {
|
|||||||
pub use vector_types::ReferencePoint;
|
pub use vector_types::ReferencePoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod repeat {
|
||||||
|
pub use repeat_nodes::repeat_nodes::*;
|
||||||
|
}
|
||||||
|
|
||||||
pub mod math {
|
pub mod math {
|
||||||
pub use core_types::math::quad;
|
pub use core_types::math::quad;
|
||||||
|
|
||||||
|
|||||||
32
node-graph/nodes/repeat/Cargo.toml
Normal file
32
node-graph/nodes/repeat/Cargo.toml
Normal 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 }
|
||||||
8
node-graph/nodes/repeat/src/lib.rs
Normal file
8
node-graph/nodes/repeat/src/lib.rs
Normal 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;
|
||||||
291
node-graph/nodes/repeat/src/repeat_nodes.rs
Normal file
291
node-graph/nodes/repeat/src/repeat_nodes.rs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ node-macro = { workspace = true }
|
|||||||
|
|
||||||
# Workspace dependencies
|
# Workspace dependencies
|
||||||
glam = { workspace = true }
|
glam = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
|
||||||
# Optional workspace dependencies
|
# Optional workspace dependencies
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ core-types = { workspace = true }
|
|||||||
vector-types = { workspace = true }
|
vector-types = { workspace = true }
|
||||||
graphic-types = { workspace = true }
|
graphic-types = { workspace = true }
|
||||||
node-macro = { workspace = true }
|
node-macro = { workspace = true }
|
||||||
|
repeat-nodes = { workspace = true }
|
||||||
|
|
||||||
# Workspace dependencies
|
# Workspace dependencies
|
||||||
dyn-any = { workspace = true }
|
dyn-any = { workspace = true }
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod generator_nodes;
|
pub mod generator_nodes;
|
||||||
pub mod instance;
|
|
||||||
pub mod merge_qr_squares;
|
pub mod merge_qr_squares;
|
||||||
pub mod vector_modification_nodes;
|
pub mod vector_modification_nodes;
|
||||||
mod vector_nodes;
|
mod vector_nodes;
|
||||||
@@ -11,7 +10,6 @@ extern crate log;
|
|||||||
pub use core_types as gcore;
|
pub use core_types as gcore;
|
||||||
pub use generator_nodes::*;
|
pub use generator_nodes::*;
|
||||||
pub use graphic_types;
|
pub use graphic_types;
|
||||||
pub use instance::*;
|
|
||||||
pub use vector_modification_nodes::*;
|
pub use vector_modification_nodes::*;
|
||||||
pub use vector_nodes::*;
|
pub use vector_nodes::*;
|
||||||
pub use vector_types;
|
pub use vector_types;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use core::cmp::Ordering;
|
use core::cmp::Ordering;
|
||||||
use core::f64::consts::PI;
|
use core::f64::consts::{PI, TAU};
|
||||||
use core::hash::{Hash, Hasher};
|
use core::hash::{Hash, Hasher};
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
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::table::{Table, TableRow, TableRowMut};
|
||||||
use core_types::transform::{Footprint, Transform};
|
use core_types::transform::{Footprint, Transform};
|
||||||
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
|
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 kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, PathEl, PathSeg, Shape};
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::f64::consts::TAU;
|
|
||||||
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
||||||
use vector_types::vector::PointDomain;
|
use vector_types::vector::PointDomain;
|
||||||
use vector_types::vector::ReferencePoint;
|
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::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::merge_by_distance::MergeByDistanceExt;
|
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
||||||
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
|
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::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::{
|
||||||
use vector_types::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear};
|
CentroidType, ExtrudeJoiningAlgorithm, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, handles_to_segment, is_linear,
|
||||||
use vector_types::vector::misc::{handles_to_segment, segment_to_handles};
|
point_to_dvec2, 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::style::{Fill, Gradient, GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use vector_types::vector::{FillId, RegionId};
|
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
||||||
use vector_types::vector::{PointId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
|
||||||
|
|
||||||
/// Implemented for types that can be converted to an iterator of vector rows.
|
/// 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>`.
|
/// Used for the fill and stroke node so they can be used on `Table<Graphic>` or `Table<Vector>`.
|
||||||
@@ -226,75 +222,6 @@ where
|
|||||||
content
|
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))]
|
#[node_macro::node(name("Copy to Points"), category("Instancing"), path(core_types::vector))]
|
||||||
async fn copy_to_points<I: 'n + Send + Clone>(
|
async fn copy_to_points<I: 'n + Send + Clone>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
@@ -373,63 +300,6 @@ async fn copy_to_points<I: 'n + Send + Clone>(
|
|||||||
result_table
|
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))]
|
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||||
async fn round_corners(
|
async fn round_corners(
|
||||||
_: impl Ctx,
|
_: 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))]
|
#[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,
|
_: impl Ctx,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
Table<Graphic>,
|
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]
|
#[tokio::test]
|
||||||
async fn bounding_box() {
|
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;
|
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]
|
#[tokio::test]
|
||||||
async fn morph() {
|
async fn morph() {
|
||||||
let rectangle = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
|
let mut rectangles = 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 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 morphed = super::morph(Footprint::default(), rectangles, 0.5).await;
|
||||||
let element = morphed.iter().next().unwrap().element;
|
let element = morphed.iter().next().unwrap().element;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Reference in New Issue
Block a user