This commit is contained in:
Firestar99 2025-06-26 18:09:23 +02:00
parent 9f9a50e79a
commit 79c47637d2
85 changed files with 1148 additions and 844 deletions

121
Cargo.lock generated
View file

@ -2133,10 +2133,16 @@ dependencies = [
"graphene-application-io",
"graphene-brush",
"graphene-core",
"graphene-element",
"graphene-element-nodes",
"graphene-math-nodes",
"graphene-path-bool",
"graphene-raster",
"graphene-raster-nodes",
"graphene-svg-renderer",
"graphene-text",
"graphene-vector",
"graphene-vector-nodes",
"iai-callgrind",
"js-sys",
"log",
@ -2161,6 +2167,7 @@ dependencies = [
"dyn-any",
"glam",
"graphene-core",
"graphene-text",
"log",
"serde",
"web-sys",
@ -2174,9 +2181,15 @@ dependencies = [
"dyn-any",
"glam",
"graphene-core",
"graphene-element",
"graphene-raster",
"graphene-raster-nodes",
"graphene-svg-renderer",
"graphene-vector",
"node-macro",
"serde",
"serde_json",
"specta",
"tokio",
]
@ -2189,7 +2202,6 @@ dependencies = [
"fern",
"futures",
"graph-craft",
"graphene-core",
"graphene-std",
"interpreted-executor",
"log",
@ -2204,14 +2216,12 @@ name = "graphene-core"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"bezier-rs",
"bytemuck",
"ctor",
"dyn-any",
"glam",
"half",
"image",
"kurbo",
"log",
"node-macro",
"num-derive",
@ -2229,6 +2239,22 @@ dependencies = [
"wgpu",
]
[[package]]
name = "graphene-element"
version = "0.1.0"
dependencies = [
"bezier-rs",
"dyn-any",
"glam",
"graphene-core",
"graphene-raster",
"graphene-vector",
"node-macro",
"serde",
"serde_json",
"specta",
]
[[package]]
name = "graphene-element-nodes"
version = "0.1.0"
@ -2237,7 +2263,10 @@ dependencies = [
"dyn-any",
"glam",
"graphene-core",
"graphene-element",
"graphene-math-nodes",
"graphene-raster",
"graphene-vector",
"log",
"node-macro",
"rand 0.9.0",
@ -2266,6 +2295,8 @@ dependencies = [
"dyn-any",
"glam",
"graphene-core",
"graphene-element",
"graphene-vector",
"log",
"node-macro",
"path-bool",
@ -2273,6 +2304,23 @@ dependencies = [
"specta",
]
[[package]]
name = "graphene-raster"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"bytemuck",
"dyn-any",
"glam",
"graphene-core",
"image",
"node-macro",
"serde",
"serde_json",
"specta",
"wgpu",
]
[[package]]
name = "graphene-raster-nodes"
version = "0.1.0"
@ -2284,6 +2332,7 @@ dependencies = [
"futures",
"glam",
"graphene-core",
"graphene-raster",
"image",
"ndarray",
"node-macro",
@ -2308,14 +2357,18 @@ dependencies = [
"graphene-application-io",
"graphene-brush",
"graphene-core",
"graphene-element",
"graphene-element-nodes",
"graphene-math-nodes",
"graphene-path-bool",
"graphene-raster",
"graphene-raster-nodes",
"graphene-svg-renderer",
"graphene-text",
"graphene-vector",
"graphene-vector-nodes",
"image",
"log",
"ndarray",
"node-macro",
"rand 0.9.0",
"rand_chacha 0.9.0",
@ -2337,13 +2390,73 @@ dependencies = [
"dyn-any",
"glam",
"graphene-core",
"graphene-element",
"graphene-raster",
"graphene-vector",
"log",
"node-macro",
"num-traits",
"serde",
"serde_json",
"specta",
"usvg",
"vello",
]
[[package]]
name = "graphene-text"
version = "0.1.0"
dependencies = [
"bezier-rs",
"dyn-any",
"glam",
"graphene-core",
"graphene-vector",
"kurbo",
"node-macro",
"rustybuzz 0.20.1",
"serde",
"specta",
]
[[package]]
name = "graphene-vector"
version = "0.1.0"
dependencies = [
"bezier-rs",
"dyn-any",
"glam",
"graphene-core",
"kurbo",
"log",
"node-macro",
"petgraph 0.7.1",
"rustc-hash 2.1.1",
"serde",
"specta",
"tinyvec",
]
[[package]]
name = "graphene-vector-nodes"
version = "0.1.0"
dependencies = [
"bezier-rs",
"dyn-any",
"glam",
"graphene-core",
"graphene-vector",
"kurbo",
"log",
"node-macro",
"petgraph 0.7.1",
"rand 0.9.0",
"rustc-hash 2.1.1",
"serde",
"specta",
"tokio",
]
[[package]]
name = "graphite-desktop"
version = "0.1.0"

View file

@ -7,14 +7,22 @@ members = [
"node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gelement",
"node-graph/gelement-nodes",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/gelement-nodes",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster",
"node-graph/graster-nodes",
"node-graph/gstd",
"node-graph/gsvg-renderer",
"node-graph/gtext",
"node-graph/gvector",
"node-graph/gvector-nodes",
"node-graph/interpreted-executor",
"node-graph/node-macro",
"node-graph/preprocessor",
@ -29,14 +37,22 @@ default-members = [
"frontend/wasm",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gelement",
"node-graph/gelement-nodes",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/gelement-nodes",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster",
"node-graph/graster-nodes",
"node-graph/gstd",
"node-graph/gsvg-renderer",
"node-graph/gtext",
"node-graph/gvector",
"node-graph/gvector-nodes",
"node-graph/interpreted-executor",
"node-graph/node-macro",
]
@ -52,13 +68,18 @@ path-bool = { path = "libraries/path-bool" }
graphene-application-io = { path = "node-graph/gapplication-io" }
graphene-brush = { path = "node-graph/gbrush" }
graphene-core = { path = "node-graph/gcore" }
graphene-element = { path = "node-graph/gelement" }
graphene-element-nodes = { path = "node-graph/gelement-nodes" }
graphene-math-nodes = { path = "node-graph/gmath-nodes" }
graphene-path-bool = { path = "node-graph/gpath-bool" }
graph-craft = { path = "node-graph/graph-craft" }
graphene-raster = { path = "node-graph/graster" }
graphene-raster-nodes = { path = "node-graph/graster-nodes" }
graphene-std = { path = "node-graph/gstd" }
graphene-svg-renderer = { path = "node-graph/gsvg-renderer" }
graphene-text = { path = "node-graph/gtext" }
graphene-vector = { path = "node-graph/gvector" }
graphene-vector-nodes = { path = "node-graph/gvector-nodes" }
interpreted-executor = { path = "node-graph/interpreted-executor" }
node-macro = { path = "node-graph/node-macro" }
wgpu-executor = { path = "node-graph/wgpu-executor" }

View file

@ -14,6 +14,7 @@ wgpu = ["dep:wgpu"]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-text = { workspace = true }
# Workspace dependencies
glam = { workspace = true }

View file

@ -1,8 +1,8 @@
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::{DAffine2, UVec2};
use graphene_core::text::FontCache;
use graphene_core::transform::Footprint;
use graphene_core::vector::style::ViewMode;
use graphene_core::view_mode::ViewMode;
use graphene_text::FontCache;
use std::fmt::Debug;
use std::future::Future;
use std::hash::{Hash, Hasher};

View file

@ -14,11 +14,16 @@ serde = ["dep:serde"]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-vector = { workspace = true }
graphene-raster = { workspace = true }
graphene-element = { workspace = true }
graphene-raster-nodes = { workspace = true }
graphene-svg-renderer = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
specta = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }
@ -26,3 +31,4 @@ serde = { workspace = true, optional = true, features = ["derive"] }
[dev-dependencies]
# Workspace dependencies
tokio = { workspace = true }
serde_json = { workspace = true }

View file

@ -1,21 +1,22 @@
use crate::brush_cache::BrushCache;
use crate::brush_stroke::{BrushStroke, BrushStyle};
use glam::{DAffine2, DVec2};
use graphene_core::Node;
use graphene_core::blending::BlendMode;
use graphene_core::bounds::BoundingBox;
use graphene_core::color::{Alpha, Color, Pixel, Sample};
use graphene_core::generic::FnNode;
use graphene_core::instances::Instance;
use graphene_core::math::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::raster::BitmapMut;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::registry::FutureWrapperNode;
use graphene_core::transform::Transform;
use graphene_core::value::ClonedNode;
use graphene_core::{Ctx, Node};
use graphene_raster::bitmap::BitmapMut;
use graphene_raster::image::Image;
use graphene_raster::{CPU, Raster, RasterDataTable};
use graphene_raster_nodes::adjustments::blend_colors;
use graphene_raster_nodes::std_nodes::{empty_image, extend_image_to_bounds};
use graphene_svg_renderer::renderer::GraphicElementRendered;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BrushStampGenerator<P: Pixel + Alpha> {

View file

@ -27,13 +27,11 @@ rustc-hash = { workspace = true }
dyn-any = { workspace = true }
ctor = { workspace = true }
rand_chacha = { workspace = true }
bezier-rs = { workspace = true }
specta = { workspace = true }
rustybuzz = { workspace = true }
image = { workspace = true }
half = { workspace = true }
tinyvec = { workspace = true }
kurbo = { workspace = true }
log = { workspace = true }
base64 = { workspace = true }

View file

@ -1,4 +1,5 @@
use dyn_any::DynAny;
use log::warn;
use std::hash::Hash;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]

View file

@ -1,4 +1,4 @@
use crate::Color;
use crate::color::Color;
use glam::{DAffine2, DVec2};
pub trait BoundingBox {

View file

@ -5,8 +5,6 @@ use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
pub use crate::blending::*;
pub trait Linear {
fn from_f32(x: f32) -> Self;
fn to_f32(self) -> f32;

View file

@ -1,4 +1,4 @@
use crate::raster::Color;
use crate::color::Color;
// RENDERING
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;

View file

@ -1,5 +1,5 @@
use crate::raster_types::{CPU, RasterDataTable};
use crate::{Color, Ctx};
use crate::color::Color;
use crate::context::Ctx;
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
#[node_macro::node(category("Debug"))]
@ -19,8 +19,4 @@ fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, O
input.unwrap_or_default()
}
/// Meant for debugging purposes, not general use. Clones the input value.
#[node_macro::node(category("Debug"))]
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&RasterDataTable<CPU>)] value: &'i T) -> T {
value.clone()
}
// FIXME am I allowed to just remove clone?

View file

@ -1,4 +1,4 @@
use crate::Ctx;
use crate::context::Ctx;
use dyn_any::DynAny;
use glam::{DVec2, IVec2, UVec2};

View file

@ -1,4 +1,4 @@
use crate::Color;
use crate::color::Color;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};

View file

@ -1,4 +1,4 @@
use crate::AlphaBlending;
use crate::blending::AlphaBlending;
use crate::uuid::NodeId;
use dyn_any::StaticType;
use glam::DAffine2;

View file

@ -1,6 +1,3 @@
#[macro_use]
extern crate log;
pub mod blending;
pub mod bounds;
pub mod color;
@ -10,35 +7,26 @@ pub mod debug;
pub mod extract_xy;
pub mod generic;
pub mod gradient;
mod graphic_element;
pub mod instances;
pub mod math;
pub mod memo;
pub mod misc;
pub mod ops;
pub mod raster;
pub mod raster_types;
pub mod registry;
pub mod structural;
pub mod text;
pub mod transform;
pub mod uuid;
pub mod value;
pub mod vector;
pub use crate as graphene_core;
pub use blending::*;
pub use context::*;
pub use ctor;
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
pub use graphic_element::*;
pub use memo::MemoHash;
pub use num_traits;
pub use raster::Color;
use std::any::TypeId;
pub use std::borrow::Cow;
use std::future::Future;
use std::pin::Pin;
pub use types::Cow;
// pub trait Node: for<'n> NodeIO<'n> {
/// The node trait allows for defining any node. Nodes can only take one call argument input, however they can store references to other nodes inside the struct.

View file

@ -1,4 +1,3 @@
pub mod bbox;
pub mod math_ext;
pub mod quad;
pub mod rect;

View file

@ -1,7 +1,3 @@
use crate::Artboard;
use crate::math::bbox::AxisAlignedBbox;
pub use crate::vector::ReferencePoint;
use core::f64;
use glam::{DAffine2, DMat2, DVec2};
pub trait Transform {
@ -31,16 +27,6 @@ impl<T: Transform> Transform for &T {
}
}
// Implementations for Artboard
impl Transform for Artboard {
fn transform(&self) -> DAffine2 {
DAffine2::from_translation(self.location.as_dvec2())
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.location.as_dvec2() + self.dimensions.as_dvec2() * pivot
}
}
// Implementations for DAffine2
impl Transform for DAffine2 {
fn transform(&self) -> DAffine2 {
@ -110,13 +96,6 @@ impl Footprint {
quality: RenderQuality::Full,
};
pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox {
let inverse = self.transform.inverse();
let start = inverse.transform_point2((0., 0.).into());
let end = inverse.transform_point2(self.resolution.as_dvec2());
AxisAlignedBbox { start, end }
}
pub fn scale(&self) -> DVec2 {
self.transform.decompose_scale()
}

View file

@ -1,14 +0,0 @@
pub mod algorithms;
pub mod click_target;
pub mod generator_nodes;
pub mod misc;
mod reference_point;
pub mod style;
mod vector_data;
mod vector_nodes;
pub use bezier_rs;
pub use reference_point::*;
pub use style::PathStyle;
pub use vector_data::*;
pub use vector_nodes::*;

View file

@ -14,6 +14,9 @@ serde = ["dep:serde"]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-raster = { workspace = true }
graphene-vector = { workspace = true }
graphene-element = { workspace = true }
node-macro = { workspace = true }
bezier-rs = { workspace = true }

View file

@ -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);

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)]

View file

@ -4,3 +4,4 @@ pub mod conversion;
pub mod instance;
pub mod logic;
pub mod transform_nodes;
pub mod vector_element_nodes;

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 {

View file

@ -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>(

View file

@ -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.),]
);
}
}
}

View file

@ -0,0 +1,22 @@
[package]
name = "graphene-element"
version = "0.1.0"
edition = "2024"
description = "graphene element data format"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-raster = { workspace = true }
graphene-vector = { workspace = true }
node-macro = { workspace = true }
bezier-rs = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
specta = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

View file

@ -1,14 +1,13 @@
use crate::blending::AlphaBlending;
use crate::bounds::BoundingBox;
use crate::color::Color;
use crate::instances::{Instance, Instances};
use crate::math::quad::Quad;
use crate::raster::image::Image;
use crate::raster_types::{CPU, GPU, Raster, RasterDataTable};
use crate::uuid::NodeId;
use crate::vector::{VectorData, VectorDataTable};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2, IVec2};
use graphene_core::blending::AlphaBlending;
use graphene_core::color::Color;
use graphene_core::instances::{Instance, Instances};
use graphene_core::transform::Transform;
use graphene_core::uuid::NodeId;
use graphene_raster::image::Image;
use graphene_raster::{CPU, GPU, Raster, RasterDataTable};
use graphene_vector::{VectorData, VectorDataTable};
use std::hash::Hash;
// TODO: Eventually remove this migration document upgrade code
@ -246,6 +245,16 @@ pub struct Artboard {
pub clip: bool,
}
// Implementations for Artboard
impl Transform for Artboard {
fn transform(&self) -> DAffine2 {
DAffine2::from_translation(self.location.as_dvec2())
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.location.as_dvec2() + self.dimensions.as_dvec2() * pivot
}
}
impl Default for Artboard {
fn default() -> Self {
Self::new(IVec2::ZERO, IVec2::new(1920, 1080))

View file

@ -0,0 +1,2 @@
mod graphic_element;
pub use graphic_element::*;

View file

@ -1,7 +1,9 @@
use glam::DVec2;
use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::gradient::GradientStops;
use graphene_core::num_traits;
use graphene_core::registry::types::{Fraction, Percentage};
use graphene_core::{Color, Ctx, num_traits};
use log::warn;
use math_parser::ast;
use math_parser::context::{EvalContext, NothingMap, ValueProvider};

View file

@ -11,6 +11,8 @@ license = "MIT OR Apache-2.0"
dyn-any = { workspace = true }
bezier-rs = { workspace = true }
graphene-core = { workspace = true }
graphene-vector = { workspace = true }
graphene-element = { workspace = true }
node-macro = { workspace = true }
glam = { workspace = true }
specta = { workspace = true }

View file

@ -1,15 +1,13 @@
use bezier_rs::{ManipulatorGroup, Subpath};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphene_core::context::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
use graphene_core::instances::{Instance, InstanceRef};
use graphene_core::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use graphene_core::vector::style::Fill;
use graphene_core::vector::{PointId, VectorData, VectorDataTable};
use graphene_core::{Color, Ctx, GraphicElement, GraphicGroupTable};
pub use path_bool as path_bool_lib;
use graphene_element::{GraphicElement, GraphicGroupTable};
use graphene_vector::style::Fill;
use graphene_vector::{PointId, VectorData, VectorDataTable};
use path_bool::{FillRule, PathBooleanOperation};
use std::ops::Mul;
// TODO: Fix boolean ops to work by removing .transform() and .one_instnace_*() calls,
// TODO: since before we used a Vec of single-row tables and now we use a single table
// TODO: with multiple rows while still assuming a single row for the boolean operations.

View file

@ -20,8 +20,14 @@ graphene-element-nodes = { workspace = true }
graphene-path-bool = { workspace = true }
graphene-brush = { workspace = true }
graphene-application-io = { workspace = true }
graphene-element = { workspace = true }
graphene-math-nodes = { workspace = true }
graphene-raster = { workspace = true }
graphene-svg-renderer = { workspace = true }
graphene-raster-nodes = { workspace = true }
graphene-text = { workspace = true }
graphene-vector = { workspace = true }
graphene-vector-nodes = { workspace = true }
# Workspace dependencies
log = { workspace = true }

View file

@ -7,12 +7,13 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphene_application_io::SurfaceFrame;
use graphene_brush::brush_cache::BrushCache;
use graphene_brush::brush_stroke::BrushStroke;
use graphene_core::raster_types::CPU;
use graphene_core::transform::ReferencePoint;
use graphene_core::color::Color;
use graphene_core::uuid::NodeId;
use graphene_core::vector::style::Fill;
use graphene_core::{Color, MemoHash, Node, Type};
use graphene_core::{MemoHash, Node, Type};
use graphene_raster::CPU;
use graphene_svg_renderer::RenderMetadata;
use graphene_vector::reference_point::ReferencePoint;
use graphene_vector::style::Fill;
use std::fmt::Display;
use std::hash::Hash;
use std::marker::PhantomData;
@ -181,41 +182,41 @@ tagged_value! {
F64Array4([f64; 4]),
NodePath(Vec<NodeId>),
#[serde(alias = "ManipulatorGroupIds")] // TODO: Eventually remove this alias document upgrade code
PointIds(Vec<graphene_core::vector::PointId>),
PointIds(Vec<graphene_vector::PointId>),
// ====================
// GRAPHICAL DATA TYPES
// ====================
GraphicElement(graphene_core::GraphicElement),
GraphicElement(graphene_element::GraphicElement),
#[cfg_attr(target_arch = "wasm32", serde(deserialize_with = "graphene_core::vector::migrate_vector_data"))] // TODO: Eventually remove this migration document upgrade code
VectorData(graphene_core::vector::VectorDataTable),
VectorData(graphene_vector::VectorDataTable),
#[cfg_attr(target_arch = "wasm32", serde(alias = "ImageFrame", deserialize_with = "graphene_core::raster::image::migrate_image_frame"))] // TODO: Eventually remove this migration document upgrade code
RasterData(graphene_core::raster_types::RasterDataTable<CPU>),
RasterData(graphene_raster::RasterDataTable<CPU>),
#[cfg_attr(target_arch = "wasm32", serde(deserialize_with = "graphene_core::migrate_graphic_group"))] // TODO: Eventually remove this migration document upgrade code
GraphicGroup(graphene_core::GraphicGroupTable),
GraphicGroup(graphene_element::GraphicGroupTable),
#[cfg_attr(target_arch = "wasm32", serde(deserialize_with = "graphene_core::migrate_artboard_group"))] // TODO: Eventually remove this migration document upgrade code
ArtboardGroup(graphene_core::ArtboardGroupTable),
ArtboardGroup(graphene_element::ArtboardGroupTable),
// ============
// STRUCT TYPES
// ============
Artboard(graphene_core::Artboard),
Image(graphene_core::raster::Image<Color>),
Color(graphene_core::raster::color::Color),
OptionalColor(Option<graphene_core::raster::color::Color>),
Artboard(graphene_element::Artboard),
Image(graphene_raster::image::Image<Color>),
Color(Color),
OptionalColor(Option<Color>),
Palette(Vec<Color>),
Subpaths(Vec<bezier_rs::Subpath<graphene_core::vector::PointId>>),
Fill(graphene_core::vector::style::Fill),
Stroke(graphene_core::vector::style::Stroke),
Gradient(graphene_core::vector::style::Gradient),
Subpaths(Vec<bezier_rs::Subpath<graphene_vector::PointId>>),
Fill(graphene_vector::style::Fill),
Stroke(graphene_vector::style::Stroke),
Gradient(graphene_core::gradient::Gradient),
#[serde(alias = "GradientPositions")] // TODO: Eventually remove this alias document upgrade code
GradientStops(graphene_core::vector::style::GradientStops),
Font(graphene_core::text::Font),
GradientStops(graphene_core::gradient::GradientStops),
Font(graphene_text::Font),
BrushStrokes(Vec<BrushStroke>),
BrushCache(BrushCache),
DocumentNode(DocumentNode),
Curve(graphene_raster_nodes::curve::Curve),
Footprint(graphene_core::transform::Footprint),
VectorModification(Box<graphene_core::vector::VectorModification>),
FontCache(Arc<graphene_core::text::FontCache>),
VectorModification(Box<graphene_vector_nodes::modification::VectorModification>),
FontCache(Arc<graphene_text::FontCache>),
// ==========
// ENUM TYPES
// ==========
@ -232,21 +233,21 @@ tagged_value! {
DomainWarpType(graphene_raster_nodes::adjustments::DomainWarpType),
RelativeAbsolute(graphene_raster_nodes::adjustments::RelativeAbsolute),
SelectiveColorChoice(graphene_raster_nodes::adjustments::SelectiveColorChoice),
GridType(graphene_core::vector::misc::GridType),
ArcType(graphene_core::vector::misc::ArcType),
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
PointSpacingType(graphene_core::vector::misc::PointSpacingType),
GridType(graphene_vector_nodes::misc::GridType),
ArcType(graphene_vector_nodes::misc::ArcType),
MergeByDistanceAlgorithm(graphene_vector_nodes::misc::MergeByDistanceAlgorithm),
PointSpacingType(graphene_vector_nodes::misc::PointSpacingType),
#[serde(alias = "LineCap")]
StrokeCap(graphene_core::vector::style::StrokeCap),
StrokeCap(graphene_vector::style::StrokeCap),
#[serde(alias = "LineJoin")]
StrokeJoin(graphene_core::vector::style::StrokeJoin),
StrokeAlign(graphene_core::vector::style::StrokeAlign),
PaintOrder(graphene_core::vector::style::PaintOrder),
FillType(graphene_core::vector::style::FillType),
FillChoice(graphene_core::vector::style::FillChoice),
GradientType(graphene_core::vector::style::GradientType),
ReferencePoint(graphene_core::transform::ReferencePoint),
CentroidType(graphene_core::vector::misc::CentroidType),
StrokeJoin(graphene_vector::style::StrokeJoin),
StrokeAlign(graphene_vector::style::StrokeAlign),
PaintOrder(graphene_vector::style::PaintOrder),
FillType(graphene_vector::style::FillType),
FillChoice(graphene_vector::style::FillChoice),
GradientType(graphene_core::gradient::GradientType),
ReferencePoint(graphene_vector::reference_point::ReferencePoint),
CentroidType(graphene_vector_nodes::misc::CentroidType),
BooleanOperation(graphene_path_bool::BooleanOperation),
}

View file

@ -16,7 +16,6 @@ gpu = ["interpreted-executor/gpu", "graphene-std/gpu", "wgpu-executor"]
[dependencies]
# Local dependencies
graphene-core = { workspace = true }
graphene-std = { workspace = true }
interpreted-executor = { workspace = true }
graph-craft = { workspace = true, features = ["loading"] }

View file

@ -14,6 +14,7 @@ serde = ["dep:serde"]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-raster = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies

View file

@ -1,6 +1,5 @@
#![allow(clippy::too_many_arguments)]
use crate::curve::CubicSplines;
use dyn_any::DynAny;
use graphene_core::Node;
use graphene_core::blending::BlendMode;
@ -8,12 +7,12 @@ use graphene_core::color::Color;
use graphene_core::color::Pixel;
use graphene_core::context::Ctx;
use graphene_core::gradient::GradientStops;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::registry::types::{Angle, Percentage, SignedPercentage};
use graphene_raster::curve::CubicSplines;
use graphene_raster::image::Image;
use graphene_raster::{CPU, Raster, RasterDataTable};
use std::cmp::Ordering;
use std::fmt::Debug;
// TODO: Implement the following:
// Color Balance
// Aims for interoperable compatibility with:
@ -1181,8 +1180,8 @@ fn color_overlay<T: Adjust<Color>>(
mod test {
use graphene_core::blending::BlendMode;
use graphene_core::color::Color;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{Raster, RasterDataTable};
use graphene_raster::image::Image;
use graphene_raster::{Raster, RasterDataTable};
#[tokio::test]
async fn color_overlay_multiply() {

View file

@ -1,7 +1,7 @@
use graphene_core::context::Ctx;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::registry::types::Percentage;
use graphene_raster::image::Image;
use graphene_raster::{CPU, Raster, RasterDataTable};
use image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage};
use ndarray::{Array2, ArrayBase, Dim, OwnedRepr};
use std::cmp::{max, min};

View file

@ -1,9 +1,9 @@
use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::raster::image::Image;
use graphene_core::raster::{Bitmap, BitmapMut};
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::registry::types::PixelLength;
use graphene_raster::bitmap::{Bitmap, BitmapMut};
use graphene_raster::image::Image;
use graphene_raster::{CPU, Raster, RasterDataTable};
/// Blurs the image with a Gaussian or blur kernel filter.
#[node_macro::node(category("Raster: Filter"))]

View file

@ -1,6 +1,6 @@
use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::raster_types::{CPU, RasterDataTable};
use graphene_raster::{CPU, RasterDataTable};
#[node_macro::node(category("Color"))]
async fn image_color_palette(
@ -65,8 +65,8 @@ async fn image_color_palette(
#[cfg(test)]
mod test {
use super::*;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{Raster, RasterDataTable};
use graphene_raster::image::Image;
use graphene_raster::{Raster, RasterDataTable};
#[test]
fn test_image_color_palette() {

View file

@ -8,10 +8,11 @@ use graphene_core::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, R
use graphene_core::context::{Ctx, ExtractFootprint};
use graphene_core::instances::Instance;
use graphene_core::math::bbox::Bbox;
use graphene_core::raster::image::Image;
use graphene_core::raster::{Bitmap, BitmapMut};
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::transform::Transform;
use graphene_raster::bitmap::{Bitmap, BitmapMut};
use graphene_raster::image::Image;
use graphene_raster::transform::FootprintExt;
use graphene_raster::{CPU, Raster, RasterDataTable};
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::fmt::Debug;

View file

@ -0,0 +1,32 @@
[package]
name = "graphene-raster"
version = "0.1.0"
edition = "2024"
description = "graphene raster data format"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde"]
wgpu = ["dep:wgpu"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
specta = { workspace = true }
base64 = { workspace = true }
bytemuck = { workspace = true }
image = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }
wgpu = { workspace = true, optional = true }
[dev-dependencies]
serde_json = { workspace = true }

View file

@ -1,20 +1,4 @@
use crate::GraphicGroupTable;
pub use crate::color::*;
use crate::raster_types::{CPU, RasterDataTable};
use crate::vector::VectorDataTable;
use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
/// as to not yet rename all references
pub mod color {
pub use super::*;
}
pub mod image;
pub use self::image::Image;
use graphene_core::color::Pixel;
pub trait Bitmap {
type Pixel: Pixel;

View file

@ -1,18 +1,19 @@
use super::Color;
use crate::AlphaBlending;
use crate::color::float_to_srgb_u8;
use crate::instances::{Instance, Instances};
use crate::raster_types::Raster;
use crate::bitmap::{Bitmap, BitmapMut};
use crate::{CPU, Raster, RasterDataTable};
use core::hash::{Hash, Hasher};
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use graphene_core::blending::AlphaBlending;
use graphene_core::color::{Alpha, AssociatedAlpha, Color, Linear, Pixel, RGB, SRGBA8, Sample, float_to_srgb_u8};
use graphene_core::instances::{Instance, Instances};
use std::fmt::Debug;
use std::vec::Vec;
mod base64_serde {
//! Basic wrapper for [`serde`] to perform [`base64`] encoding
use super::super::Pixel;
use base64::Engine;
use graphene_core::color::Pixel;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn as_base64<S: Serializer, P: Pixel>(key: &[P], serializer: S) -> Result<S::Ok, S::Error> {
@ -138,7 +139,6 @@ impl Image<Color> {
}
}
use super::*;
impl<P: Alpha + RGB + AssociatedAlpha> Image<P>
where
P::ColorChannel: Linear,
@ -485,7 +485,6 @@ mod test {
#[test]
fn test_image_serialization_roundtrip() {
use super::*;
use crate::Color;
let image = Image {
width: 2,
height: 2,

View file

@ -0,0 +1,5 @@
pub mod bitmap;
pub mod image;
mod raster_types;
pub use raster_types::*;

View file

@ -1,11 +1,11 @@
use crate::Color;
use crate::bounds::BoundingBox;
use crate::instances::Instances;
use crate::math::quad::Quad;
use crate::raster::Image;
use crate::image::Image;
use core::ops::Deref;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphene_core::bounds::BoundingBox;
use graphene_core::color::Color;
use graphene_core::instances::Instances;
use graphene_core::math::quad::Quad;
#[cfg(feature = "wgpu")]
use std::sync::Arc;

View file

@ -32,9 +32,14 @@ graphene-path-bool = { workspace = true }
graphene-math-nodes = { workspace = true }
graphene-svg-renderer = { workspace = true }
graphene-application-io = { workspace = true }
graphene-element = { workspace = true }
graphene-element-nodes = { workspace = true }
graphene-raster = { workspace = true }
graphene-raster-nodes = { workspace = true }
graphene-vector = { workspace = true }
graphene-vector-nodes = { workspace = true }
graphene-brush = { workspace = true }
graphene-text = { workspace = true }
# Workspace dependencies
fastnoise-lite = { workspace = true }
@ -66,8 +71,5 @@ web-sys = { workspace = true, optional = true, features = [
"ImageBitmapRenderingContext",
] }
# Required dependencies
ndarray = "0.16.1"
[dev-dependencies]
tokio = { workspace = true }

View file

@ -6,13 +6,14 @@ pub mod wasm_application_io;
pub use graphene_application_io as application_io;
pub use graphene_brush as brush;
pub use graphene_core::vector;
pub use graphene_core::*;
pub use graphene_element as element;
pub use graphene_element_nodes as element_nodes;
pub use graphene_element_nodes::animation;
pub use graphene_math_nodes as math_nodes;
pub use graphene_path_bool as path_bool;
pub use graphene_raster_nodes as raster_nodes;
pub use graphene_vector as vector;
/// stop gap solutions until all paths have been replaced with their absolute ones
pub mod renderer {

View file

@ -1,7 +1,7 @@
use crate::vector::{VectorData, VectorDataTable};
use graph_craft::wasm_application_io::WasmEditorApi;
use graphene_core::Ctx;
pub use graphene_core::text::*;
use graphene_core::context::Ctx;
pub use graphene_text::*;
use graphene_vector::{VectorData, VectorDataTable};
#[node_macro::node(category(""))]
fn text<'i: 'n>(

View file

@ -13,11 +13,17 @@ vello = ["dep:vello", "bezier-rs/kurbo"]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-raster = { workspace = true }
graphene-element = { workspace = true }
graphene-vector = { workspace = true }
node-macro = { workspace = true }
bezier-rs = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
specta = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
base64 = { workspace = true }
log = { workspace = true }
num-traits = { workspace = true }

View file

@ -1,6 +1,6 @@
use bezier_rs::{ManipulatorGroup, Subpath};
use glam::DVec2;
use graphene_core::vector::PointId;
use graphene_vector::PointId;
pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
let mut subpaths = Vec::new();

View file

@ -3,8 +3,8 @@ use glam::{DAffine2, DVec2};
use graphene_core::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use graphene_core::gradient::{Gradient, GradientType};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, PaintOrder, PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin, ViewMode};
use std::fmt::Write;
use graphene_core::view_mode::ViewMode;
use graphene_vector::style::{Fill, PaintOrder, PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
pub trait RenderExt {
type Output;

View file

@ -1,21 +1,22 @@
use crate::render_ext::RenderExt;
use crate::to_peniko::BlendModeExt;
use base64::Engine;
use bezier_rs::Subpath;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphene_core::blending::BlendMode;
use graphene_core::bounds::BoundingBox;
use graphene_core::color::Color;
use graphene_core::instances::Instance;
use graphene_core::math::quad::Quad;
use graphene_core::raster::Image;
use graphene_core::raster_types::{CPU, GPU, RasterDataTable};
use graphene_core::math::rect::Rect;
use graphene_core::transform::{Footprint, Transform};
use graphene_core::uuid::{NodeId, generate_uuid};
use graphene_core::vector::VectorDataTable;
use graphene_core::vector::click_target::{ClickTarget, FreePoint};
use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode};
use graphene_core::{AlphaBlending, Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTable};
use graphene_core::view_mode::ViewMode;
use graphene_element::{Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTable};
use graphene_raster::image::Image;
use graphene_raster::{CPU, GPU, RasterDataTable};
use graphene_vector::VectorDataTable;
use graphene_vector::click_target::{ClickTarget, FreePoint};
use graphene_vector::style::{Fill, Stroke, StrokeAlign};
use num_traits::Zero;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;

View file

@ -0,0 +1,28 @@
[package]
name = "graphene-text"
version = "0.1.0"
edition = "2024"
description = "graphene text nodes"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = ["dep:serde"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
bezier-rs = { workspace = true }
graphene-core = { workspace = true }
graphene-vector = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
kurbo = { workspace = true }
glam = { workspace = true }
specta = { workspace = true }
rustybuzz = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }

View file

@ -1,4 +1,5 @@
use dyn_any::DynAny;
use graphene_core::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE};
use std::collections::HashMap;
/// A font type (storing font family and font style and an optional preview URL)
@ -16,7 +17,7 @@ impl Font {
}
impl Default for Font {
fn default() -> Self {
Self::new(crate::consts::DEFAULT_FONT_FAMILY.into(), crate::consts::DEFAULT_FONT_STYLE.into())
Self::new(DEFAULT_FONT_FAMILY.into(), DEFAULT_FONT_STYLE.into())
}
}
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
@ -33,9 +34,7 @@ impl FontCache {
if self.font_file_data.contains_key(font) {
Some(font)
} else {
self.font_file_data
.keys()
.find(|font| font.font_family == crate::consts::DEFAULT_FONT_FAMILY && font.font_style == crate::consts::DEFAULT_FONT_STYLE)
self.font_file_data.keys().find(|font| font.font_family == DEFAULT_FONT_FAMILY && font.font_style == DEFAULT_FONT_STYLE)
}
}

View file

@ -1,6 +1,6 @@
use crate::vector::PointId;
use bezier_rs::{ManipulatorGroup, Subpath};
use glam::DVec2;
use graphene_vector::PointId;
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
use rustybuzz::{GlyphBuffer, UnicodeBuffer};

View file

@ -0,0 +1,38 @@
[package]
name = "graphene-vector-nodes"
version = "0.1.0"
edition = "2024"
description = "graphene vector nodes"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = [
"dep:serde",
"bezier-rs/serde",
]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
bezier-rs = { workspace = true }
graphene-core = { workspace = true }
graphene-vector = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
kurbo = { workspace = true }
glam = { workspace = true }
specta = { workspace = true }
log = { workspace = true }
rustc-hash = { workspace = true }
petgraph = { workspace = true }
rand = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }
[dev-dependencies]
# Workspace dependencies
tokio = { workspace = true }

View file

@ -1,5 +1,5 @@
use super::poisson_disk::poisson_disk_sample;
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
use crate::misc::{PointSpacingType, dvec2_to_point};
use glam::DVec2;
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape};

View file

@ -1,5 +1,5 @@
use crate::vector::{PointDomain, PointId, SegmentDomain, VectorData, VectorDataIndex};
use glam::{DAffine2, DVec2};
use graphene_vector::{PointDomain, PointId, SegmentDomain, VectorData, VectorDataIndex};
use petgraph::prelude::UnGraphMap;
use rustc_hash::FxHashSet;

View file

@ -1,5 +1,5 @@
use crate::vector::PointId;
use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue};
use graphene_vector::PointId;
/// Value to control smoothness and mathematical accuracy to offset a cubic Bezier.
const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5;

View file

@ -141,7 +141,7 @@ mod tests {
use super::*;
#[test]
fn closed_spline() {
use crate::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_vector::{dvec2_to_point, point_to_dvec2};
use kurbo::{BezPath, ParamCurve, ParamCurveDeriv};
// These points are just chosen arbitrary

View file

@ -1,10 +1,9 @@
use super::misc::{ArcType, AsU64, GridType};
use super::{PointId, SegmentId, StrokeId};
use crate::Ctx;
use crate::registry::types::{Angle, PixelSize};
use crate::vector::{HandleId, VectorData, VectorDataTable};
use bezier_rs::Subpath;
use glam::DVec2;
use graphene_core::context::Ctx;
use graphene_core::registry::types::{Angle, PixelSize};
use graphene_vector::{HandleId, PointId, SegmentId, StrokeId, VectorData, VectorDataTable};
trait CornerRadius {
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable;

View file

@ -0,0 +1,5 @@
pub mod algorithms;
pub mod generator_nodes;
pub mod misc;
pub mod modification;
pub mod vector_nodes;

View file

@ -1,12 +1,20 @@
use super::*;
use crate::Ctx;
use crate::instances::Instance;
use crate::uuid::generate_uuid;
use crate::misc::point_to_dvec2;
use bezier_rs::BezierHandles;
use dyn_any::DynAny;
use glam::DVec2;
use graphene_core::context::Ctx;
use graphene_core::instances::Instance;
use graphene_core::uuid::generate_uuid;
use graphene_vector::{FillId, HandleId, HandleType, PointDomain, PointId, RegionDomain, RegionId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
use kurbo::{BezPath, PathEl, Point};
use log::warn;
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::hash::BuildHasher;
use std::hash::Hash;
/// Represents a procedural change to the [`PointDomain`] in [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
@ -434,11 +442,6 @@ async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modificat
// Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples?
// TODO: Eventually remove this document upgrade code
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::hash::Hash;
pub fn serialize_hashmap<K, V, S, H>(hashmap: &HashMap<K, V, H>, serializer: S) -> Result<S::Ok, S::Error>
where
K: Serialize + Eq + Hash,

View file

@ -1,421 +1,26 @@
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
use super::algorithms::offset_subpath::offset_subpath;
use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use super::misc::{CentroidType, point_to_dvec2};
use super::style::{Fill, Gradient, GradientStops, Stroke};
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataExt, VectorDataTable};
use crate::bounds::BoundingBox;
use crate::instances::{Instance, InstanceMut, Instances};
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
use crate::transform::{Footprint, ReferencePoint, Transform};
use crate::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType};
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use crate::vector::{FillId, PointDomain, RegionId};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
use super::misc::{CentroidType, MergeByDistanceAlgorithm, PointSpacingType, dvec2_to_point, point_to_dvec2};
use crate::modification::VectorDataExt;
use bezier_rs::{Join, ManipulatorGroup, Subpath};
use glam::{DAffine2, DVec2};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, ParamCurve, PathEl, PathSeg, Shape};
use graphene_core::color::Color;
use graphene_core::context::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
use graphene_core::gradient::{Gradient, GradientStops};
use graphene_core::instances::{Instance, InstanceMut, Instances};
use graphene_core::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
use graphene_core::transform::{Footprint, Transform};
use graphene_vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_vector::{FillId, PointDomain, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, ParamCurve, PathEl, PathSeg, Point, Shape};
use log::warn;
use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use std::f64::consts::PI;
use std::f64::consts::TAU;
use std::hash::{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>
where
Instances<I>: BoundingBox,
{
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: Modifier"), path(graphene_core::vector))]
async fn round_corners(
_: impl Ctx,
@ -1078,61 +683,6 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
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
where
GraphicElement: From<Instances<I>>,
{
// 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(GraphicElement::from(graphic_group_input));
flatten_group(&base_graphic_group, &mut output);
// Return the single-row VectorDataTable containing the flattened VectorData subpaths
output_table
}
/// Convert vector geometry into a polyline composed of evenly spaced points.
#[node_macro::node(category(""), path(graphene_core::vector))]
async fn sample_polyline(
@ -1860,11 +1410,6 @@ fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool {
source.instance_iter().any(|instance| instance.instance.check_point_inside_shape(instance.transform, point))
}
#[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
}
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn path_length(_: impl Ctx, source: VectorDataTable) -> f64 {
source
@ -1953,8 +1498,9 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl N
#[cfg(test)]
mod test {
use super::*;
use crate::Node;
use bezier_rs::Bezier;
use graphene_core::Node;
use graphene_core::transform::Footprint;
use kurbo::Rect;
use std::pin::Pin;
@ -1990,47 +1536,6 @@ mod test {
}
vector_data_table
}
#[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 bounding_box() {
let bounding_box = super::bounding_box((), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))).await;
@ -2057,27 +1562,6 @@ mod test {
}
}
#[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.),]
);
}
}
#[tokio::test]
async fn sample_polyline() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 30., 0., 0., 0., false, vec![100.]).await;

View file

@ -0,0 +1,33 @@
[package]
name = "graphene-vector"
version = "0.1.0"
edition = "2024"
description = "graphene vector data format"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "MIT OR Apache-2.0"
[features]
default = ["serde"]
serde = [
"dep:serde",
"bezier-rs/serde",
]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
bezier-rs = { workspace = true }
graphene-core = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
kurbo = { workspace = true }
glam = { workspace = true }
specta = { workspace = true }
log = { workspace = true }
tinyvec = { workspace = true }
rustc-hash = { workspace = true }
petgraph = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }

View file

@ -1,8 +1,8 @@
use crate::math::math_ext::QuadExt;
use crate::math::quad::Quad;
use crate::vector::PointId;
use crate::math_ext::QuadExt;
use crate::vector_data::PointId;
use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use graphene_core::math::quad::Quad;
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FreePoint {

View file

@ -0,0 +1,17 @@
pub mod click_target;
pub mod math_ext;
pub mod reference_point;
pub mod style;
mod vector_data;
pub use bezier_rs;
pub use vector_data::*;
pub fn point_to_dvec2(point: kurbo::Point) -> glam::DVec2 {
glam::DVec2 { x: point.x, y: point.y }
}
pub fn dvec2_to_point(value: glam::DVec2) -> kurbo::Point {
kurbo::Point { x: value.x, y: value.y }
}

View file

@ -1,6 +1,6 @@
use crate::math::quad::Quad;
use crate::math::rect::Rect;
use bezier_rs::Bezier;
use graphene_core::math::quad::Quad;
use graphene_core::math::rect::Rect;
pub trait QuadExt {
/// Get all the edges in the rect as linear bezier curves

View file

@ -1,5 +1,5 @@
use crate::math::bbox::AxisAlignedBbox;
use glam::DVec2;
use graphene_core::math::bbox::AxisAlignedBbox;
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ReferencePoint {

View file

@ -1,9 +1,9 @@
//! Contains stylistic options for SVG elements.
use crate::Color;
pub use crate::gradient::*;
use dyn_any::DynAny;
use glam::DAffine2;
use graphene_core::color::Color;
use graphene_core::gradient::{Gradient, GradientStops};
/// Describes the fill of a layer.
///
@ -528,8 +528,8 @@ impl PathStyle {
///
/// # Example
/// ```
/// # use graphene_core::vector::style::{Fill, PathStyle};
/// # use graphene_core::raster::color::Color;
/// # use graphene_vector::style::{Fill, PathStyle};
/// # use graphene_core::color::Color;
/// let fill = Fill::solid(Color::RED);
/// let style = PathStyle::new(None, fill.clone());
///
@ -543,8 +543,8 @@ impl PathStyle {
///
/// # Example
/// ```
/// # use graphene_core::vector::style::{Fill, Stroke, PathStyle};
/// # use graphene_core::raster::color::Color;
/// # use graphene_vector::style::{Fill, Stroke, PathStyle};
/// # use graphene_core::color::Color;
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
/// let style = PathStyle::new(Some(stroke.clone()), Fill::None);
///
@ -558,8 +558,8 @@ impl PathStyle {
///
/// # Example
/// ```
/// # use graphene_core::vector::style::{Fill, PathStyle};
/// # use graphene_core::raster::color::Color;
/// # use graphene_vector::style::{Fill, PathStyle};
/// # use graphene_core::color::Color;
/// let mut style = PathStyle::default();
///
/// assert_eq!(*style.fill(), Fill::None);
@ -583,8 +583,8 @@ impl PathStyle {
///
/// # Example
/// ```
/// # use graphene_core::vector::style::{Stroke, PathStyle};
/// # use graphene_core::raster::color::Color;
/// # use graphene_vector::style::{Stroke, PathStyle};
/// # use graphene_core::color::Color;
/// let mut style = PathStyle::default();
///
/// assert_eq!(style.stroke(), None);
@ -602,8 +602,8 @@ impl PathStyle {
///
/// # Example
/// ```
/// # use graphene_core::vector::style::{Fill, PathStyle};
/// # use graphene_core::raster::color::Color;
/// # use graphene_vector::style::{Fill, PathStyle};
/// # use graphene_core::color::Color;
/// let mut style = PathStyle::new(None, Fill::Solid(Color::RED));
///
/// assert_ne!(*style.fill(), Fill::None);
@ -620,8 +620,8 @@ impl PathStyle {
///
/// # Example
/// ```
/// # use graphene_core::vector::style::{Fill, Stroke, PathStyle};
/// # use graphene_core::raster::color::Color;
/// # use graphene_vector::style::{Fill, Stroke, PathStyle};
/// # use graphene_core::color::Color;
/// let mut style = PathStyle::new(Some(Stroke::new(Some(Color::GREEN), 42.)), Fill::None);
///
/// assert!(style.stroke().is_some());
@ -634,15 +634,3 @@ impl PathStyle {
self.stroke = None;
}
}
/// Represents different ways of rendering an object
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
pub enum ViewMode {
/// Render with normal coloration at the current viewport resolution
#[default]
Normal,
/// Render only the outlines of shapes at the current viewport resolution
Outline,
/// Render with normal coloration at the document resolution, showing the pixels when the current viewport resolution is higher
Pixels,
}

View file

@ -1,25 +1,31 @@
mod attributes;
mod indexed;
mod modification;
use super::misc::{dvec2_to_point, point_to_dvec2};
use super::style::{PathStyle, Stroke};
use crate::bounds::BoundingBox;
use crate::instances::Instances;
use crate::math::quad::Quad;
use crate::transform::Transform;
use crate::vector::click_target::{ClickTargetType, FreePoint};
use crate::{AlphaBlending, Color, GraphicGroupTable};
use crate::click_target::{ClickTargetType, FreePoint};
use crate::dvec2_to_point;
use crate::style::{PathStyle, Stroke};
pub use attributes::*;
use bezier_rs::{BezierHandles, ManipulatorGroup};
use core::borrow::Borrow;
use core::hash::Hash;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphene_core::blending::AlphaBlending;
use graphene_core::bounds::BoundingBox;
use graphene_core::color::Color;
use graphene_core::instances::Instances;
use graphene_core::math::quad::Quad;
use graphene_core::transform::Transform;
pub use indexed::VectorDataIndex;
use kurbo::{Affine, Rect, Shape};
pub use modification::*;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
pub trait AnyUpstreamGraphicGroup: Any + serde::Serialize + for<'a> serde::Deserialize<'a> {}
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct UpstreamGraphicGroup(Arc<dyn AnyUpstreamGraphicGroup>);
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, D::Error> {
@ -41,7 +47,7 @@ pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
pub region_domain: RegionDomain,
// Used to store the upstream graphic group during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved.
pub upstream_graphic_group: Option<GraphicGroupTable>,
pub upstream_graphic_group: Option<UpstreamGraphicGroup>,
}
#[derive(serde::Serialize, serde::Deserialize)]
@ -91,7 +97,7 @@ pub struct VectorData {
pub region_domain: RegionDomain,
// Used to store the upstream graphic group during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved.
pub upstream_graphic_group: Option<GraphicGroupTable>,
pub upstream_graphic_group: Option<UpstreamGraphicGroup>,
}
impl Default for VectorData {

View file

@ -1,8 +1,10 @@
use crate::vector::misc::dvec2_to_point;
use crate::vector::vector_data::{HandleId, VectorData};
use crate::dvec2_to_point;
use crate::vector_data::{HandleId, VectorData};
use bezier_rs::{BezierHandles, ManipulatorGroup};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphene_core::uuid::generate_uuid;
use log::warn;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::iter::zip;
@ -21,7 +23,7 @@ macro_rules! create_ids {
/// Generate a new random id
pub fn generate() -> Self {
Self(crate::uuid::generate_uuid())
Self(generate_uuid())
}
pub fn generate_from_hash(self, node_id: u64) -> Self {
@ -82,7 +84,7 @@ impl std::hash::BuildHasher for NoHashBuilder {
pub struct PointDomain {
id: Vec<PointId>,
#[serde(alias = "positions")]
pub(crate) position: Vec<DVec2>,
pub position: Vec<DVec2>,
}
impl Hash for PointDomain {
@ -166,7 +168,7 @@ impl PointDomain {
pos
}
pub(crate) fn resolve_id(&self, id: PointId) -> Option<usize> {
pub fn resolve_id(&self, id: PointId) -> Option<usize> {
self.id.iter().position(|&check_id| check_id == id)
}
@ -280,11 +282,11 @@ impl SegmentDomain {
self.ids().iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(SegmentId::ZERO)
}
pub(crate) fn start_point(&self) -> &[usize] {
pub fn start_point(&self) -> &[usize] {
&self.start_point
}
pub(crate) fn end_point(&self) -> &[usize] {
pub fn end_point(&self) -> &[usize] {
&self.end_point
}
@ -304,7 +306,7 @@ impl SegmentDomain {
&self.stroke
}
pub(crate) fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) {
pub fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) {
debug_assert!(!self.id.contains(&id), "Tried to push an existing point to a point domain");
self.id.push(id);
@ -314,20 +316,20 @@ impl SegmentDomain {
self.stroke.push(stroke);
}
pub(crate) fn start_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut usize)> {
pub fn start_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut usize)> {
self.id.iter().copied().zip(self.start_point.iter_mut())
}
pub(crate) fn end_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut usize)> {
pub fn end_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut usize)> {
self.id.iter().copied().zip(self.end_point.iter_mut())
}
pub(crate) fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut BezierHandles, usize, usize)> {
pub fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut BezierHandles, usize, usize)> {
let nested = self.id.iter().zip(&mut self.handles).zip(&self.start_point).zip(&self.end_point);
nested.map(|(((&a, b), &c), &d)| (a, b, c, d))
}
pub(crate) fn handles_and_points_mut(&mut self) -> impl Iterator<Item = (&mut BezierHandles, &mut usize, &mut usize)> {
pub fn handles_and_points_mut(&mut self) -> impl Iterator<Item = (&mut BezierHandles, &mut usize, &mut usize)> {
let nested = self.handles.iter_mut().zip(&mut self.start_point).zip(&mut self.end_point);
nested.map(|((a, b), c)| (a, b, c))
}
@ -336,26 +338,26 @@ impl SegmentDomain {
self.id.iter().copied().zip(self.stroke.iter_mut())
}
pub(crate) fn segment_start_from_id(&self, segment: SegmentId) -> Option<usize> {
pub fn segment_start_from_id(&self, segment: SegmentId) -> Option<usize> {
self.id_to_index(segment).and_then(|index| self.start_point.get(index)).copied()
}
pub(crate) fn segment_end_from_id(&self, segment: SegmentId) -> Option<usize> {
pub fn segment_end_from_id(&self, segment: SegmentId) -> Option<usize> {
self.id_to_index(segment).and_then(|index| self.end_point.get(index)).copied()
}
/// Returns an array for the start and end points of a segment.
pub(crate) fn points_from_id(&self, segment: SegmentId) -> Option<[usize; 2]> {
pub fn points_from_id(&self, segment: SegmentId) -> Option<[usize; 2]> {
self.segment_start_from_id(segment).and_then(|start| self.segment_end_from_id(segment).map(|end| [start, end]))
}
/// Attempts to find another point in the segment that is not the one passed in.
pub(crate) fn other_point(&self, segment: SegmentId, current: usize) -> Option<usize> {
pub fn other_point(&self, segment: SegmentId, current: usize) -> Option<usize> {
self.points_from_id(segment).and_then(|points| points.into_iter().find(|&point| point != current))
}
/// Gets all points connected to the current one but not including the current one.
pub(crate) fn connected_points(&self, current: usize) -> impl Iterator<Item = usize> + '_ {
pub fn connected_points(&self, current: usize) -> impl Iterator<Item = usize> + '_ {
self.start_point.iter().zip(&self.end_point).filter_map(move |(&a, &b)| match (a == current, b == current) {
(true, false) => Some(b),
(false, true) => Some(a),
@ -400,22 +402,22 @@ impl SegmentDomain {
}
/// Enumerate all segments that start at the point.
pub(crate) fn start_connected(&self, point: usize) -> impl Iterator<Item = SegmentId> + '_ {
pub fn start_connected(&self, point: usize) -> impl Iterator<Item = SegmentId> + '_ {
self.start_point.iter().zip(&self.id).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
}
/// Enumerate all segments that end at the point.
pub(crate) fn end_connected(&self, point: usize) -> impl Iterator<Item = SegmentId> + '_ {
pub fn end_connected(&self, point: usize) -> impl Iterator<Item = SegmentId> + '_ {
self.end_point.iter().zip(&self.id).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
}
/// Enumerate all segments that start or end at a point, converting them to [`HandleId`s]. Note that the handles may not exist e.g. for a linear segment.
pub(crate) fn all_connected(&self, point: usize) -> impl Iterator<Item = HandleId> + '_ {
pub fn all_connected(&self, point: usize) -> impl Iterator<Item = HandleId> + '_ {
self.start_connected(point).map(HandleId::primary).chain(self.end_connected(point).map(HandleId::end))
}
/// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice.
pub(crate) fn connected_count(&self, point: usize) -> usize {
pub fn connected_count(&self, point: usize) -> usize {
self.all_connected(point).count()
}
@ -433,7 +435,7 @@ impl SegmentDomain {
/// Iterates over segments in the domain, mutably.
///
/// Tuple is: (id, start point, end point, handles)
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = (&mut SegmentId, &mut usize, &mut usize, &mut BezierHandles)> + '_ {
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut SegmentId, &mut usize, &mut usize, &mut BezierHandles)> + '_ {
let ids = self.id.iter_mut();
let start_point = self.start_point.iter_mut();
let end_point = self.end_point.iter_mut();

View file

@ -16,10 +16,10 @@ pub struct VectorDataIndex {
/// Points and segments form a graph. Store it here in a form amenable to graph algorithms.
///
/// Currently, segment data is not stored as it is not used, but it could easily be added.
pub(crate) point_graph: UnGraph<Point, ()>,
pub(crate) segment_to_edge: FxHashMap<SegmentId, EdgeIndex>,
pub point_graph: UnGraph<Point, ()>,
pub segment_to_edge: FxHashMap<SegmentId, EdgeIndex>,
/// Get the offset from the point ID.
pub(crate) point_to_offset: FxHashMap<PointId, usize>,
pub point_to_offset: FxHashMap<PointId, usize>,
// TODO: faces
}

View file

@ -2,7 +2,7 @@ mod benchmark_util;
use benchmark_util::{bench_for_each_demo, setup_network};
use criterion::{Criterion, criterion_group, criterion_main};
use graphene_std::Context;
use graphene_std::context::Context;
fn subsequent_evaluations(c: &mut Criterion) {
let mut group = c.benchmark_group("Subsequent Evaluations");

View file

@ -2,7 +2,7 @@ mod benchmark_util;
use benchmark_util::{bench_for_each_demo, setup_network};
use criterion::{Criterion, criterion_group, criterion_main};
use graphene_std::Context;
use graphene_std::context::Context;
fn run_once(c: &mut Criterion) {
let mut group = c.benchmark_group("Run Once");

View file

@ -2,22 +2,21 @@ use dyn_any::StaticType;
use glam::{DVec2, IVec2, UVec2};
use graph_craft::document::value::RenderOutput;
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
use graphene_core::raster::color::Color;
use graphene_core::raster::*;
use graphene_core::raster_types::{CPU, GPU, RasterDataTable};
use graphene_core::vector::VectorDataTable;
use graphene_core::{Artboard, GraphicGroupTable, concrete, generic};
use graphene_core::{Cow, ProtoNodeIdentifier, Type};
use graphene_core::{NodeIO, NodeIOTypes};
use graphene_core::{fn_type_fut, future};
use graphene_std::Context;
use graphene_std::GraphicElement;
#[cfg(feature = "gpu")]
use graphene_std::any::DowncastBothNode;
use graphene_std::any::{ComposeTypeErased, DynAnyNode, IntoTypeErasedNode};
use graphene_std::application_io::{ImageTexture, SurfaceFrame};
use graphene_std::color::Color;
use graphene_std::context::Context;
use graphene_std::element::{Artboard, GraphicElement, GraphicGroupTable};
use graphene_std::raster::*;
use graphene_std::vector::VectorDataTable;
#[cfg(feature = "gpu")]
use graphene_std::wasm_application_io::{WasmEditorApi, WasmSurfaceHandle};
use graphene_std::{Cow, ProtoNodeIdentifier, Type};
use graphene_std::{NodeIO, NodeIOTypes};
use graphene_std::{concrete, generic};
use graphene_std::{fn_type_fut, future};
use node_registry_macros::{async_node, convert_node, into_node};
use once_cell::sync::Lazy;
use std::collections::HashMap;
@ -73,7 +72,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<graphene_core::uuid::NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Color]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_core::vector::VectorModification>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::std_nodes::misc::CentroidType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => VectorDataTable]),

View file

@ -4,7 +4,7 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
use graph_craft::generic;
use graph_craft::wasm_application_io::WasmEditorApi;
use graphene_std::Context;
use graphene_std::context::Context;
use graphene_std::uuid::NodeId;
use std::sync::Arc;

View file

@ -517,7 +517,7 @@ fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], st
}
let mut constructors = Vec::new();
let unit = parse_quote!(gcore::Context);
let unit = parse_quote!(gcore::context::Context);
let parameter_types: Vec<_> = parsed
.fields
.iter()

View file

@ -643,7 +643,7 @@ impl ParsedNodeFn {
}));
self.input.ty = parse_quote!(#ident);
if self.input.implementations.is_empty() {
self.input.implementations.push(parse_quote!(gcore::Context));
self.input.implementations.push(parse_quote!(gcore::context::Context));
}
}
if self.input.pat_ident.ident == "_" {

View file

@ -5,7 +5,8 @@ pub use context::Context;
use dyn_any::StaticType;
use glam::UVec2;
use graphene_application_io::{ApplicationIo, EditorApi, SurfaceHandle};
use graphene_core::{Color, Ctx};
use graphene_core::color::Color;
use graphene_core::context::Ctx;
pub use graphene_svg_renderer::RenderContext;
use std::sync::Arc;
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};