Instance tables refactor part 2: move the transform and alpha_blending fields up a level (#2249)

* Fix domain data structure field plural naming

* Rename method one_item to one_instance

Rename method one_item to one_instance

* Move the Instance<T> methods over to providing an Instance<T>/InstanceMut<T>

Move the Instance<T> methods over to providing an Instance<T>/InstanceMut<T>

* Add transform and alpha_blending fields to Instances<T>

* Finish the refactor (Brush tool is broken though)

* Add test for brush node

* Fix brush node

* Fix default empty images being 1x1 instead of 0x0 as they should be

* Fix tests

* Fix path transform

* Add correct upgrading to move the transform/blending up a level

---------

Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2025-03-02 01:26:36 -08:00
parent 4ff2bdb04f
commit f1160e1ca6
33 changed files with 1099 additions and 984 deletions

View File

@@ -6,19 +6,18 @@ 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, Color, Image, Pixel, Sample};
use graphene_core::raster::{Alpha, Bitmap, BlendMode, 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::{Ctx, Node};
use graphene_core::{Ctx, GraphicElement, Node};
use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"))]
fn vector_points(_: impl Ctx, vector_data: VectorDataTable) -> Vec<DVec2> {
let vector_data = vector_data.one_item();
let vector_data = vector_data.one_instance().instance;
vector_data.point_domain.positions().to_vec()
}
@@ -89,17 +88,24 @@ fn brush_stamp_generator(diameter: f64, color: Color, hardness: f64, flow: f64)
}
#[node_macro::node(skip_impl)]
fn blit<P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<P>, texture: Image<P>, positions: Vec<DVec2>, blend_mode: BlendFn) -> ImageFrame<P>
fn blit<P, BlendFn>(mut target: ImageFrameTable<P>, texture: Image<P>, positions: Vec<DVec2>, blend_mode: BlendFn) -> ImageFrameTable<P>
where
P: Pixel + Alpha + std::fmt::Debug + dyn_any::StaticType,
P::Static: Pixel,
BlendFn: for<'any_input> Node<'any_input, (P, P), Output = P>,
GraphicElement: From<ImageFrame<P>>,
{
if positions.is_empty() {
return target;
}
let target_size = DVec2::new(target.image.width as f64, target.image.height as f64);
let target_width = target.one_instance().instance.image.width;
let target_height = target.one_instance().instance.image.height;
let target_size = DVec2::new(target_width as f64, target_height as f64);
let texture_size = DVec2::new(texture.width as f64, texture.height as f64);
let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * target.transform.inverse();
let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * target.transform().inverse();
for position in positions {
let start = document_to_target.transform_point2(position).round();
@@ -114,17 +120,17 @@ where
// Tight blitting loop. Eagerly assert bounds to hopefully eliminate bounds check inside loop.
let texture_index = |x: u32, y: u32| -> usize { (y as usize * texture.width as usize) + (x as usize) };
let target_index = |x: u32, y: u32| -> usize { (y as usize * target.image.width as usize) + (x as usize) };
let target_index = |x: u32, y: u32| -> usize { (y as usize * target_width as usize) + (x as usize) };
let max_y = (blit_area_offset.y + blit_area_dimensions.y).saturating_sub(1);
let max_x = (blit_area_offset.x + blit_area_dimensions.x).saturating_sub(1);
assert!(texture_index(max_x, max_y) < texture.data.len());
assert!(target_index(max_x, max_y) < target.image.data.len());
assert!(target_index(max_x, max_y) < target.one_instance().instance.image.data.len());
for y in blit_area_offset.y..blit_area_offset.y + blit_area_dimensions.y {
for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x {
let src_pixel = texture.data[texture_index(x, y)];
let dst_pixel = &mut target.image.data[target_index(x + clamp_start.x, y + clamp_start.y)];
let dst_pixel = &mut target.one_instance_mut().instance.image.data[target_index(x + clamp_start.x, y + clamp_start.y)];
*dst_pixel = blend_mode.eval((src_pixel, *dst_pixel));
}
}
@@ -138,16 +144,9 @@ pub async fn create_brush_texture(brush_style: &BrushStyle) -> Image<Color> {
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
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
let image = crate::raster::blend_image_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
image.one_instance().instance.image.clone()
}
macro_rules! inline_blend_funcs {
@@ -162,7 +161,7 @@ macro_rules! inline_blend_funcs {
};
}
pub fn blend_with_mode(background: ImageFrame<Color>, foreground: ImageFrame<Color>, blend_mode: BlendMode, opacity: f64) -> ImageFrame<Color> {
pub fn blend_with_mode(background: ImageFrameTable<Color>, foreground: ImageFrameTable<Color>, blend_mode: BlendMode, opacity: f64) -> ImageFrameTable<Color> {
let opacity = opacity / 100.;
inline_blend_funcs!(
background,
@@ -211,21 +210,21 @@ pub fn blend_with_mode(background: ImageFrame<Color>, foreground: ImageFrame<Col
}
#[node_macro::node(category(""))]
async fn brush(_: impl Ctx, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Color>, strokes: Vec<BrushStroke>, cache: BrushCache) -> ImageFrameTable<Color> {
let image = image.one_item().clone();
async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: ImageFrameTable<Color>, strokes: Vec<BrushStroke>, cache: BrushCache) -> ImageFrameTable<Color> {
let stroke_bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
let image_bbox = Bbox::from_transform(image.transform).to_axis_aligned_bbox();
let image_bbox = Bbox::from_transform(image_frame_table.transform()).to_axis_aligned_bbox();
let bbox = if image_bbox.size().length() < 0.1 { stroke_bbox } else { stroke_bbox.union(&image_bbox) };
let mut draw_strokes: Vec<_> = strokes.iter().filter(|&s| !matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)).cloned().collect();
let erase_restore_strokes: Vec<_> = strokes.iter().filter(|&s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)).cloned().collect();
let mut brush_plan = cache.compute_brush_plan(image, &draw_strokes);
let mut brush_plan = cache.compute_brush_plan(image_frame_table, &draw_strokes);
let mut background_bounds = bbox.to_transform();
if bounds.transform() != DAffine2::ZERO {
// If the bounds are empty (no size on images or det(transform) = 0), keep the target bounds
let bounds_empty = bounds.instances().all(|bounds| bounds.instance.width() == 0 || bounds.instance.height() == 0);
if bounds.transform().matrix2.determinant() != 0. && !bounds_empty {
background_bounds = bounds.transform();
}
@@ -289,10 +288,10 @@ async fn brush(_: impl Ctx, image: ImageFrameTable<Color>, bounds: ImageFrameTab
if has_erase_strokes {
let opaque_image = ImageFrame {
image: Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE),
transform: background_bounds,
alpha_blending: Default::default(),
};
let mut erase_restore_mask = opaque_image;
let mut erase_restore_mask = ImageFrameTable::new(opaque_image);
*erase_restore_mask.transform_mut() = background_bounds;
*erase_restore_mask.one_instance_mut().alpha_blending = Default::default();
for stroke in erase_restore_strokes {
let mut brush_texture = cache.get_cached_brush(&stroke.style);
@@ -314,7 +313,6 @@ async fn brush(_: impl Ctx, image: ImageFrameTable<Color>, bounds: ImageFrameTab
);
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 = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Restore, 1.));
@@ -325,7 +323,6 @@ async fn brush(_: impl Ctx, image: ImageFrameTable<Color>, bounds: ImageFrameTab
);
erase_restore_mask = blit_node.eval(erase_restore_mask).await;
}
_ => unreachable!(),
}
}
@@ -335,13 +332,14 @@ async fn brush(_: impl Ctx, image: ImageFrameTable<Color>, bounds: ImageFrameTab
actual_image = blend_executor.eval((actual_image, erase_restore_mask)).await;
}
ImageFrameTable::new(actual_image)
actual_image
}
#[cfg(test)]
mod test {
use super::*;
use graphene_core::raster::Bitmap;
use graphene_core::transform::Transform;
use glam::DAffine2;
@@ -354,4 +352,27 @@ mod test {
// center pixel should be BLACK
assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK));
}
#[tokio::test]
async fn test_brush_output_size() {
let image = brush(
(),
ImageFrameTable::<Color>::default(),
ImageFrameTable::<Color>::default(),
vec![BrushStroke {
trace: vec![crate::vector::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
style: BrushStyle {
color: Color::BLACK,
diameter: 20.,
hardness: 20.,
flow: 20.,
spacing: 20.,
blend_mode: BlendMode::Normal,
},
}],
BrushCache::new_proto(),
)
.await;
assert_eq!(image.width(), 20);
}
}