mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 02:48:12 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user