From 2c15dc838e1a7e59d9113e7f77df82d6229a8410 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 23 Aug 2026 10:11:34 +0000 Subject: [PATCH] Convert dehaze, empty_image, extend_image_to_bounds, and image to record kernels --- node-graph/nodes/brush/src/brush.rs | 29 ++++-- node-graph/nodes/raster/src/dehaze.rs | 43 ++++----- node-graph/nodes/raster/src/std_nodes.rs | 108 ++++++++++++----------- 3 files changed, 96 insertions(+), 84 deletions(-) diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index 4ac6349e79..33b685f4e7 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -11,7 +11,7 @@ use core_types::uuid::NodeId; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM}; use glam::{DAffine2, DVec2}; use raster_nodes::blending_nodes::blend_colors; -use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds}; +use raster_nodes::std_nodes::{empty_image_core, extend_image_to_bounds_core}; use raster_types::BitmapMut; use raster_types::Image; use raster_types::{CPU, Raster}; @@ -134,7 +134,11 @@ where pub fn create_brush_texture(brush_style: &BrushStyle) -> Raster { 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.)); - let blank_texture = empty_image(&(), transform, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default(); + let blank_texture = { + let mut item = Item::new_from_element(empty_image_core(transform, Color::TRANSPARENT)); + item.set_attribute(ATTR_TRANSFORM, transform); + item + }; let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)); image.into_element() @@ -221,8 +225,14 @@ fn brush( let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes); // TODO: Find a way to handle more than one item - let Some(mut actual_image) = extend_image_to_bounds(&(), List::new_from_item(brush_plan.background), background_bounds).into_iter().next() else { - return List::new(); + let mut actual_image = { + let background = brush_plan.background; + let transform: DAffine2 = background.attribute_cloned_or_default(ATTR_TRANSFORM); + let (element, attributes) = background.into_parts(); + let (element, transform) = extend_image_to_bounds_core(element, transform, background_bounds); + let mut item = Item::from_parts(element, attributes); + item.set_attribute(ATTR_TRANSFORM, transform); + item }; let final_stroke_idx = brush_plan.strokes.len().saturating_sub(1); @@ -254,9 +264,16 @@ fn brush( let blit_target = if idx == 0 { let target = core::mem::take(&mut brush_plan.first_stroke_texture); - extend_image_to_bounds(&(), List::new_from_item(target), stroke_to_layer) + let transform: DAffine2 = target.attribute_cloned_or_default(ATTR_TRANSFORM); + let (element, attributes) = target.into_parts(); + let (element, transform) = extend_image_to_bounds_core(element, transform, stroke_to_layer); + let mut item = Item::from_parts(element, attributes); + item.set_attribute(ATTR_TRANSFORM, transform); + List::new_from_item(item) } else { - empty_image(&(), stroke_to_layer, List::new_from_element(Color::TRANSPARENT)) + let mut item = Item::new_from_element(empty_image_core(stroke_to_layer, Color::TRANSPARENT)); + item.set_attribute(ATTR_TRANSFORM, stroke_to_layer); + List::new_from_item(item) }; let list = blit(&(), blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)); diff --git a/node-graph/nodes/raster/src/dehaze.rs b/node-graph/nodes/raster/src/dehaze.rs index 711fb47b0f..1db6f4537f 100644 --- a/node-graph/nodes/raster/src/dehaze.rs +++ b/node-graph/nodes/raster/src/dehaze.rs @@ -1,5 +1,4 @@ use core_types::context::Ctx; -use core_types::list::List; use core_types::registry::types::Percentage; use image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage}; use ndarray::{Array2, ArrayBase, Dim, OwnedRepr}; @@ -8,33 +7,27 @@ use raster_types::{CPU, Raster}; use std::cmp::{max, min}; #[node_macro::node(category("Raster: Filter"))] -fn dehaze(_: impl Ctx, image_frame: List>, strength: Percentage) -> List> { - image_frame - .into_iter() - .map(|mut row| { - let image = std::mem::replace(row.element_mut(), Raster::new_cpu(Image::default())); - // Prepare the image data for processing - let image_data = bytemuck::cast_vec(image.data.clone()); - let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type."); - let dynamic_image: DynamicImage = image_buffer.into(); +fn dehaze(_: impl Ctx, image_frame: Raster, strength: Percentage) -> Raster { + let image = image_frame; + // Prepare the image data for processing + let image_data = bytemuck::cast_vec(image.data.clone()); + let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type."); + let dynamic_image: DynamicImage = image_buffer.into(); - // Run the dehaze algorithm - let dehazed_dynamic_image = dehaze_image(dynamic_image, strength / 100.); + // Run the dehaze algorithm + let dehazed_dynamic_image = dehaze_image(dynamic_image, strength / 100.); - // Prepare the image data for returning - let buffer = dehazed_dynamic_image.to_rgba32f().into_raw(); - let color_vec = bytemuck::cast_vec(buffer); - let dehazed_image = Image { - width: image.width, - height: image.height, - data: color_vec, - base64_string: None, - }; + // Prepare the image data for returning + let buffer = dehazed_dynamic_image.to_rgba32f().into_raw(); + let color_vec = bytemuck::cast_vec(buffer); + let dehazed_image = Image { + width: image.width, + height: image.height, + data: color_vec, + base64_string: None, + }; - *row.element_mut() = Raster::new_cpu(dehazed_image); - row - }) - .collect() + Raster::new_cpu(dehazed_image) } // There is no real point in modifying these values because they do not change the final result all that much. diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index daee1a9a66..5e1fe31b9c 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -1,8 +1,9 @@ use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType}; use core_types::ATTR_TRANSFORM; +use core_types::attribute::{Attr, Transform as TransformAttr}; use core_types::color::Color; use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut}; -use core_types::context::{Ctx, ExtractFootprint}; +use core_types::context::{Ctx, ExtractFootprint, ExtractIndex, InjectIndex}; use core_types::list::{Item, List}; use core_types::math::bbox::Bbox; use core_types::transform::Transform; @@ -225,75 +226,76 @@ pub fn mask( .collect() } -#[node_macro::node(category(""))] -pub fn extend_image_to_bounds(_: impl Ctx, image: List>, bounds: DAffine2) -> List> { - image - .into_iter() - .map(|mut row| { - let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); - let image_aabb = Bbox::unit().affine_transform(row_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 row; - } +/// The per-lane extend, shared with the brush's plain callers. +pub fn extend_image_to_bounds_core(image: Raster, row_transform: DAffine2, bounds: DAffine2) -> (Raster, DAffine2) { + let image_aabb = Bbox::unit().affine_transform(row_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, row_transform); + } - let image_data = &row.element().data; - let (image_width, image_height) = (row.element().width, row.element().height); - if image_width == 0 || image_height == 0 { - return empty_image(&(), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap(); - } + let (image_width, image_height) = (image.width, image.height); + if image_width == 0 || image_height == 0 { + return (empty_image_core(bounds, Color::TRANSPARENT), bounds); + } + let image_data = &image.data; - let orig_image_scale = DVec2::new(image_width as f64, image_height as f64); - let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row_transform.inverse(); - let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox(); + let orig_image_scale = DVec2::new(image_width as f64, image_height as f64); + let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row_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); - let new_end = bounds_in_image_space.end.ceil().max(orig_image_scale); - let new_scale = new_end - new_start; + let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO); + let new_end = bounds_in_image_space.end.ceil().max(orig_image_scale); + let new_scale = new_end - new_start; - // Copy over original image into enlarged image. - let mut new_image = 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_height { - let old_start = y * image_width; - let new_start = (y + offset_in_new_image.y) * new_image.width + offset_in_new_image.x; - let old_row = &image_data[old_start as usize..(old_start + image_width) as usize]; - let new_row = &mut new_image.data[new_start as usize..(new_start + image_width) as usize]; - new_row.copy_from_slice(old_row); - } + // Copy over original image into enlarged image. + let mut new_image = 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_height { + let old_start = y * image_width; + let new_start = (y + offset_in_new_image.y) * new_image.width + offset_in_new_image.x; + let old_row = &image_data[old_start as usize..(old_start + image_width) as usize]; + let new_row = &mut new_image.data[new_start as usize..(new_start + 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 = row_transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale); + // 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 = row_transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale); - *row.element_mut() = Raster::new_cpu(new_image); - row.set_attribute(ATTR_TRANSFORM, new_texture_to_layer_space); - row - }) - .collect() + (Raster::new_cpu(new_image), new_texture_to_layer_space) } -#[node_macro::node(category("Debug"))] -pub fn empty_image(_: impl Ctx, transform: DAffine2, color: List) -> List> { +#[node_macro::node(category(""))] +pub fn extend_image_to_bounds(_: impl Ctx, (image, transform): (Raster, Attr), bounds: DAffine2) -> (Raster, Attr) { + let (image, transform) = extend_image_to_bounds_core(image, *transform, bounds); + (image, Attr(transform)) +} + +/// The blank texture a transform spans, shared with the brush's plain callers. +pub fn empty_image_core(transform: DAffine2, color: Color) -> Raster { 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 color = color.element(0).copied().unwrap_or(Color::WHITE); - let image = Image::new(width, height, color); + Raster::new_cpu(Image::new(width, height, color)) +} - let mut result_list = List::new_from_element(Raster::new_cpu(image)); - result_list.set_attribute(ATTR_TRANSFORM, 0, transform); - - // Callers of empty_image can safely unwrap on returned `List` - result_list +#[node_macro::node(category("Debug"))] +pub fn empty_image(_: impl Ctx + ExtractIndex + InjectIndex + Copy, transform: DAffine2, color: IList) -> (Raster, Attr) { + let color = match color.len() { + 0 => Color::WHITE, + _ => color.get(0), + }; + (empty_image_core(transform, color), Attr(transform)) } #[node_macro::node(category(""))] -pub fn image(_: impl Ctx, resource: Resource) -> List> { +pub fn image(_: impl Ctx, resource: Resource) -> Raster { let image_data = resource.as_ref(); + // A zero-size raster renders as nothing, matching the legacy empty list. let Some(image) = ::image::load_from_memory(image_data).ok() else { - return List::new(); + return Raster::new_cpu(Image::default()); }; let image = image.to_rgba32f(); let image = Image { @@ -308,7 +310,7 @@ pub fn image(_: impl Ctx, resource: Resource) -> List> { height: image.height(), ..Default::default() }; - List::new_from_element(Raster::new_cpu(image)) + Raster::new_cpu(image) } /// Generates customizable procedural noise patterns.