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

View file

@ -1,6 +1,7 @@
use graph_craft::proto::types::Percentage;
use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
use graphene_core::raster::Image;
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::{Color, Ctx};
use image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage};
@ -9,7 +10,10 @@ use std::cmp::{max, min};
#[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();
let image_frame_transform = image_frame.transform();
let image_frame_alpha_blending = image_frame.one_instance().alpha_blending;
let image_frame = image_frame.one_instance().instance;
// Prepare the image data for processing
let image = &image_frame.image;
@ -30,13 +34,11 @@ async fn dehaze(_: impl Ctx, image_frame: ImageFrameTable<Color>, strength: Perc
base64_string: None,
};
let result = ImageFrame {
image: dehazed_image,
transform: image_frame.transform,
alpha_blending: image_frame.alpha_blending,
};
let mut result = ImageFrameTable::new(ImageFrame { image: dehazed_image });
*result.transform_mut() = image_frame_transform;
*result.one_instance_mut().alpha_blending = *image_frame_alpha_blending;
ImageFrameTable::new(result)
result
}
// There is no real point in modifying these values because they do not change the final result all that much.

View file

@ -6,6 +6,8 @@ use graph_craft::proto::*;
use graphene_core::application_io::ApplicationIo;
use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
use graphene_core::raster::{BlendMode, Image, Pixel};
use graphene_core::transform::Transform;
use graphene_core::transform::TransformMut;
use graphene_core::*;
use wgpu_executor::{Bindgroup, PipelineLayout, Shader, ShaderIO, ShaderInput, WgpuExecutor, WgpuShaderInput};
@ -64,7 +66,7 @@ impl Clone for ComputePass {
#[node_macro::old_node_impl(MapGpuNode)]
async fn map_gpu<'a: 'input>(image: ImageFrameTable<Color>, node: DocumentNode, editor_api: &'a graphene_core::application_io::EditorApi<WasmApplicationIo>) -> ImageFrameTable<Color> {
let image_frame_table = &image;
let image = image.one_item();
let image = image.one_instance().instance;
log::debug!("Executing gpu node");
let executor = &editor_api.application_io.as_ref().and_then(|io| io.gpu_executor()).unwrap();
@ -81,7 +83,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrameTable<Color>, node: DocumentNode,
let name = "placeholder".to_string();
let Ok(compute_pass_descriptor) = create_compute_pass_descriptor(node, image_frame_table, executor).await else {
log::error!("Error creating compute pass descriptor in 'map_gpu()");
return ImageFrameTable::default();
return ImageFrameTable::empty();
};
self.cache.lock().as_mut().unwrap().insert(name, compute_pass_descriptor.clone());
log::error!("created compute pass");
@ -109,18 +111,17 @@ async fn map_gpu<'a: 'input>(image: ImageFrameTable<Color>, node: DocumentNode,
#[cfg(feature = "image-compare")]
log::debug!("score: {:?}", score.score);
let result = ImageFrame {
image: Image {
data: colors,
width: image.image.width,
height: image.image.height,
..Default::default()
},
transform: image.transform,
alpha_blending: image.alpha_blending,
let new_image = Image {
data: colors,
width: image.image.width,
height: image.image.height,
..Default::default()
};
let mut result = ImageFrameTable::new(ImageFrame { image: new_image });
*result.transform_mut() = image_frame_table.transform();
*result.one_instance_mut().alpha_blending = *image_frame_table.one_instance().alpha_blending;
ImageFrameTable::new(result)
result
}
impl<Node, EditorApi> MapGpuNode<Node, EditorApi> {
@ -138,7 +139,7 @@ where
GraphicElement: From<ImageFrame<T>>,
T::Static: Pixel,
{
let image = image.one_item();
let image = image.one_instance().instance;
let compiler = graph_craft::graphene_compiler::Compiler {};
let inner_network = NodeNetwork::value_network(node);
@ -280,14 +281,19 @@ where
#[node_macro::node(category("Debug: GPU"))]
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();
let foreground_transform = foreground.transform();
let background_transform = background.transform();
let background_alpha_blending = background.one_instance().alpha_blending;
let foreground = foreground.one_instance().instance;
let background = background.one_instance().instance;
let foreground_size = DVec2::new(foreground.image.width as f64, foreground.image.height as f64);
let background_size = DVec2::new(background.image.width as f64, background.image.height as f64);
// Transforms a point from the background image to the foreground image
let bg_to_fg = DAffine2::from_scale(foreground_size) * foreground.transform.inverse() * background.transform * DAffine2::from_scale(1. / background_size);
let bg_to_fg = DAffine2::from_scale(foreground_size) * foreground_transform.inverse() * background_transform * DAffine2::from_scale(1. / background_size);
let transform_matrix: Mat2 = bg_to_fg.matrix2.as_mat2();
let translation: Vec2 = bg_to_fg.translation.as_vec2();
@ -334,7 +340,7 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
let proto_networks: Result<Vec<_>, _> = compiler.compile(network.clone()).collect();
let Ok(proto_networks_result) = proto_networks else {
log::error!("Error compiling network in 'blend_gpu_image()");
return ImageFrameTable::default();
return ImageFrameTable::empty();
};
let proto_networks = proto_networks_result;
log::debug!("compiling shader");
@ -444,16 +450,16 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
let result = executor.read_output_buffer(readback_buffer).await.unwrap();
let colors = bytemuck::pod_collect_to_vec::<u8, Color>(result.as_slice());
let result = ImageFrame {
image: Image {
data: colors,
width: background.image.width,
height: background.image.height,
..Default::default()
},
transform: background.transform,
alpha_blending: background.alpha_blending,
let created_image = Image {
data: colors,
width: background.image.width,
height: background.image.height,
..Default::default()
};
ImageFrameTable::new(result)
let mut result = ImageFrameTable::new(ImageFrame { image: created_image });
*result.transform_mut() = background_transform;
*result.one_instance_mut().alpha_blending = *background_alpha_blending;
result
}

View file

@ -16,7 +16,7 @@ async fn image_color_palette(
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.one_item();
let image = image.one_instance().instance;
for pixel in image.image.data.iter() {
let r = pixel.r() * GRID;
@ -79,7 +79,6 @@ mod test {
data: vec![Color::from_rgbaf32(0., 0., 0., 1.).unwrap(); 10000],
base64_string: None,
},
..Default::default()
}),
1,
);

View file

@ -327,7 +327,7 @@ pub async fn imaginate<'a, P: Pixel>(
set_progress(ImaginateStatus::Failed(err.to_string()));
}
};
Image::empty()
Image::default()
})
}

View file

@ -5,8 +5,8 @@ use graphene_core::raster::{
Alpha, AlphaMut, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, Image, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue,
Sample,
};
use graphene_core::transform::Transform;
use graphene_core::{AlphaBlending, Color, Ctx, ExtractFootprint, Node};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::{AlphaBlending, Color, Ctx, ExtractFootprint, GraphicElement, Node};
use fastnoise_lite;
use glam::{DAffine2, DVec2, Vec2};
@ -30,7 +30,10 @@ impl From<std::io::Error> for Error {
#[node_macro::node(category("Debug: Raster"))]
fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFrameTable<Color>) -> ImageFrameTable<Color> {
let image_frame = image_frame.one_item();
let image_frame_transform = image_frame.transform();
let image_frame_alpha_blending = image_frame.one_instance().alpha_blending;
let image_frame = image_frame.one_instance().instance;
// Resize the image using the image crate
let image = &image_frame.image;
@ -38,7 +41,7 @@ fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFra
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 image_bounds = Bbox::from_transform(image_frame_transform).to_axis_aligned_bbox();
let intersection = viewport_bounds.intersect(&image_bounds);
let image_size = DAffine2::from_scale(DVec2::new(image.width as f64, image.height as f64));
let size = intersection.size();
@ -46,7 +49,7 @@ fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFra
// If the image would not be visible, return an empty image
if size.x <= 0. || size.y <= 0. {
return ImageFrameTable::default();
return ImageFrameTable::empty();
}
let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, data).expect("Failed to convert internal image format into image-rs data type.");
@ -81,15 +84,13 @@ fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFra
};
// we need to adjust the offset if we truncate the offset calculation
let new_transform = image_frame.transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
let result = ImageFrame {
image,
transform: new_transform,
alpha_blending: image_frame.alpha_blending,
};
let mut result = ImageFrameTable::new(ImageFrame { image });
*result.transform_mut() = new_transform;
*result.one_instance_mut().alpha_blending = *image_frame_alpha_blending;
ImageFrameTable::new(result)
result
}
#[derive(Debug, Clone, Copy)]
@ -256,33 +257,35 @@ fn mask_image<
// }
#[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>
async fn blend_image_tuple<_P, MapFn, _Fg>(images: (ImageFrameTable<_P>, _Fg), map_fn: &'n MapFn) -> ImageFrameTable<_P>
where
_P: Alpha + Pixel + Debug + Send + dyn_any::StaticType,
_P::Static: Pixel,
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'n + Clone,
_Fg: Sample<Pixel = _P> + Transform + Clone + Send + 'n,
GraphicElement: From<ImageFrame<_P>>,
{
let (background, foreground) = images;
blend_image(foreground, background, map_fn)
}
fn blend_image<'input, _P: Alpha + Pixel + Debug, MapFn, Frame: Sample<Pixel = _P> + Transform, Background: BitmapMut<Pixel = _P> + Transform + Sample<Pixel = _P>>(
foreground: Frame,
background: Background,
map_fn: &'input MapFn,
) -> Background
fn blend_image<'input, _P, MapFn, Frame, Background>(foreground: Frame, background: Background, map_fn: &'input MapFn) -> Background
where
MapFn: Node<'input, (_P, _P), Output = _P>,
_P: Pixel + Alpha + Debug,
Frame: Sample<Pixel = _P> + Transform,
Background: BitmapMut<Pixel = _P> + Sample<Pixel = _P> + Transform,
{
blend_image_closure(foreground, background, |a, b| map_fn.eval((a, b)))
}
pub fn blend_image_closure<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample<Pixel = _P> + Transform, Background: BitmapMut<Pixel = _P> + Transform + Sample<Pixel = _P>>(
foreground: Frame,
mut background: Background,
map_fn: MapFn,
) -> Background
pub fn blend_image_closure<_P, MapFn, Frame, Background>(foreground: Frame, mut background: Background, map_fn: MapFn) -> Background
where
MapFn: Fn(_P, _P) -> _P,
_P: Pixel + Alpha + Debug,
Frame: Sample<Pixel = _P> + Transform,
Background: BitmapMut<Pixel = _P> + Sample<Pixel = _P> + Transform,
{
let background_size = DVec2::new(background.width() as f64, background.height() as f64);
@ -319,19 +322,20 @@ pub struct ExtendImageToBoundsNode<Bounds> {
}
#[node_macro::old_node_fn(ExtendImageToBoundsNode)]
fn extend_image_to_bounds(image: ImageFrame<Color>, bounds: DAffine2) -> ImageFrame<Color> {
fn extend_image_to_bounds(image: ImageFrameTable<Color>, bounds: DAffine2) -> ImageFrameTable<Color> {
let image_aabb = Bbox::unit().affine_transform(image.transform()).to_axis_aligned_bbox();
let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox();
if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) {
return image;
}
if image.image.width == 0 || image.image.height == 0 {
let image_instance = image.one_instance().instance;
if image_instance.image.width == 0 || image_instance.image.height == 0 {
return empty_image((), bounds, Color::TRANSPARENT);
}
let orig_image_scale = DVec2::new(image.image.width as f64, image.image.height as f64);
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * image.transform.inverse();
let orig_image_scale = DVec2::new(image_instance.image.width as f64, image_instance.image.height as f64);
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * image.transform().inverse();
let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox();
let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO);
@ -341,36 +345,37 @@ fn extend_image_to_bounds(image: ImageFrame<Color>, bounds: DAffine2) -> ImageFr
// Copy over original image into enlarged image.
let mut new_img = Image::new(new_scale.x as u32, new_scale.y as u32, Color::TRANSPARENT);
let offset_in_new_image = (-new_start).as_uvec2();
for y in 0..image.image.height {
let old_start = y * image.image.width;
for y in 0..image_instance.image.height {
let old_start = y * image_instance.image.width;
let new_start = (y + offset_in_new_image.y) * new_img.width + offset_in_new_image.x;
let old_row = &image.image.data[old_start as usize..(old_start + image.image.width) as usize];
let new_row = &mut new_img.data[new_start as usize..(new_start + image.image.width) as usize];
let old_row = &image_instance.image.data[old_start as usize..(old_start + image_instance.image.width) as usize];
let new_row = &mut new_img.data[new_start as usize..(new_start + image_instance.image.width) as usize];
new_row.copy_from_slice(old_row);
}
// Compute new transform.
// let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse();
let new_texture_to_layer_space = image.transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
ImageFrame {
image: new_img,
transform: new_texture_to_layer_space,
alpha_blending: image.alpha_blending,
}
let new_texture_to_layer_space = image.transform() * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
let mut result = ImageFrameTable::new(ImageFrame { image: new_img });
*result.transform_mut() = new_texture_to_layer_space;
*result.one_instance_mut().alpha_blending = *image.one_instance().alpha_blending;
result
}
#[node_macro::node(category("Debug: Raster"))]
fn empty_image<P: Pixel>(_: impl Ctx, transform: DAffine2, #[implementations(Color)] color: P) -> ImageFrame<P> {
fn empty_image(_: impl Ctx, transform: DAffine2, color: Color) -> ImageFrameTable<Color> {
let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
let image = Image::new(width, height, color);
ImageFrame {
image,
transform,
alpha_blending: AlphaBlending::default(),
}
let mut result = ImageFrameTable::new(ImageFrame { image });
*result.transform_mut() = transform;
*result.one_instance_mut().alpha_blending = AlphaBlending::default();
result
}
// #[cfg(feature = "serde")]
@ -510,7 +515,7 @@ fn noise_pattern(
// If the image would not be visible, return an empty image
if size.x <= 0. || size.y <= 0. {
return ImageFrameTable::default();
return ImageFrameTable::empty();
}
let footprint_scale = footprint.scale();
@ -554,13 +559,11 @@ fn noise_pattern(
}
}
let result = ImageFrame {
image,
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
alpha_blending: AlphaBlending::default(),
};
let mut result = ImageFrameTable::new(ImageFrame { image });
*result.transform_mut() = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
*result.one_instance_mut().alpha_blending = AlphaBlending::default();
return ImageFrameTable::new(result);
return result;
}
};
noise.set_noise_type(Some(noise_type));
@ -618,13 +621,11 @@ fn noise_pattern(
}
}
let result = ImageFrame {
image,
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
alpha_blending: AlphaBlending::default(),
};
let mut result = ImageFrameTable::new(ImageFrame { image });
*result.transform_mut() = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
*result.one_instance_mut().alpha_blending = AlphaBlending::default();
ImageFrameTable::new(result)
result
}
#[node_macro::node(category("Raster"))]
@ -640,7 +641,7 @@ fn mandelbrot(ctx: impl ExtractFootprint + Send) -> ImageFrameTable<Color> {
// If the image would not be visible, return an empty image
if size.x <= 0. || size.y <= 0. {
return ImageFrameTable::default();
return ImageFrameTable::empty();
}
let scale = footprint.scale();
@ -662,18 +663,17 @@ fn mandelbrot(ctx: impl ExtractFootprint + Send) -> ImageFrameTable<Color> {
}
}
let result = ImageFrame {
image: Image {
width,
height,
data,
..Default::default()
},
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
alpha_blending: Default::default(),
let image = Image {
width,
height,
data,
..Default::default()
};
let mut result = ImageFrameTable::new(ImageFrame { image });
*result.transform_mut() = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
*result.one_instance_mut().alpha_blending = Default::default();
ImageFrameTable::new(result)
result
}
#[inline(always)]

View file

@ -1,8 +1,9 @@
use bezier_rs::{ManipulatorGroup, Subpath};
use graphene_core::transform::Transform;
use graphene_core::transform::TransformMut;
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, Ctx, GraphicElement, GraphicGroupTable};
pub use path_bool as path_bool_lib;
use path_bool::{FillRule, PathBooleanOperation};
@ -12,7 +13,7 @@ use std::ops::Mul;
#[node_macro::node(category(""))]
async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, operation: BooleanOperation) -> VectorDataTable {
fn vector_from_image<T: Transform>(image_frame: T) -> VectorData {
fn vector_from_image<T: Transform>(image_frame: T) -> VectorDataTable {
let corner1 = DVec2::ZERO;
let corner2 = DVec2::new(1., 1.);
@ -22,18 +23,14 @@ async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, opera
let mut vector_data = VectorData::from_subpath(subpath);
vector_data.style.set_fill(Fill::Solid(Color::from_rgb_str("777777").unwrap().to_gamma_srgb()));
vector_data
VectorDataTable::new(vector_data)
}
fn union_vector_data(graphic_element: &GraphicElement) -> VectorData {
fn union_vector_data(graphic_element: &GraphicElement) -> VectorDataTable {
match graphic_element {
GraphicElement::VectorData(vector_data) => {
let vector_data = vector_data.one_item();
vector_data.clone()
}
GraphicElement::VectorData(vector_data) => vector_data.clone(),
// Union all vector data in the graphic group into a single vector
GraphicElement::GraphicGroup(graphic_group) => {
let graphic_group = graphic_group.one_item();
let vector_data = collect_vector_data(graphic_group);
boolean_operation_on_vector_data(&vector_data, BooleanOperation::Union)
@ -42,28 +39,32 @@ async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, opera
}
}
fn collect_vector_data(graphic_group: &GraphicGroup) -> Vec<VectorData> {
fn collect_vector_data(graphic_group_table: &GraphicGroupTable) -> Vec<VectorDataTable> {
let graphic_group = graphic_group_table.one_instance();
// Ensure all non vector data in the graphic group is converted to vector data
let vector_data = graphic_group.iter().map(|(element, _)| union_vector_data(element));
let vector_data_tables = graphic_group.instance.iter().map(|(element, _)| union_vector_data(element));
// Apply the transform from the parent graphic group
let transformed_vector_data = vector_data.map(|mut vector_data| {
vector_data.transform = graphic_group.transform * vector_data.transform;
vector_data
let transformed_vector_data = vector_data_tables.map(|mut vector_data_table| {
*vector_data_table.transform_mut() = graphic_group.transform() * vector_data_table.transform();
vector_data_table
});
transformed_vector_data.collect::<Vec<_>>()
}
fn subtract<'a>(vector_data: impl Iterator<Item = &'a VectorData>) -> VectorData {
fn subtract<'a>(vector_data: impl Iterator<Item = &'a VectorDataTable>) -> VectorDataTable {
let mut vector_data = vector_data.into_iter();
let mut result = vector_data.next().cloned().unwrap_or_default();
let mut next_vector_data = vector_data.next();
while let Some(lower_vector_data) = next_vector_data {
let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform;
let transform_of_lower_into_space_of_upper = result.transform().inverse() * lower_vector_data.transform();
let upper_path_string = to_path(&result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data, transform_of_lower_into_space_of_upper);
let result = result.one_instance_mut().instance;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.one_instance().instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_subtract(upper_path_string, lower_path_string) };
@ -76,49 +77,58 @@ async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, opera
next_vector_data = vector_data.next();
}
result
}
fn boolean_operation_on_vector_data(vector_data: &[VectorData], boolean_operation: BooleanOperation) -> VectorData {
fn boolean_operation_on_vector_data(vector_data_table: &[VectorDataTable], boolean_operation: BooleanOperation) -> VectorDataTable {
match boolean_operation {
BooleanOperation::Union => {
// Reverse vector data so that the result style is the style of the first vector data
let mut vector_data = vector_data.iter().rev();
let mut result = vector_data.next().cloned().unwrap_or_default();
let mut second_vector_data = Some(vector_data.next().unwrap_or(const { &VectorData::empty() }));
let mut vector_data_table = vector_data_table.iter().rev();
let mut result_vector_data_table = vector_data_table.next().cloned().unwrap_or_default();
// Loop over all vector data and union it with the result
let default = VectorDataTable::default();
let mut second_vector_data = Some(vector_data_table.next().unwrap_or(&default));
while let Some(lower_vector_data) = second_vector_data {
let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform;
let transform_of_lower_into_space_of_upper = result_vector_data_table.transform().inverse() * lower_vector_data.transform();
let upper_path_string = to_path(&result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data, transform_of_lower_into_space_of_upper);
let result_vector_data = result_vector_data_table.one_instance_mut().instance;
let upper_path_string = to_path(result_vector_data, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.one_instance().instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_union(upper_path_string, lower_path_string) };
let boolean_operation_result = from_path(&boolean_operation_string);
result.colinear_manipulators = boolean_operation_result.colinear_manipulators;
result.point_domain = boolean_operation_result.point_domain;
result.segment_domain = boolean_operation_result.segment_domain;
result.region_domain = boolean_operation_result.region_domain;
second_vector_data = vector_data.next();
result_vector_data.colinear_manipulators = boolean_operation_result.colinear_manipulators;
result_vector_data.point_domain = boolean_operation_result.point_domain;
result_vector_data.segment_domain = boolean_operation_result.segment_domain;
result_vector_data.region_domain = boolean_operation_result.region_domain;
second_vector_data = vector_data_table.next();
}
result
result_vector_data_table
}
BooleanOperation::SubtractFront => subtract(vector_data.iter()),
BooleanOperation::SubtractBack => subtract(vector_data.iter().rev()),
BooleanOperation::SubtractFront => subtract(vector_data_table.iter()),
BooleanOperation::SubtractBack => subtract(vector_data_table.iter().rev()),
BooleanOperation::Intersect => {
let mut vector_data = vector_data.iter().rev();
let mut vector_data = vector_data_table.iter().rev();
let mut result = vector_data.next().cloned().unwrap_or_default();
let mut second_vector_data = Some(vector_data.next().unwrap_or(const { &VectorData::empty() }));
let default = VectorDataTable::default();
let mut second_vector_data = Some(vector_data.next().unwrap_or(&default));
// For each vector data, set the result to the intersection of that data and the result
while let Some(lower_vector_data) = second_vector_data {
let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform;
let transform_of_lower_into_space_of_upper = result.transform().inverse() * lower_vector_data.transform();
let upper_path_string = to_path(&result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data, transform_of_lower_into_space_of_upper);
let result = result.one_instance_mut().instance;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.one_instance().instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) };
@ -130,63 +140,67 @@ async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, opera
result.region_domain = boolean_operation_result.region_domain;
second_vector_data = vector_data.next();
}
result
}
BooleanOperation::Difference => {
let mut vector_data_iter = vector_data.iter().rev();
let mut any_intersection = VectorData::empty();
let mut second_vector_data = Some(vector_data_iter.next().unwrap_or(const { &VectorData::empty() }));
let mut vector_data_iter = vector_data_table.iter().rev();
let mut any_intersection = VectorDataTable::default();
let default = VectorDataTable::default();
let mut second_vector_data = Some(vector_data_iter.next().unwrap_or(&default));
// Find where all vector data intersect at least once
while let Some(lower_vector_data) = second_vector_data {
let all_other_vector_data = boolean_operation_on_vector_data(&vector_data.iter().filter(|v| v != &lower_vector_data).cloned().collect::<Vec<_>>(), BooleanOperation::Union);
let all_other_vector_data = boolean_operation_on_vector_data(&vector_data_table.iter().filter(|v| v != &lower_vector_data).cloned().collect::<Vec<_>>(), BooleanOperation::Union);
let all_other_vector_data_instance = all_other_vector_data.one_instance();
let transform_of_lower_into_space_of_upper = all_other_vector_data.transform.inverse() * lower_vector_data.transform;
let transform_of_lower_into_space_of_upper = all_other_vector_data.transform().inverse() * lower_vector_data.transform();
let upper_path_string = to_path(&all_other_vector_data, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data, transform_of_lower_into_space_of_upper);
let upper_path_string = to_path(all_other_vector_data_instance.instance, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.one_instance().instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_intersection_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) };
let mut boolean_intersection_result = from_path(&boolean_intersection_string);
let mut boolean_intersection_result = VectorDataTable::new(from_path(&boolean_intersection_string));
*boolean_intersection_result.transform_mut() = all_other_vector_data_instance.transform();
boolean_intersection_result.transform = all_other_vector_data.transform;
boolean_intersection_result.style = all_other_vector_data.style.clone();
boolean_intersection_result.alpha_blending = all_other_vector_data.alpha_blending;
boolean_intersection_result.one_instance_mut().instance.style = all_other_vector_data_instance.instance.style.clone();
*boolean_intersection_result.one_instance_mut().alpha_blending = *all_other_vector_data_instance.alpha_blending;
let transform_of_lower_into_space_of_upper = boolean_intersection_result.transform.inverse() * any_intersection.transform;
let transform_of_lower_into_space_of_upper = boolean_intersection_result.one_instance_mut().transform().inverse() * any_intersection.transform();
let upper_path_string = to_path(&boolean_intersection_result, DAffine2::IDENTITY);
let lower_path_string = to_path(&any_intersection, transform_of_lower_into_space_of_upper);
let upper_path_string = to_path(boolean_intersection_result.one_instance_mut().instance, DAffine2::IDENTITY);
let lower_path_string = to_path(any_intersection.one_instance_mut().instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let union_result = from_path(&unsafe { boolean_union(upper_path_string, lower_path_string) });
any_intersection = union_result;
*any_intersection.one_instance_mut().instance = union_result;
any_intersection.transform = boolean_intersection_result.transform;
any_intersection.style = boolean_intersection_result.style.clone();
any_intersection.alpha_blending = boolean_intersection_result.alpha_blending;
*any_intersection.transform_mut() = boolean_intersection_result.transform();
any_intersection.one_instance_mut().instance.style = boolean_intersection_result.one_instance_mut().instance.style.clone();
any_intersection.one_instance_mut().alpha_blending = boolean_intersection_result.one_instance_mut().alpha_blending;
second_vector_data = vector_data_iter.next();
}
// Subtract the area where they intersect at least once from the union of all vector data
let union = boolean_operation_on_vector_data(vector_data, BooleanOperation::Union);
let union = boolean_operation_on_vector_data(vector_data_table, BooleanOperation::Union);
boolean_operation_on_vector_data(&[union, any_intersection], BooleanOperation::SubtractFront)
}
}
}
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);
let mut result_vector_data_table = boolean_operation_on_vector_data(&collect_vector_data(&group_of_paths), operation);
let transform = boolean_operation_result.transform;
VectorData::transform(&mut boolean_operation_result, transform);
boolean_operation_result.style.set_stroke_transform(DAffine2::IDENTITY);
boolean_operation_result.transform = DAffine2::IDENTITY;
boolean_operation_result.upstream_graphic_group = Some(GraphicGroupTable::new(group_of_paths.clone()));
// Replace the transformation matrix with a mutation of the vector points themselves
let result_vector_data_table_transform = result_vector_data_table.transform();
*result_vector_data_table.transform_mut() = DAffine2::IDENTITY;
let result_vector_data = result_vector_data_table.one_instance_mut().instance;
VectorData::transform(result_vector_data, result_vector_data_table_transform);
result_vector_data.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector_data.upstream_graphic_group = Some(group_of_paths.clone());
VectorDataTable::new(boolean_operation_result)
result_vector_data_table
}
fn to_path(vector: &VectorData, transform: DAffine2) -> Vec<path_bool::PathSegment> {

View file

@ -11,6 +11,8 @@ use graphene_core::raster::Image;
use graphene_core::renderer::RenderMetadata;
use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, RenderSvgSegmentList, SvgRender};
use graphene_core::transform::Footprint;
#[cfg(target_arch = "wasm32")]
use graphene_core::transform::TransformMut;
use graphene_core::vector::VectorDataTable;
use graphene_core::{Color, Context, Ctx, ExtractFootprint, GraphicGroupTable, OwnedContextImpl, WasmNotSend};
@ -40,7 +42,7 @@ async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc<W
// image: ImageFrameTable<graphene_core::raster::SRGBA8>,
// surface_handle: Arc<WasmSurfaceHandle>,
// ) -> graphene_core::application_io::SurfaceHandleFrame<HtmlCanvasElement> {
// let image = image.one_item();
// let image = image.one_instance().instance;
// let image_data = image.image.data;
// let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice()));
// if image.image.width > 0 && image.image.height > 0 {
@ -76,7 +78,7 @@ async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")]
#[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();
return ImageFrameTable::empty();
};
let image = image.to_rgba32f();
let image = ImageFrame {
@ -86,8 +88,6 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> ImageFrameTable<Color> {
height: image.height(),
..Default::default()
},
transform: glam::DAffine2::IDENTITY,
alpha_blending: Default::default(),
};
ImageFrameTable::new(image)
@ -130,7 +130,7 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen
let mut child = Scene::new();
let mut context = wgpu_executor::RenderContext::default();
data.render_to_vello(&mut child, glam::DAffine2::IDENTITY, &mut context);
data.render_to_vello(&mut child, Default::default(), &mut context);
// TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost
scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array())));
@ -167,7 +167,7 @@ async fn rasterize<T: GraphicElementRendered + graphene_core::transform::Transfo
) -> ImageFrameTable<Color> {
if footprint.transform.matrix2.determinant() == 0. {
log::trace!("Invalid footprint received for rasterization");
return ImageFrameTable::default();
return ImageFrameTable::empty();
}
let mut render = SvgRender::new();
@ -204,13 +204,12 @@ async fn rasterize<T: GraphicElementRendered + graphene_core::transform::Transfo
let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap();
let result = ImageFrame {
let mut result = ImageFrameTable::new(ImageFrame {
image: Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32),
transform: footprint.transform,
..Default::default()
};
});
*result.transform_mut() = footprint.transform;
ImageFrameTable::new(result)
result
}
#[node_macro::node(category(""))]
@ -242,8 +241,10 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
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(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();