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