This commit is contained in:
Firestar99
2025-06-30 17:19:23 +02:00
parent 9f9a50e79a
commit 79c47637d2
85 changed files with 1148 additions and 844 deletions
@@ -1,10 +1,10 @@
use graphene_core::GraphicGroupTable;
use graphene_core::blending::BlendMode;
use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::raster_types::{CPU, RasterDataTable};
use graphene_core::registry::types::Percentage;
use graphene_core::vector::VectorDataTable;
use graphene_element::GraphicGroupTable;
use graphene_raster::{CPU, RasterDataTable};
use graphene_vector::VectorDataTable;
pub(super) trait MultiplyAlpha {
fn multiply_alpha(&mut self, factor: f64);
+6 -5
View File
@@ -1,10 +1,11 @@
use glam::DVec2;
use graphene_core::GraphicElement;
use graphene_core::GraphicGroupTable;
use graphene_core::color::Color;
use graphene_core::context::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, OwnedContextImpl};
use graphene_core::instances::{InstanceRef, Instances};
use graphene_core::raster_types::{CPU, RasterDataTable};
use graphene_core::vector::VectorDataTable;
use graphene_element::GraphicElement;
use graphene_element::GraphicGroupTable;
use graphene_raster::{CPU, GPU, RasterDataTable};
use graphene_vector::VectorDataTable;
use log::warn;
#[node_macro::node(name("Instance on Points"), category("Instancing"), path(graphene_core::vector))]
@@ -105,7 +106,7 @@ mod test {
use glam::DVec2;
use graphene_core::Node;
use graphene_core::extract_xy::{ExtractXyNode, XY};
use graphene_core::vector::VectorData;
use graphene_vector::VectorData;
use std::pin::Pin;
#[derive(Clone)]
+1
View File
@@ -4,3 +4,4 @@ pub mod conversion;
pub mod instance;
pub mod logic;
pub mod transform_nodes;
pub mod vector_element_nodes;
+1 -1
View File
@@ -1,7 +1,7 @@
use glam::{DAffine2, DVec2};
use graphene_core::color::Color;
use graphene_core::context::{Context, Ctx};
use graphene_core::vector::VectorDataTable;
use graphene_vector::VectorDataTable;
#[node_macro::node(category("Debug"), name("Log to Console"))]
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
@@ -1,11 +1,11 @@
use core::f64;
use glam::{DAffine2, DVec2};
use graphene_core::GraphicGroupTable;
use graphene_core::context::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
use graphene_core::instances::Instances;
use graphene_core::raster_types::{CPU, GPU, RasterDataTable};
use graphene_core::transform::{ApplyTransform, Footprint, Transform};
use graphene_core::vector::VectorDataTable;
use graphene_element::GraphicGroupTable;
use graphene_raster::{CPU, GPU, RasterDataTable};
use graphene_vector::VectorDataTable;
#[node_macro::node(category(""))]
async fn transform<T: 'n + 'static>(
@@ -0,0 +1,538 @@
use glam::{DAffine2, DVec2};
use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::gradient::{Gradient, GradientStops};
use graphene_core::instances::{InstanceMut, Instances};
use graphene_core::registry::types::{Angle, IntegerCount, Multiplier, PixelSize, SeedValue};
use graphene_element::{GraphicElement, GraphicGroupTable};
use graphene_raster::{CPU, GPU, RasterDataTable};
use graphene_vector::reference_point::ReferencePoint;
use graphene_vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_vector::{VectorData, VectorDataTable};
use rand::{Rng, SeedableRng};
use std::f64::consts::TAU;
use std::hash::{DefaultHasher, Hash, Hasher};
/// Implemented for types that can be converted to an iterator of vector data.
/// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
trait VectorDataTableIterMut {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>>;
}
impl VectorDataTableIterMut for GraphicGroupTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>> {
// Grab only the direct children
self.instance_mut_iter()
.filter_map(|element| element.instance.as_vector_data_mut())
.flat_map(move |vector_data| vector_data.instance_mut_iter())
}
}
impl VectorDataTableIterMut for VectorDataTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>> {
self.instance_mut_iter()
}
}
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
async fn assign_colors<T>(
_: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable)]
#[widget(ParsedWidgetOverride::Hidden)]
/// The vector elements, or group of vector elements, to apply the fill and/or stroke style to.
mut vector_group: T,
#[default(true)]
/// Whether to style the fill.
fill: bool,
/// Whether to style the stroke.
stroke: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")]
/// The range of colors to select from.
gradient: GradientStops,
/// Whether to reverse the gradient.
reverse: bool,
/// Whether to randomize the color selection for each element from throughout the gradient.
randomize: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")]
/// The seed used for randomization.
seed: SeedValue,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")]
/// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once.
repeat_every: u32,
) -> T
where
T: VectorDataTableIterMut + 'n + Send,
{
let length = vector_group.vector_iter_mut().count();
let gradient = if reverse { gradient.reversed() } else { gradient };
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
for (i, vector_data) in vector_group.vector_iter_mut().enumerate() {
let factor = match randomize {
true => rng.random::<f64>(),
false => match repeat_every {
0 => i as f64 / (length - 1).max(1) as f64,
1 => 0.,
_ => i as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
},
};
let color = gradient.evaluate(factor);
if fill {
vector_data.instance.style.set_fill(Fill::Solid(color));
}
if stroke {
if let Some(stroke) = vector_data.instance.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
vector_data.instance.style.set_stroke(stroke);
}
}
}
vector_group
}
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
async fn fill<F: Into<Fill> + 'n + Send, V>(
_: impl Ctx,
#[implementations(
VectorDataTable,
VectorDataTable,
VectorDataTable,
VectorDataTable,
GraphicGroupTable,
GraphicGroupTable,
GraphicGroupTable,
GraphicGroupTable
)]
/// The vector elements, or group of vector elements, to apply the fill to.
mut vector_data: V,
#[implementations(
Fill,
Option<Color>,
Color,
Gradient,
Fill,
Option<Color>,
Color,
Gradient,
)]
#[default(Color::BLACK)]
/// The fill to paint the path with.
fill: F,
_backup_color: Option<Color>,
_backup_gradient: Gradient,
) -> V
where
V: VectorDataTableIterMut + 'n + Send,
{
let fill: Fill = fill.into();
for vector in vector_data.vector_iter_mut() {
let mut fill = fill.clone();
if let Fill::Gradient(gradient) = &mut fill {
gradient.transform *= *vector.transform;
}
vector.instance.style.set_fill(fill);
}
vector_data
}
/// Applies a stroke style to the vector data contained in the input.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
_: impl Ctx,
#[implementations(VectorDataTable, VectorDataTable, GraphicGroupTable, GraphicGroupTable)]
/// The vector elements, or group of vector elements, to apply the stroke to.
mut vector_data: Instances<V>,
#[implementations(
Option<Color>,
Color,
Option<Color>,
Color,
)]
#[default(Color::BLACK)]
/// The stroke color.
color: C,
#[default(2.)]
/// The stroke weight.
weight: f64,
/// The alignment of stroke to the path's centerline or (for closed shapes) the inside or outside of the shape.
align: StrokeAlign,
/// The shape of the stroke at open endpoints.
cap: StrokeCap,
/// The curvature of the bent stroke at sharp corners.
join: StrokeJoin,
#[default(4.)]
/// The threshold for when a miter-joined stroke is converted to a bevel-joined stroke when a sharp angle becomes pointier than this ratio.
miter_limit: f64,
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
/// <https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty>
paint_order: PaintOrder,
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
dash_lengths: Vec<f64>,
/// The phase offset distance from the starting point of the dash pattern.
dash_offset: f64,
) -> Instances<V>
where
Instances<V>: VectorDataTableIterMut + 'n + Send,
{
let stroke = Stroke {
color: color.into(),
weight,
dash_lengths,
dash_offset,
cap,
join,
join_miter_limit: miter_limit,
align,
transform: DAffine2::IDENTITY,
non_scaling: false,
paint_order,
};
for vector in vector_data.vector_iter_mut() {
let mut stroke = stroke.clone();
stroke.transform *= *vector.transform;
vector.instance.style.set_stroke(stroke);
}
vector_data
}
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn repeat<I: 'n + Send + Clone>(
_: impl Ctx,
// TODO: Implement other GraphicElementRendered types.
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<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(4)] instances: IntegerCount,
) -> Instances<I> {
let angle = angle.to_radians();
let count = instances.max(1);
let total = (count - 1) as f64;
let mut result_table = Instances::<I>::default();
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 instance in instance.instance_ref_iter() {
let mut instance = instance.to_instance_cloned();
let local_translation = DAffine2::from_translation(instance.transform.translation);
let local_matrix = DAffine2::from_mat2(instance.transform.matrix2);
instance.transform = local_translation * transform * local_matrix;
result_table.push(instance);
}
}
result_table
}
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn circular_repeat<I: 'n + Send + Clone>(
_: impl Ctx,
// TODO: Implement other GraphicElementRendered types.
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
angle_offset: Angle,
#[default(5)] radius: f64,
#[default(5)] instances: IntegerCount,
) -> Instances<I> {
let count = instances.max(1);
let mut result_table = Instances::<I>::default();
for index in 0..count {
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + angle_offset.to_radians());
let translation = DAffine2::from_translation(radius * DVec2::Y);
let transform = angle * translation;
for instance in instance.instance_ref_iter() {
let mut instance = instance.to_instance_cloned();
let local_translation = DAffine2::from_translation(instance.transform.translation);
let local_matrix = DAffine2::from_mat2(instance.transform.matrix2);
instance.transform = local_translation * transform * local_matrix;
result_table.push(instance);
}
}
result_table
}
#[node_macro::node(name("Copy to Points"), category("Instancing"), path(graphene_core::vector))]
async fn copy_to_points<I: 'n + Send + Clone>(
_: impl Ctx,
points: VectorDataTable,
#[expose]
/// Artwork to be copied and placed at each point.
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)]
instance: Instances<I>,
/// Minimum range of randomized sizes given to each instance.
#[default(1)]
#[range((0., 2.))]
#[unit("x")]
random_scale_min: Multiplier,
/// Maximum range of randomized sizes given to each instance.
#[default(1)]
#[range((0., 2.))]
#[unit("x")]
random_scale_max: Multiplier,
/// Bias for the probability distribution of randomized sizes (0 is uniform, negatives favor more of small sizes, positives favor more of large sizes).
#[range((-50., 50.))]
random_scale_bias: f64,
/// Seed to determine unique variations on all the randomized instance sizes.
random_scale_seed: SeedValue,
/// Range of randomized angles given to each instance, in degrees ranging from furthest clockwise to counterclockwise.
#[range((0., 360.))]
random_rotation: Angle,
/// Seed to determine unique variations on all the randomized instance angles.
random_rotation_seed: SeedValue,
) -> Instances<I> {
let mut result_table = Instances::<I>::default();
let random_scale_difference = random_scale_max - random_scale_min;
for point_instance in points.instance_iter() {
let mut scale_rng = rand::rngs::StdRng::seed_from_u64(random_scale_seed.into());
let mut rotation_rng = rand::rngs::StdRng::seed_from_u64(random_rotation_seed.into());
let do_scale = random_scale_difference.abs() > 1e-6;
let do_rotation = random_rotation.abs() > 1e-6;
let points_transform = point_instance.transform;
for &point in point_instance.instance.point_domain.positions() {
let translation = points_transform.transform_point2(point);
let rotation = if do_rotation {
let degrees = (rotation_rng.random::<f64>() - 0.5) * random_rotation;
degrees / 360. * TAU
} else {
0.
};
let scale = if do_scale {
if random_scale_bias.abs() < 1e-6 {
// Linear
random_scale_min + scale_rng.random::<f64>() * random_scale_difference
} else {
// Weighted (see <https://www.desmos.com/calculator/gmavd3m9bd>)
let horizontal_scale_factor = 1. - 2_f64.powf(random_scale_bias);
let scale_factor = (1. - scale_rng.random::<f64>() * horizontal_scale_factor).log2() / random_scale_bias;
random_scale_min + scale_factor * random_scale_difference
}
} else {
random_scale_min
};
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation);
for mut instance in instance.instance_ref_iter().map(|instance| instance.to_instance_cloned()) {
let local_matrix = DAffine2::from_mat2(instance.transform.matrix2);
instance.transform = transform * local_matrix;
result_table.push(instance);
}
}
}
result_table
}
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn mirror<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
offset: f64,
#[range((-90., 90.))] angle: Angle,
#[default(true)] keep_original: bool,
) -> Instances<I> {
let mut result_table = Instances::default();
// Normalize the direction vector
let normal = DVec2::from_angle(angle.to_radians());
// The mirror reference is based on the bounding box (at least for now, until we have proper local layer origins)
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY, false) else {
return result_table;
};
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))
};
// Add original instance depending on the keep_original flag
if keep_original {
for instance in instance.clone().instance_iter() {
result_table.push(instance);
}
}
// Create and add mirrored instance
for mut instance in instance.instance_iter() {
instance.transform = reflected_transform * instance.transform;
instance.source_node_id = None;
result_table.push(instance);
}
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn flatten_path<I: 'n + Send>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable)] graphic_group_input: Instances<I>) -> VectorDataTable {
// A node based solution to support passing through vector data could be a network node with a cache node connected to
// a Flatten Path connected to an if else node, another connection from the cache directly
// To the if else node, and another connection from the cache to a matches type node connected to the if else node.
fn flatten_group(graphic_group_table: &GraphicGroupTable, output: &mut InstanceMut<VectorData>) {
for (group_index, current_element) in graphic_group_table.instance_ref_iter().enumerate() {
match current_element.instance {
GraphicElement::VectorData(vector_data_table) => {
// Loop through every row of the VectorDataTable and concatenate each instance's subpath into the output VectorData instance.
for (vector_index, vector_data_instance) in vector_data_table.instance_ref_iter().enumerate() {
let other = vector_data_instance.instance;
let transform = *current_element.transform * *vector_data_instance.transform;
let node_id = current_element.source_node_id.map(|node_id| node_id.0).unwrap_or_default();
let mut hasher = DefaultHasher::new();
(group_index, vector_index, node_id).hash(&mut hasher);
let collision_hash_seed = hasher.finish();
output.instance.concat(other, transform, collision_hash_seed);
// Use the last encountered style as the output style
output.instance.style = vector_data_instance.instance.style.clone();
}
}
GraphicElement::GraphicGroup(graphic_group) => {
let mut graphic_group = graphic_group.clone();
for instance in graphic_group.instance_mut_iter() {
*instance.transform = *current_element.transform * *instance.transform;
}
flatten_group(&graphic_group, output);
}
_ => {}
}
}
}
// Create a table with one instance of an empty VectorData, then get a mutable reference to it which we append flattened subpaths to
let mut output_table = VectorDataTable::new(VectorData::default());
let Some(mut output) = output_table.instance_mut_iter().next() else {
return output_table;
};
// Flatten the graphic group input into the output VectorData instance
let base_graphic_group = GraphicGroupTable::new(graphic_group_input.to_graphic_element());
flatten_group(&base_graphic_group, &mut output);
// Return the single-row VectorDataTable containing the flattened VectorData subpaths
output_table
}
#[node_macro::node(category("General"), path(graphene_core::vector))]
async fn count_elements<I>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] source: Instances<I>) -> u64 {
source.instance_iter().count() as u64
}
#[cfg(test)]
mod tests {
use super::*;
use bezier_rs::Subpath;
use graphene_core::transform::Footprint;
use graphene_vector::PointId;
fn vector_node(data: Subpath<PointId>) -> VectorDataTable {
VectorDataTable::new(VectorData::from_subpath(data))
}
#[tokio::test]
async fn repeat() {
let direction = DVec2::X * 1.5;
let instances = 3;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
let vector_data = super::flatten_path(Footprint::default(), repeated).await;
let vector_data = vector_data.instance_ref_iter().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 3);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn repeat_transform_position() {
let direction = DVec2::new(12., 10.);
let instances = 8;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
let vector_data = super::flatten_path(Footprint::default(), repeated).await;
let vector_data = vector_data.instance_ref_iter().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn circular_repeat() {
let repeated = super::circular_repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
let vector_data = super::flatten_path(Footprint::default(), repeated).await;
let vector_data = vector_data.instance_ref_iter().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
}
}
#[tokio::test]
async fn copy_to_points() {
let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.);
let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE);
let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec();
let copy_to_points = super::copy_to_points(Footprint::default(), vector_node(points), vector_node(instance), 1., 1., 0., 0, 0., 0).await;
let flatten_path = super::flatten_path(Footprint::default(), copy_to_points).await;
let flattened_copy_to_points = flatten_path.instance_ref_iter().next().unwrap().instance;
assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len());
for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() {
let offset = expected_points[index];
assert_eq!(
&subpath.anchors(),
&[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),]
);
}
}
}