This commit is contained in:
mtvare6
2025-07-07 07:01:15 +05:30
101 changed files with 3737 additions and 2830 deletions

View File

@@ -0,0 +1,28 @@
[package]
name = "graphene-brush"
version = "0.1.0"
edition = "2024"
description = "graphene brush"
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 }
graphene-core = { workspace = true }
graphene-raster-nodes = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }
[dev-dependencies]
# Workspace dependencies
tokio = { workspace = true }

View File

@@ -1,19 +1,21 @@
use crate::raster::{empty_image, extend_image_to_bounds};
use crate::brush_cache::BrushCache;
use crate::brush_stroke::{BrushStroke, BrushStyle};
use glam::{DAffine2, DVec2};
use graph_craft::generic::FnNode;
use graph_craft::proto::FutureWrapperNode;
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::adjustments::blend_colors;
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::BitmapMut;
use graphene_core::raster::image::Image;
use graphene_core::raster::{Alpha, BitmapMut, BlendMode, Color, Pixel, Sample};
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::vector::brush_stroke::{BrushStroke, BrushStyle};
use graphene_core::{Ctx, GraphicElement, Node};
use graphene_core::{Ctx, Node};
use graphene_raster_nodes::adjustments::blend_colors;
use graphene_raster_nodes::std_nodes::{empty_image, extend_image_to_bounds};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BrushStampGenerator<P: Pixel + Alpha> {
@@ -50,13 +52,13 @@ impl<P: Pixel + Alpha> Sample for BrushStampGenerator<P> {
return None;
};
use graphene_core::raster::Channel;
use graphene_core::color::Channel;
Some(self.color.multiplied_alpha(P::AlphaChannel::from_linear(result)))
}
}
#[node_macro::node(skip_impl)]
fn brush_stamp_generator(diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> {
fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> {
// Diameter
let radius = diameter / 2.;
@@ -78,7 +80,6 @@ fn brush_stamp_generator(diameter: f64, color: Color, hardness: f64, flow: f64)
fn blit<BlendFn>(mut target: RasterDataTable<CPU>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> RasterDataTable<CPU>
where
BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>,
GraphicElement: From<Raster<CPU>>,
{
if positions.is_empty() {
return target;
@@ -239,7 +240,6 @@ async fn brush(_: impl Ctx, mut image_frame_table: RasterDataTable<CPU>, strokes
let target = core::mem::take(&mut brush_plan.first_stroke_texture);
extend_image_to_bounds((), target.to_table(), stroke_to_layer)
} else {
use crate::raster::empty_image;
empty_image((), stroke_to_layer, Color::TRANSPARENT)
// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
};
@@ -393,7 +393,7 @@ mod test {
(),
RasterDataTable::<CPU>::new(Raster::new_cpu(Image::<Color>::default())),
vec![BrushStroke {
trace: vec![crate::vector::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
style: BrushStyle {
color: Color::BLACK,
diameter: 20.,

View File

@@ -1,9 +1,9 @@
use crate::instances::Instance;
use crate::raster_types::CPU;
use crate::raster_types::Raster;
use crate::vector::brush_stroke::BrushStroke;
use crate::vector::brush_stroke::BrushStyle;
use crate::brush_stroke::BrushStroke;
use crate::brush_stroke::BrushStyle;
use dyn_any::DynAny;
use graphene_core::instances::Instance;
use graphene_core::raster_types::CPU;
use graphene_core::raster_types::Raster;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
@@ -15,11 +15,11 @@ struct BrushCacheImpl {
prev_input: Vec<BrushStroke>,
// The strokes that have been fully processed and blended into the background.
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
background: Instance<Raster<CPU>>,
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
blended_image: Instance<Raster<CPU>>,
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
#[serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame_instance")]
last_stroke_texture: Instance<Raster<CPU>>,
// A cache for brush textures.

View File

@@ -1,8 +1,8 @@
use crate::Color;
use crate::math::bbox::AxisAlignedBbox;
use crate::raster::BlendMode;
use dyn_any::DynAny;
use glam::DVec2;
use graphene_core::blending::BlendMode;
use graphene_core::color::Color;
use graphene_core::math::bbox::AxisAlignedBbox;
use std::hash::{Hash, Hasher};
/// The style of a brush.

View File

@@ -0,0 +1,3 @@
pub mod brush;
pub mod brush_cache;
pub mod brush_stroke;

View File

@@ -29,10 +29,11 @@ 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 }
parley = { workspace = true }
skrifa = { workspace = true }
kurbo = { workspace = true }
log = { workspace = true }
base64 = { workspace = true }

View File

@@ -1,5 +1,14 @@
use crate::raster_types::{CPU, RasterDataTable};
use crate::vector::VectorDataTable;
use crate::{Color, Ctx};
use glam::{DAffine2, DVec2};
#[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 {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("{:#?}", value);
value
}
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
#[node_macro::node(category("Debug"))]

View File

@@ -2,7 +2,9 @@ use crate::Ctx;
use dyn_any::DynAny;
use glam::{DVec2, IVec2, UVec2};
/// Obtain the X or Y component of a coordinate.
/// Obtains the X or Y component of a coordinate point.
///
/// The inverse of this node is "Coordinate Value", which can have either or both its X and Y exposed as graph inputs.
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
match axis {

View File

@@ -558,3 +558,53 @@ impl From<GraphicGroupTable> for GraphicElement {
pub trait ToGraphicElement {
fn to_graphic_element(&self) -> GraphicElement;
}
/// Returns the value at the specified index in the collection.
/// If that index has no value, the type's default value is returned.
#[node_macro::node(category("General"))]
fn index<T: AtIndex + Clone + Default>(
_: impl Ctx,
/// The collection of data, such as a list or table.
#[implementations(
Vec<Color>,
Vec<Option<Color>>,
Vec<f64>, Vec<u64>,
Vec<DVec2>,
VectorDataTable,
RasterDataTable<CPU>,
GraphicGroupTable,
)]
collection: T,
/// The index of the item to retrieve, starting from 0 for the first item.
index: u32,
) -> T::Output
where
T::Output: Clone + Default,
{
collection.at_index(index as usize).unwrap_or_default()
}
pub trait AtIndex {
type Output;
fn at_index(&self, index: usize) -> Option<Self::Output>;
}
impl<T: Clone> AtIndex for Vec<T> {
type Output = T;
fn at_index(&self, index: usize) -> Option<Self::Output> {
self.get(index).cloned()
}
}
impl<T: Clone> AtIndex for Instances<T> {
type Output = Instances<T>;
fn at_index(&self, index: usize) -> Option<Self::Output> {
let mut result_table = Self::default();
if let Some(row) = self.instance_ref_iter().nth(index) {
result_table.push(row.to_instance_cloned());
Some(result_table)
} else {
None
}
}
}

View File

@@ -1,26 +1,26 @@
use crate::ArtboardGroupTable;
use crate::Color;
use crate::GraphicElement;
use crate::GraphicGroupTable;
use crate::gradient::GradientStops;
use crate::graphene_core::registry::types::TextArea;
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::vector::VectorDataTable;
use crate::{Color, Context, Ctx};
use crate::{Context, Ctx};
use glam::{DAffine2, DVec2};
#[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 {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("{:#?}", value);
value
}
#[node_macro::node(category("Text"))]
fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
format!("{:?}", value)
}
#[node_macro::node(category("Text"))]
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, #[implementations(String)] second: String) -> String {
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String {
first.clone() + &second
}
#[node_macro::node(category("Text"))]
fn string_replace(_: impl Ctx, #[implementations(String)] string: String, from: String, to: String) -> String {
fn string_replace(_: impl Ctx, #[implementations(String)] string: String, from: TextArea, to: TextArea) -> String {
string.replace(&from, &to)
}
@@ -45,24 +45,42 @@ async fn switch<T, C: Send + 'n + Clone>(
#[implementations(
Context -> String,
Context -> bool,
Context -> f32,
Context -> f64,
Context -> u32,
Context -> u64,
Context -> DVec2,
Context -> VectorDataTable,
Context -> DAffine2,
Context -> ArtboardGroupTable,
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> GraphicElement,
Context -> Color,
Context -> Option<Color>,
Context -> GradientStops,
)]
if_true: impl Node<C, Output = T>,
#[expose]
#[implementations(
Context -> String,
Context -> bool,
Context -> f32,
Context -> f64,
Context -> u32,
Context -> u64,
Context -> DVec2,
Context -> VectorDataTable,
Context -> DAffine2,
Context -> ArtboardGroupTable,
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> GraphicElement,
Context -> Color,
Context -> Option<Color>,
Context -> GradientStops,
)]
if_false: impl Node<C, Output = T>,
) -> T {

View File

@@ -73,9 +73,17 @@ pub trait Convert<T>: Sized {
fn convert(self) -> T;
}
impl<T: ToString> Convert<String> for T {
/// Converts this type into a `String` using its `ToString` implementation.
#[inline]
fn convert(self) -> String {
self.to_string()
}
}
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
macro_rules! impl_convert {
($from:ty,$to:ty) => {
($from:ty, $to:ty) => {
impl Convert<$to> for $from {
fn convert(self) -> $to {
self as $to

View File

@@ -12,13 +12,9 @@ pub mod color {
pub use super::*;
}
pub mod adjustments;
pub mod brush_cache;
pub mod curve;
pub mod image;
pub use self::image::Image;
pub use adjustments::*;
pub trait Bitmap {
type Pixel: Pixel;

View File

@@ -97,11 +97,20 @@ impl Raster<GPU> {
let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() };
gpu.clone()
}
}
impl Raster<GPU> {
#[cfg(feature = "wgpu")]
pub fn is_empty(&self) -> bool {
let data = self.data();
data.width() == 0 || data.height() == 0
}
#[cfg(not(feature = "wgpu"))]
pub fn is_empty(&self) -> bool {
true
}
}
#[cfg(feature = "wgpu")]
impl Deref for Raster<GPU> {
type Target = wgpu::Texture;
@@ -110,6 +119,7 @@ impl Deref for Raster<GPU> {
self.data()
}
}
pub type RasterDataTable<Storage> = Instances<Raster<Storage>>;
// TODO: Make this not dupliated

View File

@@ -30,6 +30,8 @@ pub mod types {
pub type Resolution = glam::UVec2;
/// DVec2 with px unit
pub type PixelSize = glam::DVec2;
/// String with one or more than one line
pub type TextArea = String;
}
// Translation struct between macro and definition

View File

@@ -22,11 +22,12 @@ impl Default for Font {
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)]
pub struct FontCache {
/// Actual font file data used for rendering a font with ttf_parser and rustybuzz
/// Actual font file data used for rendering a font
font_file_data: HashMap<Font, Vec<u8>>,
/// Web font preview URLs used for showing fonts when live editing
preview_urls: HashMap<Font, String>,
}
impl FontCache {
/// Returns the font family name if the font is cached, otherwise returns the fallback font family name if that is cached
pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> {
@@ -40,7 +41,7 @@ impl FontCache {
}
/// Try to get the bytes for a font
pub fn get<'a>(&'a self, font: &Font) -> Option<&'a Vec<u8>> {
pub fn get(&self, font: &Font) -> Option<&Vec<u8>> {
self.resolve_font(font).and_then(|font| self.font_file_data.get(font))
}

View File

@@ -1,29 +1,67 @@
use crate::vector::PointId;
use bezier_rs::{ManipulatorGroup, Subpath};
use glam::DVec2;
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
use core::cell::RefCell;
use glam::{DAffine2, DVec2};
use parley::fontique::Blob;
use parley::{Alignment, AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
use skrifa::GlyphId;
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::{DrawSettings, OutlinePen};
use skrifa::raw::FontRef as ReadFontsRef;
use skrifa::{MetadataProvider, OutlineGlyph};
use std::sync::Arc;
struct Builder {
// Thread-local storage avoids expensive re-initialization of font and layout contexts
// across multiple text rendering operations within the same thread
thread_local! {
static FONT_CONTEXT: RefCell<FontContext> = RefCell::new(FontContext::new());
static LAYOUT_CONTEXT: RefCell<LayoutContext<()>> = RefCell::new(LayoutContext::new());
}
struct PathBuilder {
current_subpath: Subpath<PointId>,
glyph_subpaths: Vec<Subpath<PointId>>,
other_subpaths: Vec<Subpath<PointId>>,
text_cursor: DVec2,
offset: DVec2,
ascender: f64,
origin: DVec2,
scale: f64,
id: PointId,
}
impl Builder {
impl PathBuilder {
fn point(&self, x: f32, y: f32) -> DVec2 {
self.text_cursor + self.offset + DVec2::new(x as f64, self.ascender - y as f64) * self.scale
// Y-axis inversion converts from font coordinate system (Y-up) to graphics coordinate system (Y-down)
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
}
fn set_origin(&mut self, x: f64, y: f64) {
self.origin = DVec2::new(x, y);
}
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], style_skew: Option<DAffine2>, skew: DAffine2) {
let location_ref = LocationRef::new(normalized_coords);
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
glyph.draw(settings, self).unwrap();
// Apply transforms in correct order: style-based skew first, then user-requested skew
// This ensures font synthesis (italic) is applied before user transformations
for glyph_subpath in &mut self.glyph_subpaths {
if let Some(style_skew) = style_skew {
glyph_subpath.apply_transform(style_skew);
}
glyph_subpath.apply_transform(skew);
}
if !self.glyph_subpaths.is_empty() {
self.other_subpaths.extend(core::mem::take(&mut self.glyph_subpaths));
}
}
}
impl OutlineBuilder for Builder {
impl OutlinePen for PathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
if !self.current_subpath.is_empty() {
self.other_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
}
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
}
@@ -47,36 +85,10 @@ impl OutlineBuilder for Builder {
fn close(&mut self) {
self.current_subpath.set_closed(true);
self.other_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
}
}
fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64, line_height_ratio: f64) -> (f64, f64, UnicodeBuffer) {
let scale = (buzz_face.units_per_em() as f64).recip() * font_size;
let line_height = font_size * line_height_ratio;
let buffer = UnicodeBuffer::new();
(scale, line_height, buffer)
}
fn push_str(buffer: &mut UnicodeBuffer, word: &str) {
buffer.push_str(word);
}
fn wrap_word(max_width: Option<f64>, glyph_buffer: &GlyphBuffer, font_size: f64, character_spacing: f64, x_pos: f64, space_glyph: Option<GlyphId>) -> bool {
if let Some(max_width) = max_width {
// We don't word wrap spaces (to match the browser)
let all_glyphs = glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos());
let non_space_glyphs = all_glyphs.take_while(|(_, info)| space_glyph != Some(GlyphId(info.glyph_id as u16)));
let word_length: f64 = non_space_glyphs.map(|(pos, _)| pos.x_advance as f64 * character_spacing).sum();
let scaled_word_length = word_length * font_size;
if scaled_word_length + x_pos > max_width {
return true;
}
}
false
}
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct TypesettingConfig {
pub font_size: f64,
@@ -84,6 +96,7 @@ pub struct TypesettingConfig {
pub character_spacing: f64,
pub max_width: Option<f64>,
pub max_height: Option<f64>,
pub tilt: f64,
}
impl Default for TypesettingConfig {
@@ -91,163 +104,130 @@ impl Default for TypesettingConfig {
Self {
font_size: 24.,
line_height_ratio: 1.2,
character_spacing: 1.,
character_spacing: 0.,
max_width: None,
max_height: None,
tilt: 0.,
}
}
}
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> {
let Some(buzz_face) = buzz_face else { return vec![] };
let space_glyph = buzz_face.glyph_index(' ');
fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64) {
let mut run_x = glyph_run.offset();
let run_y = glyph_run.baseline();
let (scale, line_height, mut buffer) = font_properties(&buzz_face, typesetting.font_size, typesetting.line_height_ratio);
let run = glyph_run.run();
let mut builder = Builder {
// User-requested tilt applied around baseline to avoid vertical displacement
// Translation ensures rotation point is at the baseline, not origin
let skew = DAffine2::from_translation(DVec2::new(0., run_y as f64))
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
* DAffine2::from_translation(DVec2::new(0., -run_y as f64));
let synthesis = run.synthesis();
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
// This preserves the distinction between font styling and user transformations
let style_skew = synthesis.skew().map(|angle| {
DAffine2::from_translation(DVec2::new(0., run_y as f64))
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
});
let font = run.font();
let font_size = run.font_size();
let normalized_coords = run.normalized_coords().iter().map(|coord| NormalizedCoord::from_bits(*coord)).collect::<Vec<_>>();
// TODO: This can be cached for better performance
let font_collection_ref = font.data.as_ref();
let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap();
let outlines = font_ref.outline_glyphs();
for glyph in glyph_run.glyphs() {
let glyph_x = run_x + glyph.x;
let glyph_y = run_y - glyph.y;
run_x += glyph.advance;
let glyph_id = GlyphId::from(glyph.id);
if let Some(glyph_outline) = outlines.get(glyph_id) {
path_builder.set_origin(glyph_x as f64, glyph_y as f64);
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, style_skew, skew);
}
}
}
fn layout_text(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Option<Layout<()>> {
let font_cx = FONT_CONTEXT.with(Clone::clone);
let mut font_cx = font_cx.borrow_mut();
let layout_cx = LAYOUT_CONTEXT.with(Clone::clone);
let mut layout_cx = layout_cx.borrow_mut();
let font_family = font_data.and_then(|font_data| {
font_cx
.collection
.register_fonts(font_data, None)
.first()
.and_then(|(family_id, _)| font_cx.collection.family_name(*family_id).map(String::from))
})?;
const DISPLAY_SCALE: f32 = 1.;
let mut builder = layout_cx.ranged_builder(&mut font_cx, str, DISPLAY_SCALE, true);
builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32));
builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32));
builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(std::borrow::Cow::Owned(font_family)))));
builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32));
let mut layout: Layout<()> = builder.build(str);
layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
layout.align(typesetting.max_width.map(|max_w| max_w as f32), Alignment::Left, AlignmentOptions::default());
Some(layout)
}
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> {
let Some(layout) = layout_text(str, font_data, typesetting) else { return Vec::new() };
let mut path_builder = PathBuilder {
current_subpath: Subpath::new(Vec::new(), false),
glyph_subpaths: Vec::new(),
other_subpaths: Vec::new(),
text_cursor: DVec2::ZERO,
offset: DVec2::ZERO,
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * typesetting.font_size / scale,
scale,
origin: DVec2::ZERO,
scale: layout.scale() as f64,
id: PointId::ZERO,
};
for line in str.split('\n') {
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
push_str(&mut buffer, word);
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
// Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, builder.text_cursor.x, space_glyph) {
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
for line in layout.lines() {
for item in line.items() {
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt);
}
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && builder.text_cursor.x + (glyph_position.x_advance as f64 * builder.scale * typesetting.character_spacing) >= max_width {
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
}
}
// Clip when the height is exceeded
if typesetting.max_height.is_some_and(|max_height| builder.text_cursor.y > max_height - line_height) {
return builder.other_subpaths;
}
builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
buzz_face.outline_glyph(glyph_id, &mut builder);
if !builder.current_subpath.is_empty() {
builder.other_subpaths.push(std::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false)));
}
builder.text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * builder.scale;
}
buffer = glyph_buffer.clear();
}
builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height);
}
builder.other_subpaths
path_builder.other_subpaths
}
pub fn bounding_box(str: &str, buzz_face: Option<&rustybuzz::Face>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
// Show blank layer if font has not loaded
let Some(buzz_face) = buzz_face else { return DVec2::ZERO };
let space_glyph = buzz_face.glyph_index(' ');
let (scale, line_height, mut buffer) = font_properties(buzz_face, typesetting.font_size, typesetting.line_height_ratio);
let [mut text_cursor, mut bounds] = [DVec2::ZERO; 2];
pub fn bounding_box(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
if !for_clipping_test {
if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) {
return DVec2::new(max_width, max_height);
}
}
for line in str.split('\n') {
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
push_str(&mut buffer, word);
let Some(layout) = layout_text(str, font_data, typesetting) else { return DVec2::ZERO };
let glyph_buffer = rustybuzz::shape(buzz_face, &[], buffer);
// Don't wrap the first word
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, text_cursor.x, space_glyph) {
text_cursor = DVec2::new(0., text_cursor.y + line_height);
}
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
let glyph_id = GlyphId(glyph_info.glyph_id as u16);
if let Some(max_width) = typesetting.max_width {
if space_glyph != Some(glyph_id) && text_cursor.x + (glyph_position.x_advance as f64 * scale * typesetting.character_spacing) >= max_width {
text_cursor = DVec2::new(0., text_cursor.y + line_height);
}
}
text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * scale;
bounds = bounds.max(text_cursor + DVec2::new(0., line_height));
}
buffer = glyph_buffer.clear();
}
text_cursor = DVec2::new(0., text_cursor.y + line_height);
bounds = bounds.max(text_cursor);
}
if !for_clipping_test {
if let Some(max_width) = typesetting.max_width {
bounds.x = max_width;
}
if let Some(max_height) = typesetting.max_height {
bounds.y = max_height;
}
}
bounds
DVec2::new(layout.full_width() as f64, layout.height() as f64)
}
pub fn load_face(data: &[u8]) -> rustybuzz::Face<'_> {
rustybuzz::Face::from_slice(data, 0).expect("Loading font failed")
pub fn load_font(data: &[u8]) -> Blob<u8> {
Blob::new(Arc::new(data.to_vec()))
}
pub fn lines_clipping(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> bool {
pub fn lines_clipping(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> bool {
let Some(max_height) = typesetting.max_height else { return false };
let bounds = bounding_box(str, buzz_face.as_ref(), typesetting, true);
let bounds = bounding_box(str, font_data, typesetting, true);
max_height < bounds.y
}
struct SplitWordsIncludingSpaces<'a> {
text: &'a str,
start_byte: usize,
}
impl<'a> SplitWordsIncludingSpaces<'a> {
pub fn new(text: &'a str) -> Self {
Self { text, start_byte: 0 }
}
}
impl<'a> Iterator for SplitWordsIncludingSpaces<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let mut eaten_chars = self.text[self.start_byte..].char_indices().skip_while(|(_, c)| *c != ' ').skip_while(|(_, c)| *c == ' ');
let start_byte = self.start_byte;
self.start_byte = eaten_chars.next().map_or(self.text.len(), |(offset, _)| self.start_byte + offset);
(self.start_byte > start_byte).then(|| self.text.get(start_byte..self.start_byte)).flatten()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_words_including_spaces() {
let mut split_words = SplitWordsIncludingSpaces::new("hello world .");
assert_eq!(split_words.next(), Some("hello "));
assert_eq!(split_words.next(), Some("world "));
assert_eq!(split_words.next(), Some("."));
assert_eq!(split_words.next(), None);
}
}

View File

@@ -19,10 +19,10 @@ async fn transform<T: 'n + 'static>(
translate: DVec2,
rotate: f64,
scale: DVec2,
shear: DVec2,
skew: DVec2,
_pivot: DVec2,
) -> Instances<T> {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied();
@@ -70,6 +70,7 @@ async fn boundless_footprint<T: 'n + 'static>(
transform_target.eval(ctx.into_context()).await
}
#[node_macro::node(category("Debug"))]
async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,

View File

@@ -86,6 +86,7 @@ async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
Default::default()
}
// TODO: Make this return a u32 instead of an f64, but we ned to improve math-related compatibility with integer types first.
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn instance_index(ctx: impl Ctx + ExtractIndex) -> f64 {
match ctx.try_index() {

View File

@@ -67,6 +67,10 @@ impl ClickTarget {
self.bounding_box
}
pub fn bounding_box_center(&self) -> Option<DVec2> {
self.bounding_box.map(|bbox| bbox[0] + (bbox[1] - bbox[0]) / 2.)
}
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
}

View File

@@ -37,7 +37,13 @@ impl CornerRadius for [f64; 4] {
}
#[node_macro::node(category("Vector: Shape"))]
fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataTable {
fn circle(
_: impl Ctx,
_primary: (),
#[unit(" px")]
#[default(50.)]
radius: f64,
) -> VectorDataTable {
let radius = radius.abs();
VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
}
@@ -46,7 +52,9 @@ fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataT
fn arc(
_: impl Ctx,
_primary: (),
#[default(50.)] radius: f64,
#[unit(" px")]
#[default(50.)]
radius: f64,
start_angle: Angle,
#[default(270.)]
#[range((0., 360.))]
@@ -66,7 +74,16 @@ fn arc(
}
#[node_macro::node(category("Vector: Shape"))]
fn ellipse(_: impl Ctx, _primary: (), #[default(50)] radius_x: f64, #[default(25)] radius_y: f64) -> VectorDataTable {
fn ellipse(
_: impl Ctx,
_primary: (),
#[unit(" px")]
#[default(50)]
radius_x: f64,
#[unit(" px")]
#[default(25)]
radius_y: f64,
) -> VectorDataTable {
let radius = DVec2::new(radius_x, radius_y);
let corner1 = -radius;
let corner2 = radius;
@@ -87,8 +104,12 @@ fn ellipse(_: impl Ctx, _primary: (), #[default(50)] radius_x: f64, #[default(25
fn rectangle<T: CornerRadius>(
_: impl Ctx,
_primary: (),
#[default(100)] width: f64,
#[default(100)] height: f64,
#[unit(" px")]
#[default(100)]
width: f64,
#[unit(" px")]
#[default(100)]
height: f64,
_individual_corner_radii: bool, // TODO: Move this to the bottom once we have a migration capability
#[implementations(f64, [f64; 4])] corner_radius: T,
#[default(true)] clamped: bool,
@@ -104,7 +125,9 @@ fn regular_polygon<T: AsU64>(
#[hard_min(3.)]
#[implementations(u32, u64, f64)]
sides: T,
#[default(50)] radius: f64,
#[unit(" px")]
#[default(50)]
radius: f64,
) -> VectorDataTable {
let points = sides.as_u64();
let radius: f64 = radius * 2.;
@@ -119,8 +142,12 @@ fn star<T: AsU64>(
#[hard_min(2.)]
#[implementations(u32, u64, f64)]
sides: T,
#[default(50)] radius_1: f64,
#[default(25)] radius_2: f64,
#[unit(" px")]
#[default(50)]
radius_1: f64,
#[unit(" px")]
#[default(25)]
radius_2: f64,
) -> VectorDataTable {
let points = sides.as_u64();
let diameter: f64 = radius_1 * 2.;
@@ -130,7 +157,7 @@ fn star<T: AsU64>(
}
#[node_macro::node(category("Vector: Shape"))]
fn line(_: impl Ctx, _primary: (), #[default((0., -50.))] start: PixelSize, #[default((0., 50.))] end: PixelSize) -> VectorDataTable {
fn line(_: impl Ctx, _primary: (), #[default(0., 0.)] start: PixelSize, #[default(100., 100.)] end: PixelSize) -> VectorDataTable {
VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end)))
}
@@ -153,13 +180,14 @@ fn grid<T: GridSpacing>(
_: impl Ctx,
_primary: (),
grid_type: GridType,
#[unit(" px")]
#[hard_min(0.)]
#[default(10)]
#[implementations(f64, DVec2)]
spacing: T,
#[default(30., 30.)] angles: DVec2,
#[default(10)] columns: u32,
#[default(10)] rows: u32,
#[default(30., 30.)] angles: DVec2,
) -> VectorDataTable {
let (x_spacing, y_spacing) = spacing.as_dvec2().into();
let (angle_a, angle_b) = angles.into();
@@ -251,11 +279,11 @@ mod tests {
#[test]
fn isometric_grid_test() {
// Doesn't crash with weird angles
grid((), (), GridType::Isometric, 0., (0., 0.).into(), 5, 5);
grid((), (), GridType::Isometric, 90., (90., 90.).into(), 5, 5);
grid((), (), GridType::Isometric, 0., 5, 5, (0., 0.).into());
grid((), (), GridType::Isometric, 90., 5, 5, (90., 90.).into());
// Works properly
let grid = grid((), (), GridType::Isometric, 10., (30., 30.).into(), 5, 5);
let grid = grid((), (), GridType::Isometric, 10., 5, 5, (30., 30.).into());
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() {
@@ -270,7 +298,7 @@ mod tests {
#[test]
fn skew_isometric_grid_test() {
let grid = grid((), (), GridType::Isometric, 10., (40., 30.).into(), 5, 5);
let grid = grid((), (), GridType::Isometric, 10., 5, 5, (40., 30.).into());
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.point_domain.ids().len(), 5 * 5);
assert_eq!(grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.instance_ref_iter().next().unwrap().instance.segment_bezier_iter() {

View File

@@ -1,5 +1,4 @@
pub mod algorithms;
pub mod brush_stroke;
pub mod click_target;
pub mod generator_nodes;
pub mod misc;

View File

@@ -65,6 +65,7 @@ async fn assign_colors<T>(
randomize: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")]
/// The seed used for randomization.
/// Seed to determine unique variations on the randomized color selection.
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.
@@ -165,6 +166,7 @@ async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
#[default(Color::BLACK)]
/// The stroke color.
color: C,
#[unit(" px")]
#[default(2.)]
/// The stroke weight.
weight: f64,
@@ -183,6 +185,7 @@ async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
/// 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.
#[unit(" px")]
dash_offset: f64,
) -> Instances<V>
where
@@ -253,7 +256,9 @@ async fn circular_repeat<I: 'n + Send + Clone>(
// TODO: Implement other GraphicElementRendered types.
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
angle_offset: Angle,
#[default(5)] radius: f64,
#[unit(" px")]
#[default(5)]
radius: f64,
#[default(5)] instances: IntegerCount,
) -> Instances<I> {
let count = instances.max(1);
@@ -363,7 +368,7 @@ 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,
#[unit(" px")] offset: f64,
#[range((-90., 90.))] angle: Angle,
#[default(true)] keep_original: bool,
) -> Instances<I>
@@ -1143,10 +1148,10 @@ async fn sample_polyline(
_: impl Ctx,
vector_data: VectorDataTable,
spacing: PointSpacingType,
separation: f64,
quantity: f64,
start_offset: f64,
stop_offset: f64,
#[unit(" px")] separation: f64,
quantity: u32,
#[unit(" px")] start_offset: f64,
#[unit(" px")] stop_offset: f64,
adaptive_spacing: bool,
subpath_segment_lengths: Vec<f64>,
) -> VectorDataTable {
@@ -1186,7 +1191,7 @@ async fn sample_polyline(
let amount = match spacing {
PointSpacingType::Separation => separation,
PointSpacingType::Quantity => quantity,
PointSpacingType::Quantity => quantity as f64,
};
let Some(mut sample_bezpath) = sample_polyline_on_bezpath(bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
continue;
@@ -1392,6 +1397,7 @@ async fn tangent_on_path(
async fn poisson_disk_points(
_: impl Ctx,
vector_data: VectorDataTable,
#[unit(" px")]
#[default(10.)]
#[hard_min(0.01)]
separation_disk_diameter: f64,
@@ -1502,7 +1508,14 @@ async fn spline(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
}
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)] amount: f64, seed: SeedValue) -> VectorDataTable {
async fn jitter_points(
_: impl Ctx,
vector_data: VectorDataTable,
#[unit(" px")]
#[default(5.)]
amount: f64,
seed: SeedValue,
) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
for mut vector_data_instance in vector_data.instance_iter() {
@@ -2084,7 +2097,7 @@ mod test {
#[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;
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 30., 0, 0., 0., false, vec![100.]).await;
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
@@ -2094,7 +2107,7 @@ mod test {
#[tokio::test]
async fn sample_polyline_adaptive_spacing() {
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, 18., 0., 45., 10., true, vec![100.]).await;
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 18., 0, 45., 10., true, vec![100.]).await;
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {

View File

@@ -1,6 +1,6 @@
use glam::DVec2;
use graphene_core::gradient::GradientStops;
use graphene_core::registry::types::{Fraction, Percentage};
use graphene_core::registry::types::{Fraction, Percentage, TextArea};
use graphene_core::{Color, Ctx, num_traits};
use log::warn;
use math_parser::ast;
@@ -78,8 +78,12 @@ fn math<U: num_traits::float::Float>(
#[node_macro::node(category("Math: Arithmetic"))]
fn add<U: Add<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] augend: U,
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] addend: T,
/// The left-hand side of the addition operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
augend: U,
/// The right-hand side of the addition operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
addend: T,
) -> <U as Add<T>>::Output {
augend + addend
}
@@ -88,8 +92,12 @@ fn add<U: Add<T>, T>(
#[node_macro::node(category("Math: Arithmetic"))]
fn subtract<U: Sub<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] minuend: U,
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] subtrahend: T,
/// The left-hand side of the subtraction operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
minuend: U,
/// The right-hand side of the subtraction operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
subtrahend: T,
) -> <U as Sub<T>>::Output {
minuend - subtrahend
}
@@ -98,9 +106,12 @@ fn subtract<U: Sub<T>, T>(
#[node_macro::node(category("Math: Arithmetic"))]
fn multiply<U: Mul<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] multiplier: U,
/// The left-hand side of the multiplication operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
multiplier: U,
/// The right-hand side of the multiplication operation.
#[default(1.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)]
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
multiplicand: T,
) -> <U as Mul<T>>::Output {
multiplier * multiplicand
@@ -112,7 +123,10 @@ fn multiply<U: Mul<T>, T>(
#[node_macro::node(category("Math: Arithmetic"))]
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>(
_: impl Ctx,
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, DVec2, f64)] numerator: U,
/// The left-hand side of the division operation.
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, DVec2, f64)]
numerator: U,
/// The right-hand side of the division operation.
#[default(1.)]
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, f64, DVec2)]
denominator: T,
@@ -130,10 +144,15 @@ where
#[node_macro::node(category("Math: Arithmetic"))]
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, DVec2, f64)] numerator: U,
/// The left-hand side of the modulo operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
numerator: U,
/// The right-hand side of the modulo operation.
#[default(2.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, f64, DVec2)]
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
modulus: T,
/// Ensures the result will always be positive, even if the numerator is negative.
#[default(true)]
always_positive: bool,
) -> <U as Rem<T>>::Output {
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
@@ -143,9 +162,12 @@ fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy
#[node_macro::node(category("Math: Arithmetic"))]
fn exponent<U: Pow<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32)] base: U,
/// The base number that will be raised to the power.
#[implementations(f64, f32, u32)]
base: U,
/// The power to which the base number will be raised.
#[default(2.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32)]
#[implementations(f64, f32, u32)]
power: T,
) -> <U as num_traits::Pow<T>>::Output {
base.pow(power)
@@ -155,9 +177,11 @@ fn exponent<U: Pow<T>, T>(
#[node_macro::node(category("Math: Arithmetic"))]
fn root<U: num_traits::float::Float>(
_: impl Ctx,
/// The number for which the nth root will be calculated.
#[default(2.)]
#[implementations(f64, f32)]
radicand: U,
/// The degree of the root to be calculated. Square root is 2, cube root is 3, and so on.
#[default(2.)]
#[implementations(f64, f32)]
degree: U,
@@ -175,7 +199,10 @@ fn root<U: num_traits::float::Float>(
#[node_macro::node(category("Math: Arithmetic"))]
fn logarithm<U: num_traits::float::Float>(
_: impl Ctx,
#[implementations(f64, f32)] value: U,
/// The number for which the logarithm will be calculated.
#[implementations(f64, f32)]
value: U,
/// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm).
#[default(2.)]
#[implementations(f64, f32)]
base: U,
@@ -193,39 +220,83 @@ fn logarithm<U: num_traits::float::Float>(
/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn sine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
fn sine<U: num_traits::float::Float>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
theta: U,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> U {
if radians { theta.sin() } else { theta.to_radians().sin() }
}
/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn cosine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
fn cosine<U: num_traits::float::Float>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
theta: U,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> U {
if radians { theta.cos() } else { theta.to_radians().cos() }
}
/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length.
#[node_macro::node(category("Math: Trig"))]
fn tangent<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
fn tangent<U: num_traits::float::Float>(
_: impl Ctx,
/// The given angle.
#[implementations(f64, f32)]
theta: U,
/// Whether the given angle should be interpreted as radians instead of degrees.
radians: bool,
) -> U {
if radians { theta.tan() } else { theta.to_radians().tan() }
}
/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value.
#[node_macro::node(category("Math: Trig"))]
fn sine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
fn sine_inverse<U: num_traits::float::Float>(
_: impl Ctx,
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN.
#[implementations(f64, f32)]
value: U,
/// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool,
) -> U {
if radians { value.asin() } else { value.asin().to_degrees() }
}
/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value.
#[node_macro::node(category("Math: Trig"))]
fn cosine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
fn cosine_inverse<U: num_traits::float::Float>(
_: impl Ctx,
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN.
#[implementations(f64, f32)]
value: U,
/// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool,
) -> U {
if radians { value.acos() } else { value.acos().to_degrees() }
}
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
/// atan: the angle whose tangent is the specified scalar number.
/// atan2: the angle of a ray from the origin to the specified coordinate.
///
/// The resulting angle is always in the range [0°, 180°] or, in radians, [-π/2, π/2].
#[node_macro::node(category("Math: Trig"))]
fn tangent_inverse<U: TangentInverse>(_: impl Ctx, #[implementations(f64, f32, DVec2)] value: U, radians: bool) -> U::Output {
fn tangent_inverse<U: TangentInverse>(
_: impl Ctx,
/// The given value for which the angle will be calculated.
#[implementations(f64, f32, DVec2)]
value: U,
/// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool,
) -> U::Output {
value.atan(radians)
}
@@ -257,10 +328,13 @@ impl TangentInverse for DVec2 {
fn random<U: num_traits::float::Float>(
_: impl Ctx,
_primary: (),
/// Seed to determine the unique variation of which number will be generated.
seed: u64,
/// The smaller end of the range within which the random number will be generated.
#[implementations(f64, f32)]
#[default(0.)]
min: U,
/// The larger end of the range within which the random number will be generated.
#[implementations(f64, f32)]
#[default(1.)]
max: U,
@@ -294,37 +368,73 @@ fn to_f64<U: num_traits::int::PrimInt>(_: impl Ctx, #[implementations(u32, u64)]
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
#[node_macro::node(category("Math: Numeric"))]
fn round<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
fn round<U: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded.
#[implementations(f64, f32)]
value: U,
) -> U {
value.round()
}
/// The floor function (floor) reduces an input value to its nearest larger whole number, unless the input number is already whole.
/// The floor function (floor) rounds down an input value to the nearest whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn floor<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
fn floor<U: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded down.
#[implementations(f64, f32)]
value: U,
) -> U {
value.floor()
}
/// The ceiling function (ceil) increases an input value to its nearest smaller whole number, unless the input number is already whole.
/// The ceiling function (ceil) rounds up an input value to the nearest whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn ceiling<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
fn ceiling<U: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded up.
#[implementations(f64, f32)]
value: U,
) -> U {
value.ceil()
}
/// The absolute value function (abs) removes the negative sign from an input value, if present.
#[node_macro::node(category("Math: Numeric"))]
fn absolute_value<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
fn absolute_value<U: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be made positive.
#[implementations(f64, f32)]
value: U,
) -> U {
value.abs()
}
/// The minimum function (min) picks the smaller of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn min<T: std::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
fn min<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// One of the two numbers, of which the lesser will be returned.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The other of the two numbers, of which the lesser will be returned.
#[implementations(f64, f32, u32, &str)]
other_value: T,
) -> T {
if value < other_value { value } else { other_value }
}
/// The maximum function (max) picks the larger of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn max<T: std::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
fn max<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// One of the two numbers, of which the greater will be returned.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The other of the two numbers, of which the greater will be returned.
#[implementations(f64, f32, u32, &str)]
other_value: T,
) -> T {
if value > other_value { value } else { other_value }
}
@@ -332,9 +442,15 @@ fn max<T: std::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &
#[node_macro::node(category("Math: Numeric"))]
fn clamp<T: std::cmp::PartialOrd>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] min: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] max: T,
/// The number to be clamped, which will be restricted to the range between the minimum and maximum values.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The left (smaller) side of the range. The output will never be less than this number.
#[implementations(f64, f32, u32, &str)]
min: T,
/// The right (greater) side of the range. The output will never be greater than this number.
#[implementations(f64, f32, u32, &str)]
max: T,
) -> T {
let (min, max) = if min < max { (min, max) } else { (max, min) };
if value < min {
@@ -350,8 +466,12 @@ fn clamp<T: std::cmp::PartialOrd>(
#[node_macro::node(category("Math: Logic"))]
fn equals<U: std::cmp::PartialEq<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
/// One of the two numbers to compare for equality.
#[implementations(f64, f32, u32, DVec2, &str)]
value: T,
/// The other of the two numbers to compare for equality.
#[implementations(f64, f32, u32, DVec2, &str)]
other_value: U,
) -> bool {
other_value == value
}
@@ -360,8 +480,12 @@ fn equals<U: std::cmp::PartialEq<T>, T>(
#[node_macro::node(category("Math: Logic"))]
fn not_equals<U: std::cmp::PartialEq<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
/// One of the two numbers to compare for inequality.
#[implementations(f64, f32, u32, DVec2, &str)]
value: T,
/// The other of the two numbers to compare for inequality.
#[implementations(f64, f32, u32, DVec2, &str)]
other_value: U,
) -> bool {
other_value != value
}
@@ -371,8 +495,13 @@ fn not_equals<U: std::cmp::PartialEq<T>, T>(
#[node_macro::node(category("Math: Logic"))]
fn less_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] other_value: T,
/// The number on the left-hand side of the comparison.
#[implementations(f64, f32, u32)]
value: T,
/// The number on the right-hand side of the comparison.
#[implementations(f64, f32, u32)]
other_value: T,
/// Uses the less-than-or-equal operation (<=) instead of the less-than operation (<).
or_equal: bool,
) -> bool {
if or_equal { value <= other_value } else { value < other_value }
@@ -383,8 +512,13 @@ fn less_than<T: std::cmp::PartialOrd<T>>(
#[node_macro::node(category("Math: Logic"))]
fn greater_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] other_value: T,
/// The number on the left-hand side of the comparison.
#[implementations(f64, f32, u32)]
value: T,
/// The number on the right-hand side of the comparison.
#[implementations(f64, f32, u32)]
other_value: T,
/// Uses the greater-than-or-equal operation (>=) instead of the greater-than operation (>).
or_equal: bool,
) -> bool {
if or_equal { value >= other_value } else { value > other_value }
@@ -392,19 +526,35 @@ fn greater_than<T: std::cmp::PartialOrd<T>>(
/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_or(_: impl Ctx, value: bool, other_value: bool) -> bool {
fn logical_or(
_: impl Ctx,
/// One of the two boolean values, either of which may be true for the node to output true.
value: bool,
/// The other of the two boolean values, either of which may be true for the node to output true.
other_value: bool,
) -> bool {
value || other_value
}
/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_and(_: impl Ctx, value: bool, other_value: bool) -> bool {
fn logical_and(
_: impl Ctx,
/// One of the two boolean values, both of which must be true for the node to output true.
value: bool,
/// The other of the two boolean values, both of which must be true for the node to output true.
other_value: bool,
) -> bool {
value && other_value
}
/// The logical not operation (!) reverses true and false value of the input.
#[node_macro::node(category("Math: Logic"))]
fn logical_not(_: impl Ctx, input: bool) -> bool {
fn logical_not(
_: impl Ctx,
/// The boolean value to be reversed.
input: bool,
) -> bool {
!input
}
@@ -453,7 +603,7 @@ fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> Gradien
/// Constructs a string value which may be set to any plain text.
#[node_macro::node(category("Value"))]
fn string_value(_: impl Ctx, _primary: (), string: String) -> String {
fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String {
string
}

View File

@@ -17,8 +17,10 @@ loading = ["serde_json"]
dyn-any = { workspace = true }
graphene-core = { workspace = true }
graphene-path-bool = { workspace = true }
graphene-brush = { workspace = true }
graphene-application-io = { workspace = true }
graphene-svg-renderer = { workspace = true }
graphene-raster-nodes = { workspace = true }
# Workspace dependencies
log = { workspace = true }

View File

@@ -363,17 +363,6 @@ impl NodeInput {
NodeInput::Reflection(_) => false,
}
}
/// Network node inputs in the document network are not displayed, but still exist in the compiled network
pub fn is_exposed_to_frontend(&self, is_document_network: bool) -> bool {
match self {
NodeInput::Node { .. } => true,
NodeInput::Value { exposed, .. } => *exposed,
NodeInput::Network { .. } => !is_document_network,
NodeInput::Inline(_) => false,
NodeInput::Scope(_) => false,
NodeInput::Reflection(_) => false,
}
}
pub fn ty(&self) -> Type {
match self {
@@ -1250,24 +1239,28 @@ impl NodeNetwork {
/// Create a [`RecursiveNodeIter`] that iterates over all [`DocumentNode`]s, including ones that are deeply nested.
pub fn recursive_nodes(&self) -> RecursiveNodeIter<'_> {
let nodes = self.nodes.iter().collect();
let nodes = self.nodes.iter().map(|(id, node)| (id, node, Vec::new())).collect();
RecursiveNodeIter { nodes }
}
}
/// An iterator over all [`DocumentNode`]s, including ones that are deeply nested.
pub struct RecursiveNodeIter<'a> {
nodes: Vec<(&'a NodeId, &'a DocumentNode)>,
nodes: Vec<(&'a NodeId, &'a DocumentNode, Vec<NodeId>)>,
}
impl<'a> Iterator for RecursiveNodeIter<'a> {
type Item = (&'a NodeId, &'a DocumentNode);
type Item = (&'a NodeId, &'a DocumentNode, Vec<NodeId>);
fn next(&mut self) -> Option<Self::Item> {
let node = self.nodes.pop()?;
if let DocumentNodeImplementation::Network(network) = &node.1.implementation {
self.nodes.extend(network.nodes.iter());
let (current_id, node, path) = self.nodes.pop()?;
if let DocumentNodeImplementation::Network(network) = &node.implementation {
self.nodes.extend(network.nodes.iter().map(|(id, node)| {
let mut nested_path = path.clone();
nested_path.push(*current_id);
(id, node, nested_path)
}));
}
Some(node)
Some((current_id, node, path))
}
}

View File

@@ -5,8 +5,8 @@ use dyn_any::DynAny;
pub use dyn_any::StaticType;
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphene_application_io::SurfaceFrame;
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::{BlendMode, LuminanceCalculation};
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::uuid::NodeId;
@@ -209,29 +209,29 @@ tagged_value! {
#[serde(alias = "GradientPositions")] // TODO: Eventually remove this alias document upgrade code
GradientStops(graphene_core::vector::style::GradientStops),
Font(graphene_core::text::Font),
BrushStrokes(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
BrushStrokes(Vec<BrushStroke>),
BrushCache(BrushCache),
DocumentNode(DocumentNode),
Curve(graphene_core::raster::curve::Curve),
Curve(graphene_raster_nodes::curve::Curve),
Footprint(graphene_core::transform::Footprint),
VectorModification(Box<graphene_core::vector::VectorModification>),
FontCache(Arc<graphene_core::text::FontCache>),
// ==========
// ENUM TYPES
// ==========
BlendMode(BlendMode),
LuminanceCalculation(LuminanceCalculation),
BlendMode(graphene_core::blending::BlendMode),
LuminanceCalculation(graphene_raster_nodes::adjustments::LuminanceCalculation),
XY(graphene_core::extract_xy::XY),
RedGreenBlue(graphene_core::raster::RedGreenBlue),
RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha),
RedGreenBlue(graphene_raster_nodes::adjustments::RedGreenBlue),
RedGreenBlueAlpha(graphene_raster_nodes::adjustments::RedGreenBlueAlpha),
RealTimeMode(graphene_core::animation::RealTimeMode),
NoiseType(graphene_core::raster::NoiseType),
FractalType(graphene_core::raster::FractalType),
CellularDistanceFunction(graphene_core::raster::CellularDistanceFunction),
CellularReturnType(graphene_core::raster::CellularReturnType),
DomainWarpType(graphene_core::raster::DomainWarpType),
RelativeAbsolute(graphene_core::raster::RelativeAbsolute),
SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice),
NoiseType(graphene_raster_nodes::adjustments::NoiseType),
FractalType(graphene_raster_nodes::adjustments::FractalType),
CellularDistanceFunction(graphene_raster_nodes::adjustments::CellularDistanceFunction),
CellularReturnType(graphene_raster_nodes::adjustments::CellularReturnType),
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),

View File

@@ -0,0 +1,35 @@
[package]
name = "graphene-raster-nodes"
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"]
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
graphene-core = { workspace = true }
node-macro = { workspace = true }
# Workspace dependencies
glam = { workspace = true }
specta = { workspace = true }
image = { workspace = true }
bytemuck = { workspace = true }
ndarray = { workspace = true }
bezier-rs = { workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
fastnoise-lite = { workspace = true }
# Optional workspace dependencies
serde = { workspace = true, optional = true, features = ["derive"] }
[dev-dependencies]
tokio = { workspace = true }
futures = { workspace = true }

View File

@@ -1,16 +1,16 @@
#![allow(clippy::too_many_arguments)]
use crate::GraphicElement;
use crate::blending::BlendMode;
use crate::raster::curve::{CubicSplines, CurveManipulatorGroup};
use crate::raster::curve::{Curve, ValueMapperNode};
use crate::raster::image::Image;
use crate::raster::{Channel, Color, Pixel};
use crate::raster_types::{CPU, Raster, RasterDataTable};
use crate::registry::types::{Angle, Percentage, SignedPercentage};
use crate::vector::style::GradientStops;
use crate::{Ctx, Node};
use crate::curve::CubicSplines;
use dyn_any::DynAny;
use graphene_core::Node;
use graphene_core::blending::BlendMode;
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 std::cmp::Ordering;
use std::fmt::Debug;
@@ -576,10 +576,7 @@ impl Adjust<Color> for GradientStops {
}
}
}
impl Adjust<Color> for RasterDataTable<CPU>
where
GraphicElement: From<Image<Color>>,
{
impl Adjust<Color> for RasterDataTable<CPU> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for instance in self.instance_mut_iter() {
for c in instance.instance.data_mut().data.iter_mut() {
@@ -1130,48 +1127,6 @@ async fn exposure<T: Adjust<Color>>(
input
}
const WINDOW_SIZE: usize = 1024;
#[node_macro::node(category(""))]
fn generate_curves<C: Channel + crate::raster::Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
use bezier_rs::{Bezier, TValue};
let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle];
let mut lut = vec![C::from_f64(0.); WINDOW_SIZE];
let end = CurveManipulatorGroup {
anchor: [1.; 2],
handles: [curve.last_handle, [0.; 2]],
};
for sample in curve.manipulator_groups.iter().chain(std::iter::once(&end)) {
let [x0, y0, x1, y1, x2, y2, x3, y3] = [pos[0], pos[1], param[0], param[1], sample.handles[0][0], sample.handles[0][1], sample.anchor[0], sample.anchor[1]].map(f64::from);
let bezier = Bezier::from_cubic_coordinates(x0, y0, x1, y1, x2, y2, x3, y3);
let [left, right] = [pos[0], sample.anchor[0]].map(|c| c.clamp(0., 1.));
let lut_index_left: usize = (left * (lut.len() - 1) as f32).floor() as _;
let lut_index_right: usize = (right * (lut.len() - 1) as f32).ceil() as _;
for index in lut_index_left..=lut_index_right {
let x = index as f64 / (lut.len() - 1) as f64;
let y = if x <= x0 {
y0
} else if x >= x3 {
y3
} else {
bezier.find_tvalues_for_x(x)
.next()
.map(|t| bezier.evaluate(TValue::Parametric(t.clamp(0., 1.))).y)
// Fall back to a very bad approximation if Bezier-rs fails
.unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0)
};
lut[index] = C::from_f64(y);
}
pos = sample.anchor;
param = sample.handles[1];
}
ValueMapperNode::new(lut)
}
#[node_macro::node(category("Raster: Adjustment"))]
fn color_overlay<T: Adjust<Color>>(
_: impl Ctx,
@@ -1224,10 +1179,10 @@ fn color_overlay<T: Adjust<Color>>(
#[cfg(test)]
mod test {
use crate::Color;
use crate::blending::BlendMode;
use crate::raster::image::Image;
use crate::raster_types::{Raster, RasterDataTable};
use graphene_core::blending::BlendMode;
use graphene_core::color::Color;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{Raster, RasterDataTable};
#[tokio::test]
async fn color_overlay_multiply() {

View File

@@ -1,6 +1,7 @@
use super::{Channel, Linear, LuminanceMut};
use crate::Node;
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use graphene_core::Node;
use graphene_core::color::{Channel, Linear, LuminanceMut};
use std::hash::{Hash, Hasher};
use std::ops::{Add, Mul, Sub};
#[derive(Debug, Clone, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
@@ -23,8 +24,8 @@ impl Default for Curve {
}
}
impl std::hash::Hash for Curve {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl Hash for Curve {
fn hash<H: Hasher>(&self, state: &mut H) {
self.manipulator_groups.hash(state);
[self.first_handle, self.last_handle].iter().flatten().for_each(|f| f.to_bits().hash(state));
}
@@ -36,8 +37,8 @@ pub struct CurveManipulatorGroup {
pub handles: [[f32; 2]; 2],
}
impl std::hash::Hash for CurveManipulatorGroup {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl Hash for CurveManipulatorGroup {
fn hash<H: Hasher>(&self, state: &mut H) {
for c in self.handles.iter().chain([&self.anchor]).flatten() {
c.to_bits().hash(state);
}

View File

@@ -1,7 +1,7 @@
use graph_craft::proto::types::Percentage;
use graphene_core::Ctx;
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 image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage};
use ndarray::{Array2, ArrayBase, Dim, OwnedRepr};
use std::cmp::{max, min};
@@ -15,7 +15,7 @@ async fn dehaze(_: impl Ctx, image_frame: RasterDataTable<CPU>, strength: Percen
// Prepare the image data for processing
let image_data = bytemuck::cast_vec(image.data.clone());
let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type.");
let dynamic_image: image::DynamicImage = image_buffer.into();
let dynamic_image: DynamicImage = image_buffer.into();
// Run the dehaze algorithm
let dehazed_dynamic_image = dehaze_image(dynamic_image, strength / 100.);

View File

@@ -1,8 +1,9 @@
use graph_craft::proto::types::PixelLength;
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::{Color, Ctx};
use graphene_core::registry::types::PixelLength;
/// Blurs the image with a Gaussian or blur kernel filter.
#[node_macro::node(category("Raster: Filter"))]

View File

@@ -0,0 +1,46 @@
//! requires bezier-rs
use crate::curve::{Curve, CurveManipulatorGroup, ValueMapperNode};
use bezier_rs::{Bezier, TValue};
use graphene_core::color::{Channel, Linear};
use graphene_core::context::Ctx;
const WINDOW_SIZE: usize = 1024;
#[node_macro::node(category(""))]
fn generate_curves<C: Channel + Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle];
let mut lut = vec![C::from_f64(0.); WINDOW_SIZE];
let end = CurveManipulatorGroup {
anchor: [1.; 2],
handles: [curve.last_handle, [0.; 2]],
};
for sample in curve.manipulator_groups.iter().chain(std::iter::once(&end)) {
let [x0, y0, x1, y1, x2, y2, x3, y3] = [pos[0], pos[1], param[0], param[1], sample.handles[0][0], sample.handles[0][1], sample.anchor[0], sample.anchor[1]].map(f64::from);
let bezier = Bezier::from_cubic_coordinates(x0, y0, x1, y1, x2, y2, x3, y3);
let [left, right] = [pos[0], sample.anchor[0]].map(|c| c.clamp(0., 1.));
let lut_index_left: usize = (left * (lut.len() - 1) as f32).floor() as _;
let lut_index_right: usize = (right * (lut.len() - 1) as f32).ceil() as _;
for index in lut_index_left..=lut_index_right {
let x = index as f64 / (lut.len() - 1) as f64;
let y = if x <= x0 {
y0
} else if x >= x3 {
y3
} else {
bezier.find_tvalues_for_x(x)
.next()
.map(|t| bezier.evaluate(TValue::Parametric(t.clamp(0., 1.))).y)
// Fall back to a very bad approximation if Bezier-rs fails
.unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0)
};
lut[index] = C::from_f64(y);
}
pos = sample.anchor;
param = sample.handles[1];
}
ValueMapperNode::new(lut)
}

View File

@@ -1,5 +1,6 @@
use graphene_core::color::Color;
use graphene_core::context::Ctx;
use graphene_core::raster_types::{CPU, RasterDataTable};
use graphene_core::{Color, Ctx};
#[node_macro::node(category("Color"))]
async fn image_color_palette(

View File

@@ -0,0 +1,7 @@
pub mod adjustments;
pub mod curve;
pub mod dehaze;
pub mod filter;
pub mod generate_curves;
pub mod image_color_palette;
pub mod std_nodes;

View File

@@ -1,14 +1,19 @@
use crate::instances::Instances;
use graphene_core::instances::Instances;
use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType};
use dyn_any::DynAny;
use fastnoise_lite;
use glam::{DAffine2, DVec2, Vec2};
use graphene_core::blending::AlphaBlending;
use graphene_core::color::Color;
use graphene_core::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use graphene_core::context::{Ctx, ExtractFootprint};
use graphene_core::instances::Instance;
use graphene_core::math::bbox::Bbox;
pub use graphene_core::raster::*;
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_core::vector::VectorDataTable;
use graphene_core::{Ctx, ExtractFootprint};
use graphene_core::{GraphicElement, GraphicGroupTable};
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
@@ -28,7 +33,7 @@ impl From<std::io::Error> for Error {
}
#[node_macro::node(category("Debug: Raster"))]
fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
let mut result_table = RasterDataTable::default();
for mut image_frame_instance in image_frame.instance_iter() {
@@ -95,7 +100,7 @@ fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDa
}
#[node_macro::node(category("Raster: Channels"))]
fn combine_channels(
pub fn combine_channels(
_: impl Ctx,
_primary: (),
#[expose] red: RasterDataTable<CPU>,
@@ -185,7 +190,7 @@ fn combine_channels(
}
#[node_macro::node(category("Raster"))]
fn mask<T, E>(
pub fn mask<T, E>(
_: impl Ctx,
/// The image to be masked.
#[implementations(
@@ -212,7 +217,8 @@ where
image
}
fn mask_lambda(image: RasterDataTable<CPU>, stencil: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
// TODO: Use as in-place raster modifier
fn _mask_lambda(image: RasterDataTable<CPU>, stencil: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
// TODO: Support multiple stencil instances
let Some(stencil_instance) = stencil.instance_iter().next() else {
// No stencil provided so we return the original image
@@ -255,7 +261,7 @@ fn mask_lambda(image: RasterDataTable<CPU>, stencil: RasterDataTable<CPU>) -> Ra
}
#[node_macro::node(category(""))]
fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAffine2) -> RasterDataTable<CPU> {
pub fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAffine2) -> RasterDataTable<CPU> {
let mut result_table = RasterDataTable::default();
for mut image_instance in image.instance_iter() {
@@ -308,7 +314,7 @@ fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAff
}
#[node_macro::node(category("Debug: Raster"))]
fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> RasterDataTable<CPU> {
pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> RasterDataTable<CPU> {
let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
@@ -325,13 +331,13 @@ fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> RasterDataTabl
/// Constructs a raster image.
#[node_macro::node(category(""))]
fn image_value(_: impl Ctx, _primary: (), image: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
pub fn image_value(_: impl Ctx, _primary: (), image: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
image
}
#[node_macro::node(category("Raster: Pattern"))]
#[allow(clippy::too_many_arguments)]
fn noise_pattern(
pub fn noise_pattern(
ctx: impl ExtractFootprint + Ctx,
_primary: (),
clip: bool,
@@ -487,7 +493,7 @@ fn noise_pattern(
}
#[node_macro::node(category("Raster: Pattern"))]
fn mandelbrot(ctx: impl ExtractFootprint + Send) -> RasterDataTable<CPU> {
pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> RasterDataTable<CPU> {
let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space();

View File

@@ -32,6 +32,8 @@ graphene-path-bool = { workspace = true }
graphene-math-nodes = { workspace = true }
graphene-svg-renderer = { workspace = true }
graphene-application-io = { workspace = true }
graphene-raster-nodes = { workspace = true }
graphene-brush = { workspace = true }
# Workspace dependencies
fastnoise-lite = { workspace = true }

View File

@@ -1,23 +1,26 @@
pub mod any;
pub mod brush;
pub mod dehaze;
pub mod filter;
pub mod http;
pub mod image_color_palette;
pub mod raster;
pub mod text;
#[cfg(feature = "wasm")]
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_math_nodes as math_nodes;
pub use graphene_path_bool as path_bool;
pub use graphene_raster_nodes as raster_nodes;
/// stop gap solution until all `Quad` and `Rect` paths have been replaced with their absolute ones
/// stop gap solutions until all paths have been replaced with their absolute ones
pub mod renderer {
pub use graphene_core::math::quad::Quad;
pub use graphene_core::math::rect::Rect;
pub use graphene_svg_renderer::*;
}
pub mod raster {
pub use graphene_core::raster::*;
pub use graphene_raster_nodes::adjustments::*;
pub use graphene_raster_nodes::*;
}

View File

@@ -9,23 +9,37 @@ fn text<'i: 'n>(
editor: &'i WasmEditorApi,
text: String,
font_name: Font,
#[default(24.)] font_size: f64,
#[default(1.2)] line_height_ratio: f64,
#[default(1.)] character_spacing: f64,
#[default(None)] max_width: Option<f64>,
#[default(None)] max_height: Option<f64>,
#[unit(" px")]
#[default(24.)]
font_size: f64,
#[unit("x")]
#[default(1.2)]
line_height_ratio: f64,
#[unit(" px")]
#[default(0.)]
character_spacing: f64,
#[unit(" px")]
#[default(None)]
max_width: Option<f64>,
#[unit(" px")]
#[default(None)]
max_height: Option<f64>,
#[unit("°")]
#[default(0.)]
tilt: f64,
) -> VectorDataTable {
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
let typesetting = TypesettingConfig {
font_size,
line_height_ratio,
character_spacing,
max_width,
max_height,
tilt,
};
let result = VectorData::from_subpaths(to_path(&text, buzz_face, typesetting), false);
let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f));
let result = VectorData::from_subpaths(to_path(&text, font_data, typesetting), false);
VectorDataTable::new(result)
}

View File

@@ -12,6 +12,7 @@ static NODE_ID: AtomicU64 = AtomicU64::new(0);
pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStream2> {
let ParsedNodeFn {
vis,
attributes,
fn_name,
struct_name,
@@ -345,7 +346,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
/// Underlying implementation for [#struct_name]
#[inline]
#[allow(clippy::too_many_arguments)]
pub(crate) #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
#vis #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
#[automatically_derived]
impl<'n, #(#fn_generics,)* #(#struct_generics,)* #(#future_idents,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*>

View File

@@ -7,8 +7,8 @@ use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::{Comma, RArrow};
use syn::{
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, WhereClause,
parse_quote,
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, Visibility,
WhereClause, parse_quote,
};
use crate::codegen::generate_node_code;
@@ -22,6 +22,7 @@ pub(crate) struct Implementation {
#[derive(Debug)]
pub(crate) struct ParsedNodeFn {
pub(crate) vis: Visibility,
pub(crate) attributes: NodeFnAttributes,
pub(crate) fn_name: Ident,
pub(crate) struct_name: Ident,
@@ -263,6 +264,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
let attributes = syn::parse2::<NodeFnAttributes>(attr.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node_fn attributes: {}", e)))?;
let input_fn = syn::parse2::<ItemFn>(item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse function: {}. Make sure it's a valid Rust function.", e)))?;
let vis = input_fn.vis;
let fn_name = input_fn.sig.ident.clone();
let struct_name = format_ident!("{}", fn_name.to_string().to_case(Case::Pascal));
let mod_name = fn_name.clone();
@@ -297,6 +299,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
.fold(String::new(), |acc, b| acc + &b + "\n");
Ok(ParsedNodeFn {
vis,
attributes,
fn_name,
struct_name,
@@ -748,6 +751,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes {
category: Some(parse_quote!("Math: Arithmetic")),
display_name: None,
@@ -808,6 +812,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes {
category: Some(parse_quote!("General")),
display_name: None,
@@ -879,6 +884,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes {
category: Some(parse_quote!("Vector: Shape")),
display_name: None,
@@ -935,6 +941,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes {
category: Some(parse_quote!("Raster: Adjustment")),
display_name: None,
@@ -1003,6 +1010,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes {
category: Some(parse_quote!("Math: Arithmetic")),
display_name: None,
@@ -1059,6 +1067,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes {
category: Some(parse_quote!("IO")),
display_name: None,
@@ -1115,6 +1124,7 @@ mod tests {
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
vis: Visibility::Inherited,
attributes: NodeFnAttributes {
category: Some(parse_quote!("Custom")),
display_name: Some(parse_quote!("CustomNode2")),