Instance tables refactor part 6: unwrap VectorData and ImageFrame from single-row to multi-row tables (#2684)

* Start refactoring the boolean operations code

* Switch to iterators in the boolean operations code

* Make boolean operations work on rows of a table, not Vecs of single-row tables

* Remove more .transform()

* Simplify brush code

* Attempt to remove .transform() by using Instance<Image<Color>> in brush code, but a regression is introduced

* Improve blend_image_closure

* Simplify

* Remove leading underscore from type arguments

* Remove .transform() from ImageFrameTable<P> and fix Mask node behavior on stencils not fully overlapping its target image

* Remove more .one_instance_ref()

* Fully remove .one_instance_ref() and improve the 'Combine Channels' node robustness

* Fully remove .once_instance_mut()

* Fix tests

* Remove .one_empty_image()

* Make Instances<T>::default() return an empty table for images, but still not yet vector

---------

Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2025-06-04 20:40:15 -07:00
committed by GitHub
parent 76ecdc8f1b
commit cb4289169d
29 changed files with 882 additions and 811 deletions

View File

@@ -1,14 +1,16 @@
use crate::raster::{BlendImageTupleNode, blend_image_closure, extend_image_to_bounds};
use crate::raster::{empty_image, extend_image_to_bounds};
use glam::{DAffine2, DVec2};
use graph_craft::generic::FnNode;
use graph_craft::proto::FutureWrapperNode;
use graphene_core::instances::Instance;
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::{Image, ImageFrameTable};
use graphene_core::raster::{Alpha, Bitmap, BlendMode, Color, Pixel, Sample};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::value::{ClonedNode, ValueNode};
use graphene_core::raster::{Alpha, BitmapMut, BlendMode, Color, Pixel, Sample};
use graphene_core::renderer::GraphicElementRendered;
use graphene_core::transform::Transform;
use graphene_core::value::ClonedNode;
use graphene_core::vector::VectorDataTable;
use graphene_core::vector::brush_stroke::{BrushStroke, BrushStyle};
use graphene_core::{Ctx, GraphicElement, Node};
@@ -31,12 +33,6 @@ impl<P: Pixel + Alpha> Transform for BrushStampGenerator<P> {
}
}
impl<P: Pixel + Alpha> TransformMut for BrushStampGenerator<P> {
fn transform_mut(&mut self) -> &mut DAffine2 {
&mut self.transform
}
}
impl<P: Pixel + Alpha> Sample for BrushStampGenerator<P> {
type Pixel = P;
@@ -139,93 +135,78 @@ where
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.));
use crate::raster::empty_image;
let blank_texture = empty_image((), transform, Color::TRANSPARENT);
let image = crate::raster::blend_image_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
let blank_texture = empty_image((), transform, Color::TRANSPARENT).instance_iter().next().unwrap_or_default();
let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
image.one_instance_ref().instance.clone()
image.instance
}
macro_rules! inline_blend_funcs {
($bg:ident, $fg:ident, $blend_mode:ident, $opacity:ident, [$($mode:path,)*]) => {
match std::hint::black_box($blend_mode) {
$(
$mode => {
blend_image_closure($fg, $bg, |a, b| blend_colors(a, b, $mode, $opacity))
}
)*
}
};
}
pub fn blend_with_mode(background: ImageFrameTable<Color>, foreground: ImageFrameTable<Color>, blend_mode: BlendMode, opacity: f64) -> ImageFrameTable<Color> {
pub fn blend_with_mode(background: Instance<Image<Color>>, foreground: Instance<Image<Color>>, blend_mode: BlendMode, opacity: f64) -> Instance<Image<Color>> {
let opacity = opacity / 100.;
inline_blend_funcs!(
background,
foreground,
blend_mode,
opacity,
[
// Normal group
BlendMode::Normal,
// Darken group
BlendMode::Darken,
BlendMode::Multiply,
BlendMode::ColorBurn,
BlendMode::LinearBurn,
BlendMode::DarkerColor,
// Lighten group
BlendMode::Lighten,
BlendMode::Screen,
BlendMode::ColorDodge,
BlendMode::LinearDodge,
BlendMode::LighterColor,
// Contrast group
BlendMode::Overlay,
BlendMode::SoftLight,
BlendMode::HardLight,
BlendMode::VividLight,
BlendMode::LinearLight,
BlendMode::PinLight,
BlendMode::HardMix,
// Inversion group
BlendMode::Difference,
BlendMode::Exclusion,
BlendMode::Subtract,
BlendMode::Divide,
// Component group
BlendMode::Hue,
BlendMode::Saturation,
BlendMode::Color,
BlendMode::Luminosity,
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase,
BlendMode::Restore,
BlendMode::MultiplyAlpha,
]
)
match std::hint::black_box(blend_mode) {
// Normal group
BlendMode::Normal => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Normal, opacity)),
// Darken group
BlendMode::Darken => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Darken, opacity)),
BlendMode::Multiply => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Multiply, opacity)),
BlendMode::ColorBurn => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::ColorBurn, opacity)),
BlendMode::LinearBurn => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::LinearBurn, opacity)),
BlendMode::DarkerColor => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::DarkerColor, opacity)),
// Lighten group
BlendMode::Lighten => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Lighten, opacity)),
BlendMode::Screen => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Screen, opacity)),
BlendMode::ColorDodge => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::ColorDodge, opacity)),
BlendMode::LinearDodge => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::LinearDodge, opacity)),
BlendMode::LighterColor => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::LighterColor, opacity)),
// Contrast group
BlendMode::Overlay => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Overlay, opacity)),
BlendMode::SoftLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::SoftLight, opacity)),
BlendMode::HardLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::HardLight, opacity)),
BlendMode::VividLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::VividLight, opacity)),
BlendMode::LinearLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::LinearLight, opacity)),
BlendMode::PinLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::PinLight, opacity)),
BlendMode::HardMix => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::HardMix, opacity)),
// Inversion group
BlendMode::Difference => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Difference, opacity)),
BlendMode::Exclusion => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Exclusion, opacity)),
BlendMode::Subtract => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Subtract, opacity)),
BlendMode::Divide => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Divide, opacity)),
// Component group
BlendMode::Hue => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Hue, opacity)),
BlendMode::Saturation => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Saturation, opacity)),
BlendMode::Color => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Color, opacity)),
BlendMode::Luminosity => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Luminosity, opacity)),
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Erase, opacity)),
BlendMode::Restore => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Restore, opacity)),
BlendMode::MultiplyAlpha => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, opacity)),
}
}
#[node_macro::node(category(""))]
async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: ImageFrameTable<Color>, strokes: Vec<BrushStroke>, cache: BrushCache) -> ImageFrameTable<Color> {
#[node_macro::node(category("Raster"))]
async fn brush(_: impl Ctx, mut image_frame_table: ImageFrameTable<Color>, strokes: Vec<BrushStroke>, cache: BrushCache) -> ImageFrameTable<Color> {
// TODO: Find a way to handle more than one instance
let Some(image_frame_instance) = image_frame_table.instance_ref_iter().next() else {
return ImageFrameTable::default();
};
let image_frame_instance = image_frame_instance.to_instance_cloned();
let [start, end] = image_frame_instance.clone().to_table().bounding_box(DAffine2::IDENTITY, false).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let image_bbox = AxisAlignedBbox { start, end };
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_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 background_bounds = bbox.to_transform();
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_frame_table, &draw_strokes);
let mut brush_plan = cache.compute_brush_plan(image_frame_instance, &draw_strokes);
let mut background_bounds = bbox.to_transform();
// TODO: Find a way to handle more than one instance
let Some(mut actual_image) = extend_image_to_bounds((), brush_plan.background.to_table(), background_bounds).instance_iter().next() else {
return ImageFrameTable::default();
};
// If the bounds are empty (no size on images or det(transform) = 0), keep the target bounds
let bounds_empty = bounds.instance_ref_iter().all(|bounds| bounds.instance.width() == 0 || bounds.instance.height() == 0);
if bounds.transform().matrix2.determinant() != 0. && !bounds_empty {
background_bounds = bounds.transform();
}
let mut actual_image = extend_image_to_bounds((), brush_plan.background, background_bounds);
let final_stroke_idx = brush_plan.strokes.len().saturating_sub(1);
for (idx, stroke) in brush_plan.strokes.into_iter().enumerate() {
// Create brush texture.
@@ -262,14 +243,16 @@ async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: I
);
let blit_target = if idx == 0 {
let target = core::mem::take(&mut brush_plan.first_stroke_texture);
extend_image_to_bounds((), target, stroke_to_layer)
extend_image_to_bounds((), target.to_table(), stroke_to_layer)
} else {
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).await
let instances = blit_node.eval(blit_target).await;
assert_eq!(instances.len(), 1);
instances.instance_iter().next().unwrap_or_default()
};
// Cache image before doing final blend, and store final stroke texture.
@@ -284,9 +267,11 @@ async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: I
let has_erase_strokes = strokes.iter().any(|s| s.style.blend_mode == BlendMode::Erase);
if has_erase_strokes {
let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE);
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();
let mut erase_restore_mask = Instance {
instance: opaque_image,
transform: background_bounds,
..Default::default()
};
for stroke in erase_restore_strokes {
let mut brush_texture = cache.get_cached_brush(&stroke.style);
@@ -306,7 +291,7 @@ async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: I
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(blend_params)),
);
erase_restore_mask = blit_node.eval(erase_restore_mask).await;
erase_restore_mask = blit_node.eval(erase_restore_mask.to_table()).await.instance_iter().next().unwrap_or_default();
}
// Yes, this is essentially the same as the above, but we duplicate to inline the blend mode.
BlendMode::Restore => {
@@ -316,25 +301,87 @@ async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: I
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(blend_params)),
);
erase_restore_mask = blit_node.eval(erase_restore_mask).await;
erase_restore_mask = blit_node.eval(erase_restore_mask.to_table()).await.instance_iter().next().unwrap_or_default();
}
_ => unreachable!(),
}
}
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;
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
}
actual_image
let first_row = image_frame_table.instance_mut_iter().next().unwrap();
*first_row.instance = actual_image.instance;
*first_row.transform = actual_image.transform;
*first_row.alpha_blending = actual_image.alpha_blending;
*first_row.source_node_id = actual_image.source_node_id;
image_frame_table
}
pub fn blend_image_closure(foreground: Instance<Image<Color>>, mut background: Instance<Image<Color>>, map_fn: impl Fn(Color, Color) -> Color) -> Instance<Image<Color>> {
let foreground_size = DVec2::new(foreground.instance.width as f64, foreground.instance.height as f64);
let background_size = DVec2::new(background.instance.width as f64, background.instance.height as f64);
// Transforms a point from the background image to the foreground image
let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground.transform.inverse() * background.transform * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0, 0)..(1, 1) in the background image space
let background_aabb = Bbox::unit().affine_transform(background.transform.inverse() * foreground.transform).to_axis_aligned_bbox();
// Clamp the foreground image to the background image
let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
let end = (background_aabb.end * background_size).min(background_size).as_uvec2();
for y in start.y..end.y {
for x in start.x..end.x {
let background_point = DVec2::new(x as f64, y as f64);
let foreground_point = background_to_foreground.transform_point2(background_point);
let source_pixel = foreground.instance.sample(foreground_point);
let Some(destination_pixel) = background.instance.get_pixel_mut(x, y) else { continue };
*destination_pixel = map_fn(source_pixel, *destination_pixel);
}
}
background
}
pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut background: Instance<Image<Color>>, map_fn: impl Fn(Color, Color) -> Color) -> Instance<Image<Color>> {
let background_size = DVec2::new(background.instance.width as f64, background.instance.height as f64);
// Transforms a point from the background image to the foreground image
let background_to_foreground = background.transform * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0, 0)..(1, 1) in the background image space
let background_aabb = Bbox::unit().affine_transform(background.transform.inverse() * foreground.transform).to_axis_aligned_bbox();
// Clamp the foreground image to the background image
let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
let end = (background_aabb.end * background_size).min(background_size).as_uvec2();
let area = background_to_foreground.transform_point2(DVec2::new(1., 1.)) - background_to_foreground.transform_point2(DVec2::ZERO);
for y in start.y..end.y {
for x in start.x..end.x {
let background_point = DVec2::new(x as f64, y as f64);
let foreground_point = background_to_foreground.transform_point2(background_point);
let Some(source_pixel) = foreground.sample(foreground_point, area) else { continue };
let Some(destination_pixel) = background.instance.get_pixel_mut(x, y) else { continue };
*destination_pixel = map_fn(source_pixel, *destination_pixel);
}
}
background
}
#[cfg(test)]
mod test {
use super::*;
use glam::DAffine2;
use graphene_core::raster::Bitmap;
use graphene_core::transform::Transform;
#[test]
@@ -350,8 +397,7 @@ mod test {
async fn test_brush_output_size() {
let image = brush(
(),
ImageFrameTable::<Color>::default(),
ImageFrameTable::<Color>::default(),
ImageFrameTable::<Color>::new(Image::<Color>::default()),
vec![BrushStroke {
trace: vec![crate::vector::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
style: BrushStyle {
@@ -366,6 +412,6 @@ mod test {
BrushCache::new_proto(),
)
.await;
assert_eq!(image.width(), 20);
assert_eq!(image.instance_ref_iter().next().unwrap().instance.width, 20);
}
}

View File

@@ -19,6 +19,7 @@ async fn blur(
gamma: bool,
) -> ImageFrameTable<Color> {
let mut result_table = ImageFrameTable::empty();
for mut image_instance in image_frame.instance_iter() {
let image = image_instance.instance.clone();
@@ -36,6 +37,7 @@ async fn blur(
image_instance.source_node_id = None;
result_table.push(image_instance);
}
result_table
}

View File

@@ -36,6 +36,7 @@ async fn compile_gpu<'a: 'n>(_: impl Ctx, node: &'a DocumentNode, typing_context
#[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 mut result_table = ImageFrameTable::empty();
for (foreground_instance, mut background_instance) in foreground.instance_iter().zip(background.instance_iter()) {
let foreground_transform = foreground_instance.transform;
let background_transform = background_instance.transform;
@@ -94,7 +95,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::one_empty_image();
return ImageFrameTable::default();
};
let proto_networks = proto_networks_result;
log::debug!("compiling shader");
@@ -215,6 +216,7 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
background_instance.source_node_id = None;
result_table.push(background_instance);
}
result_table
}
@@ -241,7 +243,7 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
// #[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_instance_ref().instance;
// let image = image.instance_ref_iter().next().unwrap().instance;
// log::debug!("Executing gpu node");
// let executor = &editor_api.application_io.as_ref().and_then(|io| io.gpu_executor()).unwrap();
@@ -256,7 +258,7 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
// 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::one_empty_image();
// return ImageFrameTable::default();
// };
// self.cache.lock().as_mut().unwrap().insert(name, compute_pass_descriptor.clone());
// log::error!("created compute pass");
@@ -292,7 +294,7 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
// };
// let mut result = ImageFrameTable::new(new_image);
// *result.transform_mut() = image_frame_table.transform();
// *result.one_instance_mut().alpha_blending = *image_frame_table.one_instance_ref().alpha_blending;
// *result.instance_mut_iter().next().unwrap().alpha_blending = *image_frame_table.instance_ref_iter().next().unwrap().alpha_blending;
// result
// }
@@ -312,7 +314,7 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
// GraphicElement: From<Image<T>>,
// T::Static: Pixel,
// {
// let image = image.one_instance_ref().instance;
// let image = image.instance_ref_iter().next().unwrap().instance;
// let compiler = graph_craft::graphene_compiler::Compiler {};
// let inner_network = NodeNetwork::value_network(node);

View File

@@ -1,13 +1,12 @@
use dyn_any::DynAny;
use fastnoise_lite;
use glam::{DAffine2, DVec2, Vec2};
use graphene_core::instances::Instance;
use graphene_core::raster::bbox::Bbox;
use graphene_core::raster::image::{Image, ImageFrameTable};
use graphene_core::raster::{
Alpha, AlphaMut, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, Sample,
};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::{AlphaBlending, Color, Ctx, ExtractFootprint, GraphicElement, Node};
use graphene_core::raster::{Alpha, AlphaMut, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, Channel, DomainWarpType, FractalType, LinearChannel, Luminance, NoiseType, RGBMut};
use graphene_core::transform::Transform;
use graphene_core::{AlphaBlending, Color, Ctx, ExtractFootprint};
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::fmt::Debug;
@@ -89,172 +88,146 @@ fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFra
result_table.push(image_frame_instance)
}
// TODO: Remove when we've completed part 6 of the instance tables refactor
if result_table.is_empty() {
return ImageFrameTable::one_empty_image();
result_table
}
#[node_macro::node(category("Raster"))]
fn combine_channels(
_: impl Ctx,
_primary: (),
#[expose] red: ImageFrameTable<Color>,
#[expose] green: ImageFrameTable<Color>,
#[expose] blue: ImageFrameTable<Color>,
#[expose] alpha: ImageFrameTable<Color>,
) -> ImageFrameTable<Color> {
let mut result_table = ImageFrameTable::empty();
let max_len = red.len().max(green.len()).max(blue.len()).max(alpha.len());
let red = red.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let green = green.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let blue = blue.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
let alpha = alpha.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
for (((red, green), blue), alpha) in red.zip(green).zip(blue).zip(alpha) {
// Turn any default zero-sized image instances into None
let red = red.filter(|i| i.instance.width > 0 && i.instance.height > 0);
let green = green.filter(|i| i.instance.width > 0 && i.instance.height > 0);
let blue = blue.filter(|i| i.instance.width > 0 && i.instance.height > 0);
let alpha = alpha.filter(|i| i.instance.width > 0 && i.instance.height > 0);
// Get this instance's transform and alpha blending mode from the first non-empty channel
let Some((transform, alpha_blending)) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| (i.transform, i.alpha_blending)) else {
continue;
};
// Get the common width and height of the channels, which must have equal dimensions
let channel_dimensions = [
red.as_ref().map(|r| (r.instance.width, r.instance.height)),
green.as_ref().map(|g| (g.instance.width, g.instance.height)),
blue.as_ref().map(|b| (b.instance.width, b.instance.height)),
alpha.as_ref().map(|a| (a.instance.width, a.instance.height)),
];
if channel_dimensions.iter().all(Option::is_none)
|| channel_dimensions
.iter()
.flatten()
.any(|&(x, y)| channel_dimensions.iter().flatten().any(|&(other_x, other_y)| x != other_x || y != other_y))
{
continue;
}
let Some(&(width, height)) = channel_dimensions.iter().flatten().next() else { continue };
// Create a new image for this instance output
let mut image = Image::new(width, height, Color::TRANSPARENT);
// Iterate over all pixels in the image and set the color channels
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.as_ref().and_then(|r| r.instance.get_pixel(x, y)) {
image_pixel.set_red(r.l().cast_linear_channel());
} else {
image_pixel.set_red(Channel::from_linear(0.));
}
if let Some(g) = green.as_ref().and_then(|g| g.instance.get_pixel(x, y)) {
image_pixel.set_green(g.l().cast_linear_channel());
} else {
image_pixel.set_green(Channel::from_linear(0.));
}
if let Some(b) = blue.as_ref().and_then(|b| b.instance.get_pixel(x, y)) {
image_pixel.set_blue(b.l().cast_linear_channel());
} else {
image_pixel.set_blue(Channel::from_linear(0.));
}
if let Some(a) = alpha.as_ref().and_then(|a| a.instance.get_pixel(x, y)) {
image_pixel.set_alpha(a.l().cast_linear_channel());
} else {
image_pixel.set_alpha(Channel::from_linear(1.));
}
}
}
// Add this instance to the result table
result_table.push(Instance {
instance: image,
transform,
alpha_blending,
source_node_id: None,
});
}
result_table
}
#[node_macro::node(category("Raster"))]
fn combine_channels<_I, Red, Green, Blue, Alpha>(
_: impl Ctx,
_primary: (),
#[implementations(ImageFrameTable<Color>)] red: Red,
#[implementations(ImageFrameTable<Color>)] green: Green,
#[implementations(ImageFrameTable<Color>)] blue: Blue,
#[implementations(ImageFrameTable<Color>)] alpha: Alpha,
) -> ImageFrameTable<Color>
where
_I: Pixel + Luminance,
Red: Bitmap<Pixel = _I>,
Green: Bitmap<Pixel = _I>,
Blue: Bitmap<Pixel = _I>,
Alpha: Bitmap<Pixel = _I>,
{
let dimensions = [red.dim(), green.dim(), blue.dim(), alpha.dim()];
if dimensions.iter().any(|&(x, y)| x == 0 || y == 0) || dimensions.iter().any(|&(x, y)| dimensions.iter().any(|&(other_x, other_y)| x != other_x || y != other_y)) {
return ImageFrameTable::one_empty_image();
}
let mut image = Image::new(red.width(), red.height(), Color::TRANSPARENT);
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());
}
}
}
ImageFrameTable::new(image)
}
#[node_macro::node(category("Raster"))]
fn mask<_P, _S, Input, Stencil>(
fn mask(
_: impl Ctx,
/// The image to be masked.
#[implementations(ImageFrameTable<Color>)]
mut image: Input,
image: ImageFrameTable<Color>,
/// The stencil to be used for masking.
#[implementations(ImageFrameTable<Color>)]
#[expose]
stencil: Stencil,
) -> Input
where
// _P is the color of the input image. It must have an alpha channel because that is going to be modified by the mask.
_P: Alpha,
// _S is the color of the stencil. It must have a luminance channel because that is used to mask the input image.
_S: Luminance,
// Input image
Input: Transform + BitmapMut<Pixel = _P>,
// Stencil
Stencil: Transform + Sample<Pixel = _S>,
{
let image_size = DVec2::new(image.width() as f64, image.height() as f64);
let mask_size = stencil.transform().decompose_scale();
if mask_size == DVec2::ZERO {
stencil: ImageFrameTable<Color>,
) -> ImageFrameTable<Color> {
// TODO: Support multiple stencil instances
let Some(stencil_instance) = stencil.instance_iter().next() else {
// No stencil provided so we return the original image
return image;
}
};
let stencil_size = DVec2::new(stencil_instance.instance.width as f64, stencil_instance.instance.height as f64);
// Transforms a point from the background image to the foreground image
let bg_to_fg = image.transform() * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil.transform().inverse();
let mut result_table = ImageFrameTable::empty();
let area = bg_to_fg.transform_vector2(DVec2::ONE);
for y in 0..image.height() {
for x in 0..image.width() {
let image_point = DVec2::new(x as f64, y as f64);
let mut mask_point = bg_to_fg.transform_point2(image_point);
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
mask_point = stencil.transform().transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
for mut image_instance in image.instance_iter() {
let image_size = DVec2::new(image_instance.instance.width as f64, image_instance.instance.height as f64);
let mask_size = stencil_instance.transform.decompose_scale();
let image_pixel = image.get_pixel_mut(x, y).unwrap();
if let Some(mask_pixel) = stencil.sample(mask_point, area) {
if mask_size == DVec2::ZERO {
continue;
}
// Transforms a point from the background image to the foreground image
let bg_to_fg = image_instance.transform * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil_instance.transform.inverse();
for y in 0..image_instance.instance.height {
for x in 0..image_instance.instance.width {
let image_point = DVec2::new(x as f64, y as f64);
let mask_point = bg_to_fg.transform_point2(image_point);
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
let mask_point = stencil_instance.transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_instance.transform.inverse()).transform_point2(mask_point);
let image_pixel = image_instance.instance.get_pixel_mut(x, y).unwrap();
let mask_pixel = stencil_instance.instance.sample(mask_point);
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
}
}
result_table.push(image_instance);
}
image
}
// #[derive(Debug, Clone, Copy)]
// pub struct BlendImageTupleNode<P, Fg, MapFn> {
// map_fn: MapFn,
// _p: PhantomData<P>,
// _fg: PhantomData<Fg>,
// }
#[node_macro::node(skip_impl)]
async fn blend_image_tuple<_P, MapFn, _Fg>(images: (ImageFrameTable<_P>, _Fg), map_fn: &'n MapFn) -> ImageFrameTable<_P>
where
_P: Alpha + Pixel + Debug + Send,
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'n + Clone,
_Fg: Sample<Pixel = _P> + Transform + Clone + Send + 'n,
GraphicElement: From<Image<_P>>,
{
let (background, foreground) = images;
blend_image(foreground, background, map_fn)
}
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, 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);
// Transforms a point from the background image to the foreground image
let bg_to_fg = background.transform() * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0,0) (1, 1) in the background image space
let bg_aabb = Bbox::unit().affine_transform(background.transform().inverse() * foreground.transform()).to_axis_aligned_bbox();
// Clamp the foreground image to the background image
let start = (bg_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
let end = (bg_aabb.end * background_size).min(background_size).as_uvec2();
let area = bg_to_fg.transform_point2(DVec2::new(1., 1.)) - bg_to_fg.transform_point2(DVec2::ZERO);
for y in start.y..end.y {
for x in start.x..end.x {
let bg_point = DVec2::new(x as f64, y as f64);
let fg_point = bg_to_fg.transform_point2(bg_point);
if let Some(src_pixel) = foreground.sample(fg_point, area) {
if let Some(dst_pixel) = background.get_pixel_mut(x, y) {
*dst_pixel = map_fn(src_pixel, *dst_pixel);
}
}
}
}
background
result_table
}
#[node_macro::node(category(""))]
@@ -469,7 +442,7 @@ fn noise_pattern(
// If the image would not be visible, return an empty image
if size.x <= 0. || size.y <= 0. {
return ImageFrameTable::one_empty_image();
return ImageFrameTable::default();
}
let footprint_scale = footprint.scale();
@@ -513,9 +486,12 @@ fn noise_pattern(
}
}
let mut result = ImageFrameTable::new(image);
*result.transform_mut() = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
*result.one_instance_mut().alpha_blending = AlphaBlending::default();
let mut result = ImageFrameTable::empty();
result.push(Instance {
instance: image,
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
});
return result;
}
@@ -575,9 +551,12 @@ fn noise_pattern(
}
}
let mut result = ImageFrameTable::new(image);
*result.transform_mut() = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
*result.one_instance_mut().alpha_blending = AlphaBlending::default();
let mut result = ImageFrameTable::empty();
result.push(Instance {
instance: image,
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
});
result
}
@@ -595,7 +574,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::one_empty_image();
return ImageFrameTable::default();
}
let scale = footprint.scale();
@@ -623,9 +602,12 @@ fn mandelbrot(ctx: impl ExtractFootprint + Send) -> ImageFrameTable<Color> {
data,
..Default::default()
};
let mut result = ImageFrameTable::new(image);
*result.transform_mut() = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
*result.one_instance_mut().alpha_blending = Default::default();
let mut result = ImageFrameTable::empty();
result.push(Instance {
instance: image,
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
..Default::default()
});
result
}

View File

@@ -1,7 +1,7 @@
use bezier_rs::{ManipulatorGroup, Subpath};
use glam::{DAffine2, DVec2};
use graphene_core::transform::Transform;
use graphene_core::transform::TransformMut;
use graphene_core::RasterFrame;
use graphene_core::instances::{Instance, InstanceRef};
use graphene_core::vector::misc::BooleanOperation;
use graphene_core::vector::style::Fill;
pub use graphene_core::vector::*;
@@ -10,207 +10,245 @@ pub use path_bool as path_bool_lib;
use path_bool::{FillRule, PathBooleanOperation};
use std::ops::Mul;
// TODO: Fix boolean ops to work by removing .transform() and .one_instnace_*() calls,
// TODO: since before we used a Vec of single-row tables and now we use a single table
// TODO: with multiple rows while still assuming a single row for the boolean operations.
#[node_macro::node(category(""))]
async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, operation: BooleanOperation) -> VectorDataTable {
fn flatten_vector_data(graphic_group_table: &GraphicGroupTable) -> Vec<VectorDataTable> {
graphic_group_table
.instance_ref_iter()
.map(|element| match element.instance.clone() {
GraphicElement::VectorData(mut vector_data) => {
// Apply the parent group's transform to each element of vector data
for sub_vector_data in vector_data.instance_mut_iter() {
*sub_vector_data.transform = *element.transform * *sub_vector_data.transform;
}
vector_data
}
GraphicElement::RasterFrame(mut image) => {
// Apply the parent group's transform to each element of raster data
match &mut image {
graphene_core::RasterFrame::ImageFrame(image) => {
for instance in image.instance_mut_iter() {
*instance.transform = *element.transform * *instance.transform;
}
}
graphene_core::RasterFrame::TextureFrame(image) => {
for instance in image.instance_mut_iter() {
*instance.transform = *element.transform * *instance.transform;
}
}
}
// Convert the image frame into a rectangular subpath with the image's transform
let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(image.transform());
// Create a vector data table from the rectangular subpath, with a default black fill
let mut vector_data = VectorData::from_subpath(subpath);
vector_data.style.set_fill(Fill::Solid(Color::BLACK));
VectorDataTable::new(vector_data)
}
GraphicElement::GraphicGroup(mut graphic_group) => {
// Apply the parent group's transform to each element of inner group
for sub_element in graphic_group.instance_mut_iter() {
*sub_element.transform = *element.transform * *sub_element.transform;
}
// Recursively flatten the inner group into vector data
boolean_operation_on_vector_data(&flatten_vector_data(&graphic_group), BooleanOperation::Union)
}
})
.collect()
}
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 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_ref().instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_subtract(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;
next_vector_data = vector_data.next();
}
result
}
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_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_vector_data_table.transform().inverse() * lower_vector_data.transform();
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_ref().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_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_vector_data_table
}
BooleanOperation::SubtractFront => subtract(vector_data_table.iter()),
BooleanOperation::SubtractBack => subtract(vector_data_table.iter().rev()),
BooleanOperation::Intersect => {
let mut vector_data = vector_data_table.iter().rev();
let mut result = vector_data.next().cloned().unwrap_or_default();
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 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_ref().instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_intersect(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
}
BooleanOperation::Difference => {
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_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_ref();
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_instance.instance, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.one_instance_ref().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 = VectorDataTable::new(from_path(&boolean_intersection_string));
*boolean_intersection_result.transform_mut() = *all_other_vector_data_instance.transform;
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.one_instance_mut().transform.inverse() * any_intersection.transform();
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.one_instance_mut().instance = union_result;
*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_table, BooleanOperation::Union);
boolean_operation_on_vector_data(&[union, any_intersection], BooleanOperation::SubtractFront)
}
}
}
// The first index is the bottom of the stack
let mut result_vector_data_table = boolean_operation_on_vector_data(&flatten_vector_data(&group_of_paths), operation);
let mut result_vector_data_table = boolean_operation_on_vector_data_table(flatten_vector_data(&group_of_paths).instance_ref_iter(), operation);
// 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());
if let Some(result_vector_data) = result_vector_data_table.instance_mut_iter().next() {
let transform = *result_vector_data.transform;
*result_vector_data.transform = DAffine2::IDENTITY;
VectorData::transform(result_vector_data.instance, transform);
result_vector_data.instance.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector_data.instance.upstream_graphic_group = Some(group_of_paths.clone());
}
result_vector_data_table
}
fn boolean_operation_on_vector_data_table<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>> + Clone, boolean_operation: BooleanOperation) -> VectorDataTable {
match boolean_operation {
BooleanOperation::Union => union(vector_data),
BooleanOperation::SubtractFront => subtract(vector_data),
BooleanOperation::SubtractBack => subtract(vector_data.rev()),
BooleanOperation::Intersect => intersect(vector_data),
BooleanOperation::Difference => difference(vector_data),
}
}
fn union<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
// Reverse vector data so that the result style is the style of the first vector data
let mut vector_data_reversed = vector_data.rev();
let mut result_vector_data_table = VectorDataTable::empty();
result_vector_data_table.push(vector_data_reversed.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
// Loop over all vector data and union it with the result
let default = Instance::default();
let mut second_vector_data = Some(vector_data_reversed.next().unwrap_or(default.to_instance_ref()));
while let Some(lower_vector_data) = second_vector_data {
let transform_of_lower_into_space_of_upper = first_instance.transform.inverse() * *lower_vector_data.transform;
let result = &mut first_instance.instance;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.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_reversed.next();
}
result_vector_data_table
}
fn subtract<'a>(vector_data: impl Iterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
let mut vector_data = vector_data.into_iter();
let mut result_vector_data_table = VectorDataTable::empty();
result_vector_data_table.push(vector_data.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
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 = first_instance.transform.inverse() * *lower_vector_data.transform;
let result = &mut first_instance.instance;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_subtract(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;
next_vector_data = vector_data.next();
}
result_vector_data_table
}
fn intersect<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
let mut vector_data = vector_data.rev();
let mut result_vector_data_table = VectorDataTable::empty();
result_vector_data_table.push(vector_data.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
let default = Instance::default();
let mut second_vector_data = Some(vector_data.next().unwrap_or(default.to_instance_ref()));
// 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 = first_instance.transform.inverse() * *lower_vector_data.transform;
let result = &mut first_instance.instance;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.instance, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let boolean_operation_string = unsafe { boolean_intersect(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_table
}
fn difference<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>> + Clone) -> VectorDataTable {
let mut vector_data_iter = vector_data.clone().rev();
let mut any_intersection = Instance::default();
let default = Instance::default();
let mut second_vector_data = Some(vector_data_iter.next().unwrap_or(default.to_instance_ref()));
// Find where all vector data intersect at least once
while let Some(lower_vector_data) = second_vector_data {
let filtered_vector_data = vector_data.clone().filter(|v| *v != lower_vector_data).collect::<Vec<_>>().into_iter();
let unioned = boolean_operation_on_vector_data_table(filtered_vector_data, BooleanOperation::Union);
let first_instance = unioned.instance_ref_iter().next().expect("Expected at least one instance after the boolean union");
let transform_of_lower_into_space_of_upper = first_instance.transform.inverse() * *lower_vector_data.transform;
let upper_path_string = to_path(first_instance.instance, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector_data.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 instance = from_path(&boolean_intersection_string);
instance.style = first_instance.instance.style.clone();
let boolean_intersection_result = Instance {
instance,
transform: *first_instance.transform,
alpha_blending: *first_instance.alpha_blending,
source_node_id: *first_instance.source_node_id,
};
let transform_of_lower_into_space_of_upper = boolean_intersection_result.transform.inverse() * any_intersection.transform;
let upper_path_string = to_path(&boolean_intersection_result.instance, DAffine2::IDENTITY);
let lower_path_string = to_path(&any_intersection.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.instance = union_result;
any_intersection.transform = boolean_intersection_result.transform;
any_intersection.instance.style = boolean_intersection_result.instance.style.clone();
any_intersection.alpha_blending = boolean_intersection_result.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_table(vector_data, BooleanOperation::Union);
boolean_operation_on_vector_data_table(union.instance_ref_iter().chain(std::iter::once(any_intersection.to_instance_ref())), BooleanOperation::SubtractFront)
}
fn flatten_vector_data(graphic_group_table: &GraphicGroupTable) -> VectorDataTable {
let mut result_table = VectorDataTable::empty();
for element in graphic_group_table.instance_ref_iter() {
match element.instance.clone() {
GraphicElement::VectorData(vector_data) => {
// Apply the parent group's transform to each element of vector data
for mut sub_vector_data in vector_data.instance_iter() {
sub_vector_data.transform = *element.transform * sub_vector_data.transform;
result_table.push(sub_vector_data);
}
}
GraphicElement::RasterFrame(image) => {
let make_instance = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector data table row from the rectangular subpath, with a default black fill
let mut instance = VectorData::from_subpath(subpath);
instance.style.set_fill(Fill::Solid(Color::BLACK));
Instance { instance, ..Default::default() }
};
// Apply the parent group's transform to each element of raster data
match image {
RasterFrame::ImageFrame(image) => {
for instance in image.instance_ref_iter() {
result_table.push(make_instance(*element.transform * *instance.transform));
}
}
RasterFrame::TextureFrame(image) => {
for instance in image.instance_ref_iter() {
result_table.push(make_instance(*element.transform * *instance.transform));
}
}
}
}
GraphicElement::GraphicGroup(mut graphic_group) => {
// Apply the parent group's transform to each element of inner group
for sub_element in graphic_group.instance_mut_iter() {
*sub_element.transform = *element.transform * *sub_element.transform;
}
// Recursively flatten the inner group into vector data
let unioned = boolean_operation_on_vector_data_table(flatten_vector_data(&graphic_group).instance_ref_iter(), BooleanOperation::Union);
for element in unioned.instance_iter() {
result_table.push(element);
}
}
}
}
result_table
}
fn to_path(vector: &VectorData, transform: DAffine2) -> Vec<path_bool::PathSegment> {
let mut path = Vec::new();
for subpath in vector.stroke_bezier_paths() {

View File

@@ -12,8 +12,6 @@ use graphene_core::raster::image::{Image, ImageFrameTable};
use graphene_core::renderer::RenderMetadata;
use graphene_core::renderer::{GraphicElementRendered, RenderParams, RenderSvgSegmentList, SvgRender, format_transform_matrix};
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};
@@ -44,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_instance_ref().instance;
// let image = image.instance_ref_iter().next().unwrap().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 {
@@ -80,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::one_empty_image();
return ImageFrameTable::default();
};
let image = image.to_rgba32f();
let image = Image {
@@ -177,6 +175,8 @@ async fn rasterize<T: WasmNotSend + 'n>(
where
Instances<T>: GraphicElementRendered,
{
use graphene_core::instances::Instance;
if footprint.transform.matrix2.determinant() == 0. {
log::trace!("Invalid footprint received for rasterization");
return ImageFrameTable::empty();
@@ -218,8 +218,12 @@ where
let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap();
let mut result = ImageFrameTable::new(Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32));
*result.transform_mut() = footprint.transform;
let mut result = ImageFrameTable::empty();
result.push(Instance {
instance: Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32),
transform: footprint.transform,
..Default::default()
});
result
}