mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Restructure node crates (#3384)
* Restructure node-graph folder * Fix wasm compilation * Move node definitions out of *-types crates * Cleanup * Fix warnings * Fix warnings * Start adding migrations * Add migrations and move memo nodes to gcore * Move nodes/gsvg-render -> rendering * Replace some hard coded identifiers and fix automatic conversion * Fix Vec2Value node migration * Fix formatting * Add more migrations * Cleanup features * Fix core_types::raster import * Update demo artwork (to make profile ci work) * Move *-types to node-graph/libraries folder * Add missing node migrations * Migrate more nodes * Remove impure memo node * More fixes and remove warning * Migrate context and add a few missing migrations --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
33
node-graph/nodes/vector/Cargo.toml
Normal file
33
node-graph/nodes/vector/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "vector-nodes"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Vector operation nodes for Graphene"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
core-types = { workspace = true }
|
||||
vector-types = { workspace = true }
|
||||
graphic-types = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
dyn-any = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
graphene-core = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
futures = { workspace = true }
|
||||
352
node-graph/nodes/vector/src/generator_nodes.rs
Normal file
352
node-graph/nodes/vector/src/generator_nodes.rs
Normal file
@@ -0,0 +1,352 @@
|
||||
use core_types::Ctx;
|
||||
use core_types::registry::types::{Angle, PixelSize};
|
||||
use core_types::table::Table;
|
||||
use glam::DVec2;
|
||||
use graphic_types::Vector;
|
||||
use vector_types::subpath;
|
||||
use vector_types::vector::misc::{ArcType, AsU64, GridType};
|
||||
use vector_types::vector::misc::{HandleId, SpiralType};
|
||||
use vector_types::vector::{PointId, SegmentId, StrokeId};
|
||||
|
||||
trait CornerRadius {
|
||||
fn generate(self, size: DVec2, clamped: bool) -> Table<Vector>;
|
||||
}
|
||||
impl CornerRadius for f64 {
|
||||
fn generate(self, size: DVec2, clamped: bool) -> Table<Vector> {
|
||||
let clamped_radius = if clamped { self.clamp(0., size.x.min(size.y).max(0.) / 2.) } else { self };
|
||||
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4])))
|
||||
}
|
||||
}
|
||||
impl CornerRadius for [f64; 4] {
|
||||
fn generate(self, size: DVec2, clamped: bool) -> Table<Vector> {
|
||||
let clamped_radius = if clamped {
|
||||
// Algorithm follows the CSS spec: <https://drafts.csswg.org/css-backgrounds/#corner-overlap>
|
||||
|
||||
let mut scale_factor: f64 = 1.;
|
||||
for i in 0..4 {
|
||||
let side_length = if i % 2 == 0 { size.x } else { size.y };
|
||||
let adjacent_corner_radius_sum = self[i] + self[(i + 1) % 4];
|
||||
if side_length < adjacent_corner_radius_sum {
|
||||
scale_factor = scale_factor.min(side_length / adjacent_corner_radius_sum);
|
||||
}
|
||||
}
|
||||
self.map(|x| x * scale_factor)
|
||||
} else {
|
||||
self
|
||||
};
|
||||
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a circle shape with a chosen radius.
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn circle(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[unit(" px")]
|
||||
#[default(50.)]
|
||||
radius: f64,
|
||||
) -> Table<Vector> {
|
||||
let radius = radius.abs();
|
||||
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
|
||||
}
|
||||
|
||||
/// Generates an arc shape forming a portion of a circle which may be open, closed, or a pie slice.
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn arc(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[unit(" px")]
|
||||
#[default(50.)]
|
||||
radius: f64,
|
||||
start_angle: Angle,
|
||||
#[default(270.)]
|
||||
#[range((0., 360.))]
|
||||
sweep_angle: Angle,
|
||||
arc_type: ArcType,
|
||||
) -> Table<Vector> {
|
||||
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_arc(
|
||||
radius,
|
||||
start_angle / 360. * std::f64::consts::TAU,
|
||||
sweep_angle / 360. * std::f64::consts::TAU,
|
||||
match arc_type {
|
||||
ArcType::Open => subpath::ArcType::Open,
|
||||
ArcType::Closed => subpath::ArcType::Closed,
|
||||
ArcType::PieSlice => subpath::ArcType::PieSlice,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
/// Generates a spiral shape that winds from an inner to an outer radius.
|
||||
#[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))]
|
||||
fn spiral(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
spiral_type: SpiralType,
|
||||
#[default(5.)] turns: f64,
|
||||
#[default(0.)] start_angle: f64,
|
||||
#[default(0.)] inner_radius: f64,
|
||||
#[default(25)] outer_radius: f64,
|
||||
#[default(90.)] angular_resolution: f64,
|
||||
) -> Table<Vector> {
|
||||
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_spiral(
|
||||
inner_radius,
|
||||
outer_radius,
|
||||
turns,
|
||||
start_angle.to_radians(),
|
||||
angular_resolution.to_radians(),
|
||||
spiral_type,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Generates an ellipse shape (an oval or stretched circle) with the chosen radii.
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn ellipse(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[unit(" px")]
|
||||
#[default(50)]
|
||||
radius_x: f64,
|
||||
#[unit(" px")]
|
||||
#[default(25)]
|
||||
radius_y: f64,
|
||||
) -> Table<Vector> {
|
||||
let radius = DVec2::new(radius_x, radius_y);
|
||||
let corner1 = -radius;
|
||||
let corner2 = radius;
|
||||
|
||||
let mut ellipse = Vector::from_subpath(subpath::Subpath::new_ellipse(corner1, corner2));
|
||||
|
||||
let len = ellipse.segment_domain.ids().len();
|
||||
for i in 0..len {
|
||||
ellipse
|
||||
.colinear_manipulators
|
||||
.push([HandleId::end(ellipse.segment_domain.ids()[i]), HandleId::primary(ellipse.segment_domain.ids()[(i + 1) % len])]);
|
||||
}
|
||||
|
||||
Table::new_from_element(ellipse)
|
||||
}
|
||||
|
||||
/// Generates a rectangle shape with the chosen width and height. It may also have rounded corners if desired.
|
||||
#[node_macro::node(category("Vector: Shape"), properties("rectangle_properties"))]
|
||||
fn rectangle<T: CornerRadius>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[unit(" px")]
|
||||
#[default(100)]
|
||||
width: f64,
|
||||
#[unit(" px")]
|
||||
#[default(100)]
|
||||
height: f64,
|
||||
_individual_corner_radii: bool, // TODO: Move this to the bottom once we have a migration capability
|
||||
#[implementations(f64, [f64; 4])] corner_radius: T,
|
||||
#[default(true)] clamped: bool,
|
||||
) -> Table<Vector> {
|
||||
corner_radius.generate(DVec2::new(width, height), clamped)
|
||||
}
|
||||
|
||||
/// Generates an regular polygon shape like a triangle, square, pentagon, hexagon, heptagon, octagon, or any higher n-gon.
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn regular_polygon<T: AsU64>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(6)]
|
||||
#[hard_min(3.)]
|
||||
#[implementations(u32, u64, f64)]
|
||||
sides: T,
|
||||
#[unit(" px")]
|
||||
#[default(50)]
|
||||
radius: f64,
|
||||
) -> Table<Vector> {
|
||||
let points = sides.as_u64();
|
||||
let radius: f64 = radius * 2.;
|
||||
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius)))
|
||||
}
|
||||
|
||||
/// Generates an n-pointed star shape with inner and outer points at chosen radii from the center.
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn star<T: AsU64>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(5)]
|
||||
#[hard_min(2.)]
|
||||
#[implementations(u32, u64, f64)]
|
||||
sides: T,
|
||||
#[unit(" px")]
|
||||
#[default(50)]
|
||||
radius_1: f64,
|
||||
#[unit(" px")]
|
||||
#[default(25)]
|
||||
radius_2: f64,
|
||||
) -> Table<Vector> {
|
||||
let points = sides.as_u64();
|
||||
let diameter: f64 = radius_1 * 2.;
|
||||
let inner_diameter = radius_2 * 2.;
|
||||
|
||||
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
|
||||
}
|
||||
|
||||
/// Generates a line with endpoints at the two chosen coordinates.
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn line(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
/// Coordinate of the line's initial endpoint.
|
||||
#[default(0., 0.)]
|
||||
start: PixelSize,
|
||||
/// Coordinate of the line's terminal endpoint.
|
||||
#[default(100., 100.)]
|
||||
end: PixelSize,
|
||||
) -> Table<Vector> {
|
||||
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_line(start, end)))
|
||||
}
|
||||
|
||||
trait GridSpacing {
|
||||
fn as_dvec2(&self) -> DVec2;
|
||||
}
|
||||
impl GridSpacing for f64 {
|
||||
fn as_dvec2(&self) -> DVec2 {
|
||||
DVec2::splat(*self)
|
||||
}
|
||||
}
|
||||
impl GridSpacing for DVec2 {
|
||||
fn as_dvec2(&self) -> DVec2 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a rectangular or isometric grid with the chosen number of columns and rows. Line segments connect the points, forming a vector mesh.
|
||||
#[node_macro::node(category("Vector: Shape"), properties("grid_properties"))]
|
||||
fn grid<T: GridSpacing>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
grid_type: GridType,
|
||||
#[unit(" px")]
|
||||
#[hard_min(0.)]
|
||||
#[default(10)]
|
||||
#[implementations(f64, DVec2)]
|
||||
spacing: T,
|
||||
#[default(10)] columns: u32,
|
||||
#[default(10)] rows: u32,
|
||||
#[default(30., 30.)] angles: DVec2,
|
||||
) -> Table<Vector> {
|
||||
let (x_spacing, y_spacing) = spacing.as_dvec2().into();
|
||||
let (angle_a, angle_b) = angles.into();
|
||||
|
||||
let mut vector = Vector::default();
|
||||
let mut segment_id = SegmentId::ZERO;
|
||||
let mut point_id = PointId::ZERO;
|
||||
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
// Create rectangular grid points and connect them with line segments
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add current point to the grid
|
||||
let current_index = vector.point_domain.ids().len();
|
||||
vector.point_domain.push(point_id.next_id(), DVec2::new(x_spacing * x as f64, y_spacing * y as f64));
|
||||
|
||||
// Helper function to connect points with line segments
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the point to the left (horizontal connection)
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
// Connect to the point above (vertical connection)
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
}
|
||||
}
|
||||
}
|
||||
GridType::Isometric => {
|
||||
// Calculate isometric grid spacing based on angles
|
||||
let tan_a = angle_a.to_radians().tan();
|
||||
let tan_b = angle_b.to_radians().tan();
|
||||
let spacing = DVec2::new(y_spacing / (tan_a + tan_b), y_spacing);
|
||||
|
||||
// Create isometric grid points and connect them with line segments
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add current point to the grid with offset for odd columns
|
||||
let current_index = vector.point_domain.ids().len();
|
||||
|
||||
let a_angles_eaten = x.div_ceil(2) as f64;
|
||||
let b_angles_eaten = (x / 2) as f64;
|
||||
|
||||
let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a;
|
||||
|
||||
let position = DVec2::new(spacing.x * x as f64, spacing.y * y as f64 + offset_y_fraction * spacing.x);
|
||||
vector.point_domain.push(point_id.next_id(), position);
|
||||
|
||||
// Helper function to connect points with line segments
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the point to the left
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
// Connect to the point directly above
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
|
||||
// Additional diagonal connections for odd columns (creates hexagonal pattern)
|
||||
if x % 2 == 1 {
|
||||
// Connect to the point diagonally up-right (if not at right edge)
|
||||
push_segment(current_index.checked_sub(columns as usize - 1).filter(|_| x + 1 < columns));
|
||||
|
||||
// Connect to the point diagonally up-left
|
||||
push_segment(current_index.checked_sub(columns as usize + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Table::new_from_element(vector)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn isometric_grid_test() {
|
||||
// Doesn't crash with weird angles
|
||||
grid((), (), GridType::Isometric, 0., 5, 5, (0., 0.).into());
|
||||
grid((), (), GridType::Isometric, 90., 5, 5, (90., 90.).into());
|
||||
|
||||
// Works properly
|
||||
let grid = grid((), (), GridType::Isometric, 10., 5, 5, (30., 30.).into());
|
||||
assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() {
|
||||
assert_eq!(bezier.handles, subpath::BezierHandles::Linear);
|
||||
assert!(
|
||||
((bezier.start - bezier.end).length() - 10.).abs() < 1e-5,
|
||||
"Length of {} should be 10",
|
||||
(bezier.start - bezier.end).length()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skew_isometric_grid_test() {
|
||||
let grid = grid((), (), GridType::Isometric, 10., 5, 5, (40., 30.).into());
|
||||
assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() {
|
||||
assert_eq!(bezier.handles, subpath::BezierHandles::Linear);
|
||||
let vector = bezier.start - bezier.end;
|
||||
let angle = (vector.angle_to(DVec2::X).to_degrees() + 180.) % 180.;
|
||||
assert!([90., 150., 40.].into_iter().any(|target| (target - angle).abs() < 1e-10), "unexpected angle of {angle}")
|
||||
}
|
||||
}
|
||||
}
|
||||
170
node-graph/nodes/vector/src/instance.rs
Normal file
170
node-graph/nodes/vector/src/instance.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use core_types::Color;
|
||||
use core_types::table::{Table, TableRowRef};
|
||||
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, InjectVarArgs, OwnedContextImpl};
|
||||
use glam::DVec2;
|
||||
use graphic_types::Graphic;
|
||||
use graphic_types::Vector;
|
||||
use graphic_types::raster_types::{CPU, Raster};
|
||||
use vector_types::GradientStops;
|
||||
|
||||
use log::*;
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(dyn_any::DynAny)]
|
||||
struct HashableDVec2(DVec2);
|
||||
|
||||
impl std::hash::Hash for HashableDVec2 {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.x.to_bits().hash(state);
|
||||
self.0.y.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[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 + 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_vararg(Box::new(HashableDVec2(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
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Instancing"), path(core_types::vector))]
|
||||
async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
|
||||
match ctx.vararg(0).map(|dynamic| dynamic.downcast_ref::<HashableDVec2>()) {
|
||||
Ok(Some(position)) => return position.0,
|
||||
Ok(_) => warn!("Extracted value of incorrect type"),
|
||||
Err(e) => warn!("Cannot extract position vararg: {e:?}"),
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
|
||||
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
|
||||
// TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.)
|
||||
#[node_macro::node(category("Instancing"), path(core_types::vector))]
|
||||
async fn instance_index(ctx: impl Ctx + ExtractIndex, _primary: (), loop_level: u32) -> f64 {
|
||||
let Some(index_iter) = ctx.try_index() else { return 0. };
|
||||
let mut last = 0;
|
||||
for (i, index) in index_iter.enumerate() {
|
||||
if i == loop_level as usize {
|
||||
return index as f64;
|
||||
}
|
||||
last = index;
|
||||
}
|
||||
last as f64
|
||||
}
|
||||
|
||||
#[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::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(InstancePositionNode {}, 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_linear(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
node-graph/nodes/vector/src/lib.rs
Normal file
16
node-graph/nodes/vector/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod generator_nodes;
|
||||
pub mod instance;
|
||||
pub mod vector_modification_nodes;
|
||||
mod vector_nodes;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
// Re-export for convenience
|
||||
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;
|
||||
44
node-graph/nodes/vector/src/vector_modification_nodes.rs
Normal file
44
node-graph/nodes/vector/src/vector_modification_nodes.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use core_types::Ctx;
|
||||
use core_types::table::Table;
|
||||
use core_types::uuid::NodeId;
|
||||
use glam::DAffine2;
|
||||
use graphic_types::Vector;
|
||||
use vector_types::vector::VectorModification;
|
||||
|
||||
/// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments.
|
||||
#[node_macro::node(category(""))]
|
||||
async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Box<VectorModification>, node_path: Vec<NodeId>) -> Table<Vector> {
|
||||
use core_types::table::TableRow;
|
||||
|
||||
if vector.is_empty() {
|
||||
vector.push(TableRow::default());
|
||||
}
|
||||
let row = vector.get_mut(0).expect("push should give one item");
|
||||
modification.apply(row.element);
|
||||
|
||||
// Update the source node id
|
||||
let this_node_path = node_path.iter().rev().nth(1).copied();
|
||||
*row.source_node_id = row.source_node_id.or(this_node_path);
|
||||
|
||||
if vector.len() > 1 {
|
||||
warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len());
|
||||
}
|
||||
vector
|
||||
}
|
||||
|
||||
/// Applies the vector path's local transformation to its geometry and resets the transform to the identity.
|
||||
#[node_macro::node(category("Vector"))]
|
||||
async fn apply_transform(_ctx: impl Ctx, mut vector: Table<Vector>) -> Table<Vector> {
|
||||
for row in vector.iter_mut() {
|
||||
let vector = row.element;
|
||||
let transform = *row.transform;
|
||||
|
||||
for (_, point) in vector.point_domain.positions_mut() {
|
||||
*point = transform.transform_point2(*point);
|
||||
}
|
||||
|
||||
*row.transform = DAffine2::IDENTITY;
|
||||
}
|
||||
|
||||
vector
|
||||
}
|
||||
2445
node-graph/nodes/vector/src/vector_nodes.rs
Normal file
2445
node-graph/nodes/vector/src/vector_nodes.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user