Remove the old node macro and fix/clean up several raster nodes (#2650)

* Fix several broken raster nodes and clean up leftover old node system code

* Migrate Brightness/Contrast to the new node macro, and fix it

* Remove last usages of old_node_fn

* Remove old_node_fn
This commit is contained in:
Keavon Chambers
2025-05-17 21:24:32 -07:00
committed by GitHub
parent 77f8bfd9ed
commit a8e209e44c
17 changed files with 511 additions and 1423 deletions

View File

@@ -1,4 +1,4 @@
use crate::raster::{BlendImageTupleNode, ExtendImageToBoundsNode, blend_image_closure};
use crate::raster::{BlendImageTupleNode, blend_image_closure, extend_image_to_bounds};
use glam::{DAffine2, DVec2};
use graph_craft::generic::FnNode;
use graph_craft::proto::FutureWrapperNode;
@@ -8,7 +8,7 @@ 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, CopiedNode, ValueNode};
use graphene_core::value::{ClonedNode, ValueNode};
use graphene_core::vector::VectorDataTable;
use graphene_core::vector::brush_stroke::{BrushStroke, BrushStyle};
use graphene_core::{Ctx, GraphicElement, Node};
@@ -225,7 +225,7 @@ async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: I
background_bounds = bounds.transform();
}
let mut actual_image = ExtendImageToBoundsNode::new(ClonedNode::new(background_bounds)).eval(brush_plan.background);
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,7 +262,7 @@ 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);
ExtendImageToBoundsNode::new(CopiedNode::new(stroke_to_layer)).eval(target)
extend_image_to_bounds((), target, stroke_to_layer)
} else {
use crate::raster::empty_image;
empty_image((), stroke_to_layer, Color::TRANSPARENT)

View File

@@ -1,19 +1,15 @@
use crate::wasm_application_io::WasmApplicationIo;
use dyn_any::StaticTypeSized;
use glam::{DAffine2, DVec2, Mat2, Vec2};
use gpu_executor::{ComputePassDimensions, StorageBufferOptions};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::*;
use graph_craft::proto::*;
use graphene_core::application_io::ApplicationIo;
use graphene_core::raster::BlendMode;
use graphene_core::raster::image::{Image, ImageFrameTable};
use graphene_core::raster::{BlendMode, Pixel};
use graphene_core::transform::Transform;
use graphene_core::transform::TransformMut;
use graphene_core::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use wgpu_executor::{Bindgroup, PipelineLayout, Shader, ShaderIO, ShaderInput, WgpuExecutor, WgpuShaderInput};
use std::sync::Arc;
use wgpu_executor::{Bindgroup, PipelineLayout, Shader, ShaderIO, ShaderInput, WgpuExecutor};
// TODO: Move to graph-craft
#[node_macro::node(category("Debug: GPU"))]
@@ -39,240 +35,6 @@ async fn compile_gpu<'a: 'n>(_: impl Ctx, node: &'a DocumentNode, typing_context
Ok(compilation_client::compile(proto_networks, input_types, output_types, io).await.unwrap())
}
pub struct MapGpuNode<Node, EditorApi> {
node: Node,
editor_api: EditorApi,
cache: Mutex<HashMap<String, ComputePass>>,
}
struct ComputePass {
pipeline_layout: PipelineLayout,
readback_buffer: Option<Arc<WgpuShaderInput>>,
}
impl Clone for ComputePass {
fn clone(&self) -> Self {
Self {
pipeline_layout: self.pipeline_layout.clone(),
readback_buffer: self.readback_buffer.clone(),
}
}
}
#[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;
log::debug!("Executing gpu node");
let executor = &editor_api.application_io.as_ref().and_then(|io| io.gpu_executor()).unwrap();
#[cfg(feature = "image-compare")]
let img: image::DynamicImage = image::Rgba32FImage::from_raw(image.width, image.height, bytemuck::cast_vec(image.data.clone())).unwrap().into();
// TODO: The cache should be based on the network topology not the node name
let compute_pass_descriptor = if self.cache.lock().as_ref().unwrap().contains_key("placeholder") {
self.cache.lock().as_ref().unwrap().get("placeholder").unwrap().clone()
} else {
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();
};
self.cache.lock().as_mut().unwrap().insert(name, compute_pass_descriptor.clone());
log::error!("created compute pass");
compute_pass_descriptor
};
let compute_pass = executor
.create_compute_pass(
&compute_pass_descriptor.pipeline_layout,
compute_pass_descriptor.readback_buffer.clone(),
ComputePassDimensions::XY(image.width / 12 + 1, image.height / 8 + 1),
)
.unwrap();
executor.execute_compute_pipeline(compute_pass).unwrap();
log::debug!("executed pipeline");
log::debug!("reading buffer");
let result = executor.read_output_buffer(compute_pass_descriptor.readback_buffer.clone().unwrap()).await.unwrap();
let colors = bytemuck::pod_collect_to_vec::<u8, Color>(result.as_slice());
log::debug!("first color: {:?}", colors[0]);
#[cfg(feature = "image-compare")]
let img2: image::DynamicImage = image::Rgba32FImage::from_raw(image.width, image.height, bytemuck::cast_vec(colors.clone())).unwrap().into();
#[cfg(feature = "image-compare")]
let score = image_compare::rgb_hybrid_compare(&img.into_rgb8(), &img2.into_rgb8()).unwrap();
#[cfg(feature = "image-compare")]
log::debug!("score: {:?}", score.score);
let new_image = Image {
data: colors,
width: image.width,
height: image.height,
..Default::default()
};
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
}
impl<Node, EditorApi> MapGpuNode<Node, EditorApi> {
pub fn new(node: Node, editor_api: EditorApi) -> Self {
Self {
node,
editor_api,
cache: Mutex::new(HashMap::new()),
}
}
}
async fn create_compute_pass_descriptor<T: Clone + Pixel + StaticTypeSized>(node: DocumentNode, image: &ImageFrameTable<T>, executor: &&WgpuExecutor) -> Result<ComputePass, String>
where
GraphicElement: From<Image<T>>,
T::Static: Pixel,
{
let image = image.one_instance_ref().instance;
let compiler = graph_craft::graphene_compiler::Compiler {};
let inner_network = NodeNetwork::value_network(node);
log::debug!("inner_network: {inner_network:?}");
let network = NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::Inline(InlineRust::new("i1[(_global_index.y * i0 + _global_index.x) as usize]".into(), concrete![Color]))],
implementation: DocumentNodeImplementation::ProtoNode("graphene_core::value::CopiedNode".into()),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::network(concrete!(u32), 0)],
implementation: DocumentNodeImplementation::ProtoNode("graphene_core::ops::IdentityNode".into()),
..Default::default()
},
// DocumentNode {
// name: "Index".into(),
// // inputs: vec![NodeInput::Network(concrete!(UVec3))],
// inputs: vec![NodeInput::Inline(InlineRust::new("i1.x as usize".into(), concrete![u32]))],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::value::CopiedNode".into()),
// ..Default::default()
// },
// DocumentNode {
// name: "Get Node".into(),
// inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(0), 0)],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::storage::GetNode".into()),
// ..Default::default()
// },
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::Network(inner_network),
..Default::default()
},
// DocumentNode {
// name: "Save Node".into(),
// inputs: vec![
// NodeInput::node(NodeId(5), 0),
// NodeInput::Inline(InlineRust::new(
// "|x| o0[(_global_index.y * i1 + _global_index.x) as usize] = x".into(),
// // "|x|()".into(),
// Type::Fn(Box::new(concrete!(PackedPixel)), Box::new(concrete!(()))),
// )),
// ],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::generic::FnMutNode".into()),
// ..Default::default()
// },
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
};
log::debug!("compiling network");
let proto_networks: Result<Vec<_>, _> = compiler.compile(network.clone()).collect();
log::debug!("compiling shader");
let shader = compilation_client::compile(
proto_networks?,
vec![concrete!(u32), concrete!(Color)],
vec![concrete!(Color)],
ShaderIO {
inputs: vec![
ShaderInput::UniformBuffer((), concrete!(u32)),
ShaderInput::StorageBuffer((), concrete!(Color)),
ShaderInput::OutputBuffer((), concrete!(Color)),
],
output: ShaderInput::OutputBuffer((), concrete!(Color)),
},
)
.await
.unwrap();
let len: usize = image.data.len();
let storage_buffer = executor
.create_storage_buffer(
image.data.clone(),
StorageBufferOptions {
cpu_writable: false,
gpu_writable: true,
cpu_readable: false,
storage: true,
},
)
.unwrap();
// let canvas = editor_api.application_io.create_surface();
// let surface = unsafe { executor.create_surface(canvas) }.unwrap();
// let surface_id = surface.surface_id;
// let texture = executor.create_texture_buffer(image.clone(), TextureBufferOptions::Texture).unwrap();
// // executor.create_render_pass(texture, surface).unwrap();
// let frame = SurfaceFrame {
// surface_id,
// transform: image.transform,
// };
// return frame;
log::debug!("creating buffer");
let width_uniform = executor.create_uniform_buffer(image.width).unwrap();
let storage_buffer = Arc::new(storage_buffer);
let output_buffer = executor.create_output_buffer(len, concrete!(Color), false).unwrap();
let output_buffer = Arc::new(output_buffer);
let readback_buffer = executor.create_output_buffer(len, concrete!(Color), true).unwrap();
let readback_buffer = Arc::new(readback_buffer);
log::debug!("created buffer");
let bind_group = Bindgroup {
buffers: vec![width_uniform.into(), storage_buffer],
};
let shader = Shader {
source: shader.spirv_binary.into(),
name: "gpu::eval",
io: shader.io,
};
log::debug!("loading shader");
let shader = executor.load_shader(shader).unwrap();
log::debug!("loaded shader");
let pipeline = PipelineLayout {
shader: shader.into(),
entry_point: "eval".to_string(),
bind_group: bind_group.into(),
output_buffer,
};
log::debug!("created pipeline");
Ok(ComputePass {
pipeline_layout: pipeline,
readback_buffer: Some(readback_buffer),
})
}
#[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_transform = foreground.transform();
@@ -457,3 +219,237 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
result
}
// struct ComputePass {
// pipeline_layout: PipelineLayout,
// readback_buffer: Option<Arc<WgpuShaderInput>>,
// }
// impl Clone for ComputePass {
// fn clone(&self) -> Self {
// Self {
// pipeline_layout: self.pipeline_layout.clone(),
// readback_buffer: self.readback_buffer.clone(),
// }
// }
// }
// pub struct MapGpuNode<Node, EditorApi> {
// node: Node,
// editor_api: EditorApi,
// cache: Mutex<HashMap<String, 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_instance_ref().instance;
// log::debug!("Executing gpu node");
// let executor = &editor_api.application_io.as_ref().and_then(|io| io.gpu_executor()).unwrap();
// #[cfg(feature = "image-compare")]
// let img: image::DynamicImage = image::Rgba32FImage::from_raw(image.width, image.height, bytemuck::cast_vec(image.data.clone())).unwrap().into();
// // TODO: The cache should be based on the network topology not the node name
// let compute_pass_descriptor = if self.cache.lock().as_ref().unwrap().contains_key("placeholder") {
// self.cache.lock().as_ref().unwrap().get("placeholder").unwrap().clone()
// } else {
// 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();
// };
// self.cache.lock().as_mut().unwrap().insert(name, compute_pass_descriptor.clone());
// log::error!("created compute pass");
// compute_pass_descriptor
// };
// let compute_pass = executor
// .create_compute_pass(
// &compute_pass_descriptor.pipeline_layout,
// compute_pass_descriptor.readback_buffer.clone(),
// ComputePassDimensions::XY(image.width / 12 + 1, image.height / 8 + 1),
// )
// .unwrap();
// executor.execute_compute_pipeline(compute_pass).unwrap();
// log::debug!("executed pipeline");
// log::debug!("reading buffer");
// let result = executor.read_output_buffer(compute_pass_descriptor.readback_buffer.clone().unwrap()).await.unwrap();
// let colors = bytemuck::pod_collect_to_vec::<u8, Color>(result.as_slice());
// log::debug!("first color: {:?}", colors[0]);
// #[cfg(feature = "image-compare")]
// let img2: image::DynamicImage = image::Rgba32FImage::from_raw(image.width, image.height, bytemuck::cast_vec(colors.clone())).unwrap().into();
// #[cfg(feature = "image-compare")]
// let score = image_compare::rgb_hybrid_compare(&img.into_rgb8(), &img2.into_rgb8()).unwrap();
// #[cfg(feature = "image-compare")]
// log::debug!("score: {:?}", score.score);
// let new_image = Image {
// data: colors,
// width: image.width,
// height: image.height,
// ..Default::default()
// };
// 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
// }
// impl<Node, EditorApi> MapGpuNode<Node, EditorApi> {
// pub fn new(node: Node, editor_api: EditorApi) -> Self {
// Self {
// node,
// editor_api,
// cache: Mutex::new(HashMap::new()),
// }
// }
// }
// async fn create_compute_pass_descriptor<T: Clone + Pixel + StaticTypeSized>(node: DocumentNode, image: &ImageFrameTable<T>, executor: &&WgpuExecutor) -> Result<ComputePass, String>
// where
// GraphicElement: From<Image<T>>,
// T::Static: Pixel,
// {
// let image = image.one_instance_ref().instance;
// let compiler = graph_craft::graphene_compiler::Compiler {};
// let inner_network = NodeNetwork::value_network(node);
// log::debug!("inner_network: {inner_network:?}");
// let network = NodeNetwork {
// exports: vec![NodeInput::node(NodeId(2), 0)],
// nodes: [
// DocumentNode {
// inputs: vec![NodeInput::Inline(InlineRust::new("i1[(_global_index.y * i0 + _global_index.x) as usize]".into(), concrete![Color]))],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::value::CopiedNode".into()),
// ..Default::default()
// },
// DocumentNode {
// inputs: vec![NodeInput::network(concrete!(u32), 0)],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::ops::IdentityNode".into()),
// ..Default::default()
// },
// // DocumentNode {
// // name: "Index".into(),
// // // inputs: vec![NodeInput::Network(concrete!(UVec3))],
// // inputs: vec![NodeInput::Inline(InlineRust::new("i1.x as usize".into(), concrete![u32]))],
// // implementation: DocumentNodeImplementation::ProtoNode("graphene_core::value::CopiedNode".into()),
// // ..Default::default()
// // },
// // DocumentNode {
// // name: "Get Node".into(),
// // inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(0), 0)],
// // implementation: DocumentNodeImplementation::ProtoNode("graphene_core::storage::GetNode".into()),
// // ..Default::default()
// // },
// DocumentNode {
// inputs: vec![NodeInput::node(NodeId(0), 0)],
// implementation: DocumentNodeImplementation::Network(inner_network),
// ..Default::default()
// },
// // DocumentNode {
// // name: "Save Node".into(),
// // inputs: vec![
// // NodeInput::node(NodeId(5), 0),
// // NodeInput::Inline(InlineRust::new(
// // "|x| o0[(_global_index.y * i1 + _global_index.x) as usize] = x".into(),
// // // "|x|()".into(),
// // Type::Fn(Box::new(concrete!(PackedPixel)), Box::new(concrete!(()))),
// // )),
// // ],
// // implementation: DocumentNodeImplementation::ProtoNode("graphene_core::generic::FnMutNode".into()),
// // ..Default::default()
// // },
// ]
// .into_iter()
// .enumerate()
// .map(|(id, node)| (NodeId(id as u64), node))
// .collect(),
// ..Default::default()
// };
// log::debug!("compiling network");
// let proto_networks: Result<Vec<_>, _> = compiler.compile(network.clone()).collect();
// log::debug!("compiling shader");
// let shader = compilation_client::compile(
// proto_networks?,
// vec![concrete!(u32), concrete!(Color)],
// vec![concrete!(Color)],
// ShaderIO {
// inputs: vec![
// ShaderInput::UniformBuffer((), concrete!(u32)),
// ShaderInput::StorageBuffer((), concrete!(Color)),
// ShaderInput::OutputBuffer((), concrete!(Color)),
// ],
// output: ShaderInput::OutputBuffer((), concrete!(Color)),
// },
// )
// .await
// .unwrap();
// let len: usize = image.data.len();
// let storage_buffer = executor
// .create_storage_buffer(
// image.data.clone(),
// StorageBufferOptions {
// cpu_writable: false,
// gpu_writable: true,
// cpu_readable: false,
// storage: true,
// },
// )
// .unwrap();
// // let canvas = editor_api.application_io.create_surface();
// // let surface = unsafe { executor.create_surface(canvas) }.unwrap();
// // let surface_id = surface.surface_id;
// // let texture = executor.create_texture_buffer(image.clone(), TextureBufferOptions::Texture).unwrap();
// // // executor.create_render_pass(texture, surface).unwrap();
// // let frame = SurfaceFrame {
// // surface_id,
// // transform: image.transform,
// // };
// // return frame;
// log::debug!("creating buffer");
// let width_uniform = executor.create_uniform_buffer(image.width).unwrap();
// let storage_buffer = Arc::new(storage_buffer);
// let output_buffer = executor.create_output_buffer(len, concrete!(Color), false).unwrap();
// let output_buffer = Arc::new(output_buffer);
// let readback_buffer = executor.create_output_buffer(len, concrete!(Color), true).unwrap();
// let readback_buffer = Arc::new(readback_buffer);
// log::debug!("created buffer");
// let bind_group = Bindgroup {
// buffers: vec![width_uniform.into(), storage_buffer],
// };
// let shader = Shader {
// source: shader.spirv_binary.into(),
// name: "gpu::eval",
// io: shader.io,
// };
// log::debug!("loading shader");
// let shader = executor.load_shader(shader).unwrap();
// log::debug!("loaded shader");
// let pipeline = PipelineLayout {
// shader: shader.into(),
// entry_point: "eval".to_string(),
// bind_group: bind_group.into(),
// output_buffer,
// };
// log::debug!("created pipeline");
// Ok(ComputePass {
// pipeline_layout: pipeline,
// readback_buffer: Some(readback_buffer),
// })
// }

View File

@@ -4,7 +4,7 @@ use glam::{DAffine2, DVec2, Vec2};
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, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue, Sample,
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};
@@ -12,7 +12,6 @@ use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::fmt::Debug;
use std::hash::Hash;
use std::marker::PhantomData;
#[derive(Debug, DynAny)]
pub enum Error {
@@ -90,95 +89,28 @@ fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFra
result
}
#[derive(Debug, Clone, Copy)]
pub struct MapImageNode<P, MapFn> {
map_fn: MapFn,
_p: PhantomData<P>,
}
#[node_macro::old_node_fn(MapImageNode<_P>)]
fn map_image<MapFn, _P, Img: BitmapMut<Pixel = _P>>(image: Img, map_fn: &'input MapFn) -> Img
where
MapFn: for<'any_input> Node<'any_input, _P, Output = _P> + 'input,
{
let mut image = image;
image.map_pixels(|c| map_fn.eval(c));
image
}
#[node_macro::node]
fn insert_channel<
// _P is the color of the input image.
_P: RGBMut,
_S: Pixel + Luminance,
// Input image
Input: BitmapMut<Pixel = _P>,
Insertion: Bitmap<Pixel = _S>,
>(
#[node_macro::node(category("Raster"))]
fn combine_channels<_I, Red, Green, Blue, Alpha>(
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
#[implementations(ImageFrameTable<Color>)] insertion: Insertion,
target_channel: RedGreenBlue,
) -> Input
where
_P::ColorChannel: Linear,
{
if insertion.width() == 0 {
return image;
}
if insertion.width() != image.width() || insertion.height() != image.height() {
log::warn!("Stencil and image have different sizes. This is not supported.");
return image;
}
for y in 0..image.height() {
for x in 0..image.width() {
let image_pixel = image.get_pixel_mut(x, y).unwrap();
let insertion_pixel = insertion.get_pixel(x, y).unwrap();
match target_channel {
RedGreenBlue::Red => image_pixel.set_red(insertion_pixel.l().cast_linear_channel()),
RedGreenBlue::Green => image_pixel.set_green(insertion_pixel.l().cast_linear_channel()),
RedGreenBlue::Blue => image_pixel.set_blue(insertion_pixel.l().cast_linear_channel()),
}
}
}
image
}
#[node_macro::node]
fn combine_channels<
// _P is the color of the input image.
_P: RGBMut + AlphaMut,
_S: Pixel + Luminance,
// Input image
Input: BitmapMut<Pixel = _P>,
Red: Bitmap<Pixel = _S>,
Green: Bitmap<Pixel = _S>,
Blue: Bitmap<Pixel = _S>,
Alpha: Bitmap<Pixel = _S>,
>(
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
_primary: (),
#[implementations(ImageFrameTable<Color>)] red: Red,
#[implementations(ImageFrameTable<Color>)] green: Green,
#[implementations(ImageFrameTable<Color>)] blue: Blue,
#[implementations(ImageFrameTable<Color>)] alpha: Alpha,
) -> Input
) -> ImageFrameTable<Color>
where
_P::ColorChannel: Linear,
_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().all(|&(x, _)| x == 0) {
return image;
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();
}
if dimensions.iter().any(|&(x, y)| x != image.width() || y != image.height()) {
log::warn!("Stencil and image have different sizes. This is not supported.");
return image;
}
let mut image = Image::new(red.width(), red.height(), Color::TRANSPARENT);
for y in 0..image.height() {
for x in 0..image.width() {
@@ -198,26 +130,30 @@ where
}
}
image
ImageFrameTable::new(image)
}
#[node_macro::node()]
fn mask_image<
// _P is the color of the input image. It must have an alpha channel because that is going to
// be modified by the mask
#[node_macro::node(category("Raster"))]
fn mask<_P, _S, Input, Stencil>(
_: impl Ctx,
/// The image to be masked.
#[implementations(ImageFrameTable<Color>)]
mut image: Input,
/// 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 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>,
>(
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
#[implementations(ImageFrameTable<Color>)] stencil: Stencil,
) -> Input {
{
let image_size = DVec2::new(image.width() as f64, image.height() as f64);
let mask_size = stencil.transform().decompose_scale();
@@ -313,13 +249,8 @@ where
background
}
#[derive(Debug, Clone, Copy)]
pub struct ExtendImageToBoundsNode<Bounds> {
bounds: Bounds,
}
#[node_macro::old_node_fn(ExtendImageToBoundsNode)]
fn extend_image_to_bounds(image: ImageFrameTable<Color>, bounds: DAffine2) -> ImageFrameTable<Color> {
#[node_macro::node(category(""))]
fn extend_image_to_bounds(_: impl Ctx, 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) {