Replace Footprint/() call arguments with dynamically-bound Contexts (#2232)

* Implement experimental Context struct and traits

* Add Ctx super trait

* Checkpoint

* Return Any instead of DynAny

* Fix send implementation for inputs with lifetimes

* Port more nodes

* Uncomment nodes

* Port more nodes

* Port vector nodes

* Partial progress (the stuff I'm more sure about)

* Partial progress (the stuff that's not compiling and I'm not sure about)

* Fix more errors

* First pass of fixing errors introduced by rebase

* Port wasm application io

* Fix brush node types

* Add type annotation

* Fix warnings and wasm compilation

* Change types for Document Node definitions

* Improve debugging for footprint not found errors

* Forward context in append artboard node

* Fix thumbnails

* Fix loading most demo artwork

* Wrap output type of all nodes in future

* Encode futures as part of the type

* Fix document node definitions for future types

* Remove Clippy warnings

* Fix more things

* Fix opening demo art with manual composition upgrading

* Set correct type for manual composition

* Fix brush

* Fix tests

* Update docs for deps

* Fix up some node signature issues

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
Dennis Kobert
2025-03-01 23:54:52 +01:00
committed by Keavon Chambers
parent 0c1e96b9c6
commit 4ff2bdb04f
43 changed files with 1338 additions and 1545 deletions

View File

@@ -1,21 +1,23 @@
use crate::raster::{blend_image_closure, BlendImageTupleNode, EmptyImageNode, ExtendImageToBoundsNode};
use crate::raster::{blend_image_closure, BlendImageTupleNode, ExtendImageToBoundsNode};
use graph_craft::generic::FnNode;
use graph_craft::proto::FutureWrapperNode;
use graphene_core::raster::adjustments::blend_colors;
use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
use graphene_core::raster::BlendMode;
use graphene_core::raster::{Alpha, BlendColorPairNode, Color, Image, Pixel, Sample};
use graphene_core::transform::{Footprint, Transform, TransformMut};
use graphene_core::raster::{Alpha, Color, Image, Pixel, Sample};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
use graphene_core::vector::brush_stroke::{BrushStroke, BrushStyle};
use graphene_core::vector::VectorDataTable;
use graphene_core::Node;
use graphene_core::{Ctx, Node};
use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"))]
fn vector_points(_: (), vector_data: VectorDataTable) -> Vec<DVec2> {
fn vector_points(_: impl Ctx, vector_data: VectorDataTable) -> Vec<DVec2> {
let vector_data = vector_data.one_item();
vector_data.point_domain.positions().to_vec()
@@ -131,14 +133,21 @@ where
target
}
pub fn create_brush_texture(brush_style: &BrushStyle) -> Image<Color> {
let stamp = BrushStampGeneratorNode::new(CopiedNode::new(brush_style.color), CopiedNode::new(brush_style.hardness), CopiedNode::new(brush_style.flow));
let stamp = stamp.eval(brush_style.diameter);
pub async fn create_brush_texture(brush_style: &BrushStyle) -> Image<Color> {
let stamp = brush_stamp_generator(brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow);
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
let blank_texture = EmptyImageNode::new(CopiedNode::new(transform), CopiedNode::new(Color::TRANSPARENT)).eval(());
let normal_blend = BlendColorPairNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
let blend_executor = BlendImageTupleNode::new(ValueNode::new(normal_blend));
blend_executor.eval((blank_texture, stamp)).image
use crate::raster::empty_image;
let blank_texture = empty_image((), transform, Color::TRANSPARENT);
// let normal_blend = BlendColorPairNode::new(
// FutureWrapperNode::new(ValueNode::new(CopiedNode::new(BlendMode::Normal))),
// FutureWrapperNode::new(ValueNode::new(CopiedNode::new(100.))),
// );
// normal_blend.eval((Color::default(), Color::default()));
// use crate::raster::blend_image_tuple;
// blend_image_tuple((blank_texture, stamp), &normal_blend).await.image;
crate::raster::blend_image_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)).image
// let blend_executoc = BlendImageTupleNode::new(FutureWrapperNode::new(ValueNode::new(normal_blend)));
// blend_executor.eval((blank_texture, stamp)).image
}
macro_rules! inline_blend_funcs {
@@ -202,7 +211,7 @@ pub fn blend_with_mode(background: ImageFrame<Color>, foreground: ImageFrame<Col
}
#[node_macro::node(category(""))]
fn brush(_: Footprint, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Color>, strokes: Vec<BrushStroke>, cache: BrushCache) -> ImageFrameTable<Color> {
async fn brush(_: impl Ctx, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Color>, strokes: Vec<BrushStroke>, cache: BrushCache) -> ImageFrameTable<Color> {
let image = image.one_item().clone();
let stroke_bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
@@ -225,11 +234,13 @@ fn brush(_: Footprint, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Co
for (idx, stroke) in brush_plan.strokes.into_iter().enumerate() {
// Create brush texture.
// TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes.
let brush_texture = cache.get_cached_brush(&stroke.style).unwrap_or_else(|| {
let tex = create_brush_texture(&stroke.style);
let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() {
let tex = create_brush_texture(&stroke.style).await;
cache.store_brush(stroke.style.clone(), tex.clone());
tex
});
brush_texture = Some(tex);
}
let brush_texture = brush_texture.unwrap();
// Compute transformation from stroke texture space into layer space, and create the stroke texture.
let skip = if idx == 0 { brush_plan.first_stroke_point_skip } else { 0 };
@@ -246,16 +257,23 @@ fn brush(_: Footprint, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Co
let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.);
let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size);
let normal_blend = BlendColorPairNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(normal_blend));
// let normal_blend = BlendColorPairNode::new(ValueNode::new(CopiedNode::new(BlendMode::Normal)), ValueNode::new(CopiedNode::new(100.)));
let normal_blend = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Normal, 1.));
let blit_node = BlitNode::new(
FutureWrapperNode::new(ClonedNode::new(brush_texture)),
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(normal_blend)),
);
let blit_target = if idx == 0 {
let target = core::mem::take(&mut brush_plan.first_stroke_texture);
ExtendImageToBoundsNode::new(CopiedNode::new(stroke_to_layer)).eval(target)
} else {
EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
use crate::raster::empty_image;
empty_image((), stroke_to_layer, Color::TRANSPARENT)
// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
};
blit_node.eval(blit_target)
blit_node.eval(blit_target).await
};
// Cache image before doing final blend, and store final stroke texture.
@@ -277,34 +295,44 @@ fn brush(_: Footprint, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Co
let mut erase_restore_mask = opaque_image;
for stroke in erase_restore_strokes {
let brush_texture = cache.get_cached_brush(&stroke.style).unwrap_or_else(|| {
let tex = create_brush_texture(&stroke.style);
let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() {
let tex = create_brush_texture(&stroke.style).await;
cache.store_brush(stroke.style.clone(), tex.clone());
tex
});
brush_texture = Some(tex);
}
let brush_texture = brush_texture.unwrap();
let positions: Vec<_> = stroke.compute_blit_points().into_iter().collect();
match stroke.style.blend_mode {
BlendMode::Erase => {
let blend_params = BlendColorPairNode::new(CopiedNode::new(BlendMode::Erase), CopiedNode::new(100.));
let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params));
erase_restore_mask = blit_node.eval(erase_restore_mask);
let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Erase, 1.));
let blit_node = BlitNode::new(
FutureWrapperNode::new(ClonedNode::new(brush_texture)),
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(blend_params)),
);
erase_restore_mask = blit_node.eval(erase_restore_mask).await;
}
// Yes, this is essentially the same as the above, but we duplicate to inline the blend mode.
BlendMode::Restore => {
let blend_params = BlendColorPairNode::new(CopiedNode::new(BlendMode::Restore), CopiedNode::new(100.));
let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params));
erase_restore_mask = blit_node.eval(erase_restore_mask);
let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Restore, 1.));
let blit_node = BlitNode::new(
FutureWrapperNode::new(ClonedNode::new(brush_texture)),
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(blend_params)),
);
erase_restore_mask = blit_node.eval(erase_restore_mask).await;
}
_ => unreachable!(),
}
}
let blend_params = BlendColorPairNode::new(CopiedNode::new(BlendMode::MultiplyAlpha), CopiedNode::new(100.));
let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params));
actual_image = blend_executor.eval((actual_image, erase_restore_mask));
let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.));
let blend_executor = BlendImageTupleNode::new(FutureWrapperNode::new(ValueNode::new(blend_params)));
actual_image = blend_executor.eval((actual_image, erase_restore_mask)).await;
}
ImageFrameTable::new(actual_image)
@@ -315,15 +343,13 @@ mod test {
use super::*;
use graphene_core::transform::Transform;
use graphene_core::value::ClonedNode;
use glam::DAffine2;
#[test]
fn test_brush_texture() {
let brush_texture_node = BrushStampGeneratorNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(100.), ClonedNode::new(100.));
let size = 20.;
let image = brush_texture_node.eval(size);
let image = brush_stamp_generator(size, Color::BLACK, 100., 100.);
assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.)));
// center pixel should be BLACK
assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK));

View File

@@ -1,28 +1,14 @@
use graph_craft::proto::types::Percentage;
use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
use graphene_core::raster::Image;
use graphene_core::transform::Footprint;
use graphene_core::Color;
use graphene_core::{Color, Ctx};
use image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage};
use ndarray::{Array2, ArrayBase, Dim, OwnedRepr};
use std::cmp::{max, min};
#[node_macro::node(category("Raster: Filter"))]
async fn dehaze<F: 'n + Send + Sync>(
#[implementations(
(),
Footprint,
)]
footprint: F,
#[implementations(
() -> ImageFrameTable<Color>,
Footprint -> ImageFrameTable<Color>,
)]
image_frame: impl Node<F, Output = ImageFrameTable<Color>>,
strength: Percentage,
) -> ImageFrameTable<Color> {
let image_frame = image_frame.eval(footprint).await;
#[node_macro::node(category("Raster"))]
async fn dehaze(_: impl Ctx, image_frame: ImageFrameTable<Color>, strength: Percentage) -> ImageFrameTable<Color> {
let image_frame = image_frame.one_item();
// Prepare the image data for processing

View File

@@ -19,7 +19,7 @@ use crate::wasm_application_io::WasmApplicationIo;
// TODO: Move to graph-craft
#[node_macro::node(category("Debug: GPU"))]
async fn compile_gpu<'a: 'n>(_: (), node: &'a DocumentNode, typing_context: TypingContext, io: ShaderIO) -> Result<compilation_client::Shader, String> {
async fn compile_gpu<'a: 'n>(_: impl Ctx, node: &'a DocumentNode, typing_context: TypingContext, io: ShaderIO) -> Result<compilation_client::Shader, String> {
let mut typing_context = typing_context;
let compiler = graph_craft::graphene_compiler::Compiler {};
let DocumentNodeImplementation::Network(ref network) = node.implementation else { panic!() };
@@ -279,7 +279,7 @@ where
}
#[node_macro::node(category("Debug: GPU"))]
async fn blend_gpu_image(_: (), foreground: ImageFrameTable<Color>, background: ImageFrameTable<Color>, blend_mode: BlendMode, opacity: f64) -> ImageFrameTable<Color> {
async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, background: ImageFrameTable<Color>, blend_mode: BlendMode, opacity: f64) -> ImageFrameTable<Color> {
let foreground = foreground.one_item();
let background = background.one_item();

View File

@@ -1,9 +1,11 @@
use graphene_core::Ctx;
#[node_macro::node(category("Network"))]
async fn get_request(_: (), url: String) -> reqwest::Response {
async fn get_request(_: impl Ctx, url: String) -> reqwest::Response {
reqwest::get(url).await.unwrap()
}
#[node_macro::node(category("Network"))]
async fn post_request(_: (), url: String, body: String) -> reqwest::Response {
async fn post_request(_: impl Ctx, url: String, body: String) -> reqwest::Response {
reqwest::Client::new().post(url).body(body).send().await.unwrap()
}

View File

@@ -1,19 +1,10 @@
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::transform::Footprint;
use graphene_core::Color;
use graphene_core::{Color, Ctx};
#[node_macro::node(category("Raster"))]
async fn image_color_palette<F: 'n + Send>(
#[implementations(
(),
Footprint,
)]
footprint: F,
#[implementations(
() -> ImageFrameTable<Color>,
Footprint -> ImageFrameTable<Color>,
)]
image: impl Node<F, Output = ImageFrameTable<Color>>,
async fn image_color_palette(
_: impl Ctx,
image: ImageFrameTable<Color>,
#[min(1.)]
#[max(28.)]
max_size: u32,
@@ -25,7 +16,6 @@ async fn image_color_palette<F: 'n + Send>(
let mut histogram: Vec<usize> = vec![0; (bins + 1.) as usize];
let mut colors: Vec<Vec<Color>> = vec![vec![]; (bins + 1.) as usize];
let image = image.eval(footprint).await;
let image = image.one_item();
for pixel in image.image.data.iter() {
@@ -75,30 +65,24 @@ async fn image_color_palette<F: 'n + Send>(
mod test {
use super::*;
use graph_craft::generic::FnNode;
use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
use graphene_core::raster::Image;
use graphene_core::value::CopiedNode;
use graphene_core::Node;
#[test]
fn test_image_color_palette() {
let node = ImageColorPaletteNode {
max_size: CopiedNode(1u32),
image: FnNode::new(|_| {
Box::pin(async move {
ImageFrameTable::new(ImageFrame {
image: Image {
width: 100,
height: 100,
data: vec![Color::from_rgbaf32(0., 0., 0., 1.).unwrap(); 10000],
base64_string: None,
},
..Default::default()
})
})
let result = image_color_palette(
(),
ImageFrameTable::new(ImageFrame {
image: Image {
width: 100,
height: 100,
data: vec![Color::from_rgbaf32(0., 0., 0., 1.).unwrap(); 10000],
base64_string: None,
},
..Default::default()
}),
};
assert_eq!(futures::executor::block_on(node.eval(())), [Color::from_rgbaf32(0., 0., 0., 1.).unwrap()]);
1,
);
assert_eq!(futures::executor::block_on(result), [Color::from_rgbaf32(0., 0., 0., 1.).unwrap()]);
}
}

View File

@@ -2,10 +2,11 @@ use dyn_any::DynAny;
use graphene_core::raster::bbox::Bbox;
use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
use graphene_core::raster::{
Alpha, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, Image, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue, Sample,
Alpha, AlphaMut, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, Image, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue,
Sample,
};
use graphene_core::transform::{Footprint, Transform};
use graphene_core::{AlphaBlending, Color, Node};
use graphene_core::transform::Transform;
use graphene_core::{AlphaBlending, Color, Ctx, ExtractFootprint, Node};
use fastnoise_lite;
use glam::{DAffine2, DVec2, Vec2};
@@ -28,13 +29,14 @@ impl From<std::io::Error> for Error {
}
#[node_macro::node(category("Debug: Raster"))]
fn sample_image(footprint: Footprint, image_frame: ImageFrameTable<Color>) -> ImageFrameTable<Color> {
fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFrameTable<Color>) -> ImageFrameTable<Color> {
let image_frame = image_frame.one_item();
// Resize the image using the image crate
let image = &image_frame.image;
let data = bytemuck::cast_vec(image.data.clone());
let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space();
let image_bounds = Bbox::from_transform(image_frame.transform).to_axis_aligned_bbox();
let intersection = viewport_bounds.intersect(&image_bounds);
@@ -107,15 +109,7 @@ where
image
}
#[derive(Debug, Clone, Copy)]
pub struct InsertChannelNode<P, S, Insertion, TargetChannel> {
insertion: Insertion,
target_channel: TargetChannel,
_p: PhantomData<P>,
_s: PhantomData<S>,
}
#[node_macro::old_node_fn(InsertChannelNode<_P, _S>)]
#[node_macro::node]
fn insert_channel<
// _P is the color of the input image.
_P: RGBMut,
@@ -124,8 +118,9 @@ fn insert_channel<
Input: BitmapMut<Pixel = _P>,
Insertion: Bitmap<Pixel = _S>,
>(
mut image: Input,
insertion: Insertion,
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
#[implementations(ImageFrameTable<Color>)] insertion: Insertion,
target_channel: RedGreenBlue,
) -> Input
where
@@ -154,15 +149,60 @@ where
image
}
#[node_macro::node]
fn combine_channels<
// _P is the color of the input image.
_P: RGBMut + AlphaMut,
_S: Pixel + Luminance,
// Input image
Input: BitmapMut<Pixel = _P>,
Red: Bitmap<Pixel = _S>,
Green: Bitmap<Pixel = _S>,
Blue: Bitmap<Pixel = _S>,
Alpha: Bitmap<Pixel = _S>,
>(
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
#[implementations(ImageFrameTable<Color>)] red: Red,
#[implementations(ImageFrameTable<Color>)] green: Green,
#[implementations(ImageFrameTable<Color>)] blue: Blue,
#[implementations(ImageFrameTable<Color>)] alpha: Alpha,
) -> Input
where
_P::ColorChannel: Linear,
{
let dimensions = [red.dim(), green.dim(), blue.dim(), alpha.dim()];
if dimensions.iter().all(|&(x, _)| x == 0) {
return image;
}
#[derive(Debug, Clone, Copy)]
pub struct MaskImageNode<P, S, Stencil> {
stencil: Stencil,
_p: PhantomData<P>,
_s: PhantomData<S>,
if dimensions.iter().any(|&(x, y)| x != image.width() || y != image.height()) {
log::warn!("Stencil and image have different sizes. This is not supported.");
return image;
}
for y in 0..image.height() {
for x in 0..image.width() {
let image_pixel = image.get_pixel_mut(x, y).unwrap();
if let Some(r) = red.get_pixel(x, y) {
image_pixel.set_red(r.l().cast_linear_channel());
}
if let Some(g) = green.get_pixel(x, y) {
image_pixel.set_green(g.l().cast_linear_channel());
}
if let Some(b) = blue.get_pixel(x, y) {
image_pixel.set_blue(b.l().cast_linear_channel());
}
if let Some(a) = alpha.get_pixel(x, y) {
image_pixel.set_alpha(a.l().cast_linear_channel());
}
}
}
image
}
#[node_macro::old_node_fn(MaskImageNode<_P, _S>)]
#[node_macro::node()]
fn mask_image<
// _P is the color of the input image. It must have an alpha channel because that is going to
// be modified by the mask
@@ -175,8 +215,9 @@ fn mask_image<
// Stencil
Stencil: Transform + Sample<Pixel = _S>,
>(
mut image: Input,
stencil: Stencil,
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
#[implementations(ImageFrameTable<Color>)] stencil: Stencil,
) -> Input {
let image_size = DVec2::new(image.width() as f64, image.height() as f64);
let mask_size = stencil.transform().decompose_scale();
@@ -207,17 +248,17 @@ fn mask_image<
image
}
#[derive(Debug, Clone, Copy)]
pub struct BlendImageTupleNode<P, Fg, MapFn> {
map_fn: MapFn,
_p: PhantomData<P>,
_fg: PhantomData<Fg>,
}
// #[derive(Debug, Clone, Copy)]
// pub struct BlendImageTupleNode<P, Fg, MapFn> {
// map_fn: MapFn,
// _p: PhantomData<P>,
// _fg: PhantomData<Fg>,
// }
#[node_macro::old_node_fn(BlendImageTupleNode<_P, _Fg>)]
fn blend_image_tuple<_P: Alpha + Pixel + Debug, MapFn, _Fg: Sample<Pixel = _P> + Transform>(images: (ImageFrame<_P>, _Fg), map_fn: &'input MapFn) -> ImageFrame<_P>
#[node_macro::node(skip_impl)]
async fn blend_image_tuple<_P: Alpha + Pixel + Debug + Send, MapFn, _Fg: Sample<Pixel = _P> + Transform + Clone + Send + 'n>(images: (ImageFrame<_P>, _Fg), map_fn: &'n MapFn) -> ImageFrame<_P>
where
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'input + Clone,
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'n + Clone,
{
let (background, foreground) = images;
@@ -319,7 +360,7 @@ fn extend_image_to_bounds(image: ImageFrame<Color>, bounds: DAffine2) -> ImageFr
}
#[node_macro::node(category("Debug: Raster"))]
fn empty_image<P: Pixel>(_: (), transform: DAffine2, #[implementations(Color)] color: P) -> ImageFrame<P> {
fn empty_image<P: Pixel>(_: impl Ctx, transform: DAffine2, #[implementations(Color)] color: P) -> ImageFrame<P> {
let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
@@ -431,10 +472,10 @@ fn empty_image<P: Pixel>(_: (), transform: DAffine2, #[implementations(Color)] c
// tiling: Tiling: bool,
// }
#[node_macro::node(category("Raster: Generator"))]
#[node_macro::node(category("Raster"))]
#[allow(clippy::too_many_arguments)]
fn noise_pattern(
footprint: Footprint,
ctx: impl ExtractFootprint + Ctx,
_primary: (),
clip: bool,
seed: u32,
@@ -452,6 +493,7 @@ fn noise_pattern(
cellular_return_type: CellularReturnType,
cellular_jitter: f64,
) -> ImageFrameTable<Color> {
let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space();
let mut size = viewport_bounds.size();
@@ -585,8 +627,9 @@ fn noise_pattern(
ImageFrameTable::new(result)
}
#[node_macro::node(category("Raster: Generator"))]
fn mandelbrot(footprint: Footprint) -> ImageFrameTable<Color> {
#[node_macro::node(category("Raster"))]
fn mandelbrot(ctx: impl ExtractFootprint + Send) -> ImageFrameTable<Color> {
let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space();
let image_bounds = Bbox::from_transform(DAffine2::IDENTITY).to_axis_aligned_bbox();

View File

@@ -3,10 +3,11 @@ use crate::vector::{VectorData, VectorDataTable};
use graph_craft::wasm_application_io::WasmEditorApi;
use graphene_core::text::TypesettingConfig;
pub use graphene_core::text::{bounding_box, load_face, to_path, Font, FontCache};
use graphene_core::Ctx;
#[node_macro::node(category(""))]
fn text<'i: 'n>(
_: (),
_: impl Ctx,
editor: &'i WasmEditorApi,
text: String,
font_name: Font,

View File

@@ -1,11 +1,9 @@
use crate::transform::Footprint;
use bezier_rs::{ManipulatorGroup, Subpath};
use graphene_core::vector::misc::BooleanOperation;
use graphene_core::vector::style::Fill;
pub use graphene_core::vector::*;
use graphene_core::{transform::Transform, GraphicGroup};
use graphene_core::{Color, GraphicElement, GraphicGroupTable};
use graphene_core::{Color, Ctx, GraphicElement, GraphicGroupTable};
pub use path_bool as path_bool_lib;
use path_bool::{FillRule, PathBooleanOperation};
@@ -13,22 +11,7 @@ use glam::{DAffine2, DVec2};
use std::ops::Mul;
#[node_macro::node(category(""))]
async fn boolean_operation<F: 'n + Send>(
#[implementations(
(),
Footprint,
)]
footprint: F,
#[implementations(
() -> GraphicGroupTable,
Footprint -> GraphicGroupTable,
)]
group_of_paths: impl Node<F, Output = GraphicGroupTable>,
operation: BooleanOperation,
) -> VectorDataTable {
let group_of_paths = group_of_paths.eval(footprint).await;
let group_of_paths = group_of_paths.one_item();
async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, operation: BooleanOperation) -> VectorDataTable {
fn vector_from_image<T: Transform>(image_frame: T) -> VectorData {
let corner1 = DVec2::ZERO;
let corner2 = DVec2::new(1., 1.);
@@ -193,6 +176,7 @@ async fn boolean_operation<F: 'n + Send>(
}
}
let group_of_paths = group_of_paths.one_item();
// The first index is the bottom of the stack
let mut boolean_operation_result = boolean_operation_on_vector_data(&collect_vector_data(group_of_paths), operation);

View File

@@ -12,8 +12,7 @@ use graphene_core::renderer::RenderMetadata;
use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, RenderSvgSegmentList, SvgRender};
use graphene_core::transform::Footprint;
use graphene_core::vector::VectorDataTable;
use graphene_core::GraphicGroupTable;
use graphene_core::{Color, WasmNotSend};
use graphene_core::{Color, Context, Ctx, ExtractFootprint, GraphicGroupTable, OwnedContextImpl, WasmNotSend};
#[cfg(target_arch = "wasm32")]
use base64::Engine;
@@ -27,7 +26,7 @@ use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
#[node_macro::node(category("Debug: GPU"))]
async fn create_surface<'a: 'n>(_: (), editor: &'a WasmEditorApi) -> Arc<WasmSurfaceHandle> {
async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc<WasmSurfaceHandle> {
Arc::new(editor.application_io.as_ref().unwrap().create_window())
}
@@ -37,7 +36,7 @@ async fn create_surface<'a: 'n>(_: (), editor: &'a WasmEditorApi) -> Arc<WasmSur
// #[node_macro::node(category("Debug: GPU"))]
// #[cfg(target_arch = "wasm32")]
// async fn draw_image_frame(
// _: (),
// _: impl Ctx,
// image: ImageFrameTable<graphene_core::raster::SRGBA8>,
// surface_handle: Arc<WasmSurfaceHandle>,
// ) -> graphene_core::application_io::SurfaceHandleFrame<HtmlCanvasElement> {
@@ -60,7 +59,7 @@ async fn create_surface<'a: 'n>(_: (), editor: &'a WasmEditorApi) -> Arc<WasmSur
// }
#[node_macro::node(category("Network"))]
async fn load_resource<'a: 'n>(_: (), _primary: (), #[scope("editor-api")] editor: &'a WasmEditorApi, url: String) -> Arc<[u8]> {
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor: &'a WasmEditorApi, #[name("URL")] url: String) -> Arc<[u8]> {
let Some(api) = editor.application_io.as_ref() else {
return Arc::from(include_bytes!("../../graph-craft/src/null.png").to_vec());
};
@@ -74,8 +73,8 @@ async fn load_resource<'a: 'n>(_: (), _primary: (), #[scope("editor-api")] edito
data
}
#[node_macro::node(category("Raster"))]
fn decode_image(_: (), data: Arc<[u8]>) -> ImageFrameTable<Color> {
#[node_macro::node(category("Network"))]
fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> ImageFrameTable<Color> {
let Some(image) = image::load_from_memory(data.as_ref()).ok() else {
return ImageFrameTable::default();
};
@@ -156,13 +155,13 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen
#[node_macro::node(category(""))]
#[cfg(target_arch = "wasm32")]
async fn rasterize<T: GraphicElementRendered + graphene_core::transform::TransformMut + WasmNotSend + 'n>(
_: (),
_: impl Ctx,
#[implementations(
Footprint -> VectorDataTable,
Footprint -> ImageFrameTable<Color>,
Footprint -> GraphicGroupTable,
VectorDataTable,
ImageFrameTable<Color>,
GraphicGroupTable,
)]
data: impl Node<Footprint, Output = T>,
mut data: T,
footprint: Footprint,
surface_handle: Arc<SurfaceHandle<HtmlCanvasElement>>,
) -> ImageFrameTable<Color> {
@@ -171,7 +170,6 @@ async fn rasterize<T: GraphicElementRendered + graphene_core::transform::Transfo
return ImageFrameTable::default();
}
let mut data = data.eval(footprint).await;
let mut render = SvgRender::new();
let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox();
let size = aabb.size();
@@ -218,31 +216,34 @@ async fn rasterize<T: GraphicElementRendered + graphene_core::transform::Transfo
#[node_macro::node(category(""))]
async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
render_config: RenderConfig,
editor_api: &'a WasmEditorApi,
editor_api: impl Node<Context<'static>, Output = &'a WasmEditorApi>,
#[implementations(
Footprint -> VectorDataTable,
Footprint -> ImageFrameTable<Color>,
Footprint -> GraphicGroupTable,
Footprint -> graphene_core::Artboard,
Footprint -> graphene_core::ArtboardGroup,
Footprint -> Option<Color>,
Footprint -> Vec<Color>,
Footprint -> bool,
Footprint -> f32,
Footprint -> f64,
Footprint -> String,
Context -> VectorDataTable,
Context -> ImageFrameTable<Color>,
Context -> GraphicGroupTable,
Context -> graphene_core::Artboard,
Context -> graphene_core::ArtboardGroup,
Context -> Option<Color>,
Context -> Vec<Color>,
Context -> bool,
Context -> f32,
Context -> f64,
Context -> String,
)]
data: impl Node<Footprint, Output = T>,
_surface_handle: impl Node<(), Output = Option<wgpu_executor::WgpuSurface>>,
data: impl Node<Context<'static>, Output = T>,
_surface_handle: impl Node<Context<'static>, Output = Option<wgpu_executor::WgpuSurface>>,
) -> RenderOutput {
let footprint = render_config.viewport;
let ctx = OwnedContextImpl::default().with_footprint(footprint).into_context();
ctx.footprint();
let RenderConfig { hide_artboards, for_export, .. } = render_config;
let render_params = RenderParams::new(render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
let data = data.eval(footprint).await;
let data = data.eval(ctx.clone()).await;
let editor_api = editor_api.eval(ctx.clone()).await;
#[cfg(all(feature = "vello", target_arch = "wasm32"))]
let surface_handle = _surface_handle.eval(()).await;
let surface_handle = _surface_handle.eval(ctx.clone()).await;
let use_vello = editor_api.editor_preferences.use_vello();
#[cfg(all(feature = "vello", target_arch = "wasm32"))]
let use_vello = use_vello && surface_handle.is_some();