Convert the raster std and adjustments families to element kernels and keep the unit primary's slot

This commit is contained in:
Dennis Kobert
2026-08-23 16:52:43 +00:00
parent 2c15dc838e
commit 511b4da9f5
8 changed files with 326 additions and 256 deletions
@@ -1402,6 +1402,12 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.set_input(&InputConnector::node(*node_id, index), input, network_path); document.network_interface.set_input(&InputConnector::node(*node_id, index), input, network_path);
} }
} }
// The leveled-records flip gave Mandelbrot a unit primary input.
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::mandelbrot::IDENTIFIER) {
let mut node_template = node_definition.default_node_template();
document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
}
} }
// Rebuild stale Merge/Artboard subgraphs that still use the removed LegacyLayerExtendNode internally // Rebuild stale Merge/Artboard subgraphs that still use the removed LegacyLayerExtendNode internally
@@ -7,3 +7,12 @@ impl Ctx for () {}
pub trait ArcCtx: Send + Sync {} pub trait ArcCtx: Send + Sync {}
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl<T: ArcCtx> Ctx for std::sync::Arc<T> {} impl<T: ArcCtx> Ctx for std::sync::Arc<T> {}
// The cache-hash bound record kernels place on their element generics; the
// shader build compiles the same signatures without the hashing machinery.
#[cfg(feature = "std")]
pub use graphene_hash::CacheHash;
#[cfg(not(feature = "std"))]
pub trait CacheHash {}
#[cfg(not(feature = "std"))]
impl<T> CacheHash for T {}
+3 -3
View File
@@ -74,10 +74,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
(Some(crate::codegen::ir::NodeKind::Routing), crate::codegen::ir::Element::Generic(ident)) => Some(ident.clone()), (Some(crate::codegen::ir::NodeKind::Routing), crate::codegen::ir::Element::Generic(ident)) => Some(ident.clone()),
_ => None, _ => None,
}; };
// A `_: ()` primary stays visible in the metadata but claims no struct field. // A `_: ()` primary keeps its slot: dropping it would shift every
let record_unit_carrier = record_io && crate::codegen::classify::record_shape(parsed).is_some_and(|shape| shape.skips_carrier()); // per-index classification against the IR and the document's arity.
let record_skips_carrier = record_io && !carrier_present; let record_skips_carrier = record_io && !carrier_present;
let struct_regular_fields: Vec<_> = regular_fields.iter().skip(record_unit_carrier as usize).copied().collect(); let struct_regular_fields: Vec<_> = regular_fields.to_vec();
let struct_regular_field_names: Vec<_> = struct_regular_fields.iter().map(|f| &f.pat_ident.ident).collect(); let struct_regular_field_names: Vec<_> = struct_regular_fields.iter().map(|f| &f.pat_ident.ident).collect();
// Extract function generics used by data fields // Extract function generics used by data fields
+1 -1
View File
@@ -1938,7 +1938,7 @@ mod tests {
let layout = source_opacity_layout(); let layout = source_opacity_layout();
reserve_for(&[&layout]); reserve_for(&[&layout]);
let node = install(SourceOpacityNode::new(ValueNode(3.), ValueNode(0.25)), source_opacity_layout_meta(), &[]); let node = install(SourceOpacityNode::new(ValueNode(()), ValueNode(3.), ValueNode(0.25)), source_opacity_layout_meta(), &[]);
assert_eq!(Node::<ContextImpl>::layout(&node), &layout); assert_eq!(Node::<ContextImpl>::layout(&node), &layout);
let GPoll::Final(value) = node.eval(&ctx) else { let GPoll::Final(value) = node.eval(&ctx) else {
panic!("expected a final record"); panic!("expected a final record");
+8 -3
View File
@@ -19,9 +19,14 @@ mod adjust_std {
impl Adjust<Color> for List<Raster<CPU>> { impl Adjust<Color> for List<Raster<CPU>> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for element in self.iter_element_values_mut() { for element in self.iter_element_values_mut() {
for color in element.data_mut().data.iter_mut() { element.adjust(&map_fn);
*color = map_fn(color); }
} }
}
impl Adjust<Color> for Raster<CPU> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for color in self.data_mut().data.iter_mut() {
*color = map_fn(color);
} }
} }
} }
+64 -66
View File
@@ -3,8 +3,6 @@
use crate::adjust::Adjust; use crate::adjust::Adjust;
use crate::cubic_spline::CubicSplines; use crate::cubic_spline::CubicSplines;
use core::fmt::Debug; use core::fmt::Debug;
#[cfg(feature = "std")]
use core_types::list::List;
use glam::Vec3; use glam::Vec3;
use no_std_types::color::{Color, linear_to_srgb, srgb_to_linear}; use no_std_types::color::{Color, linear_to_srgb, srgb_to_linear};
use no_std_types::context::Ctx; use no_std_types::context::Ctx;
@@ -50,12 +48,12 @@ pub enum LuminanceCalculation {
} }
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn luminance<T: Adjust<Color>>( fn luminance<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -75,12 +73,12 @@ fn luminance<T: Adjust<Color>>(
} }
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn gamma_correction<T: Adjust<Color>>( fn gamma_correction<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -97,12 +95,12 @@ fn gamma_correction<T: Adjust<Color>>(
} }
#[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))]
fn extract_channel<T: Adjust<Color>>( fn extract_channel<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -121,12 +119,12 @@ fn extract_channel<T: Adjust<Color>>(
} }
#[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))]
fn make_opaque<T: Adjust<Color>>( fn make_opaque<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -143,12 +141,12 @@ fn make_opaque<T: Adjust<Color>>(
// TODO: Remove this once GPU shader nodes are able to support the non-classic algorithm // TODO: Remove this once GPU shader nodes are able to support the non-classic algorithm
// TODO: Maybe re-add the "Raster: Adjustment" category to make this user-facing if we care to make this not just for testing // TODO: Maybe re-add the "Raster: Adjustment" category to make this user-facing if we care to make this not just for testing
#[node_macro::node(name("Brightness/Contrast Classic"), category(""), properties("brightness_contrast_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(name("Brightness/Contrast Classic"), category(""), properties("brightness_contrast_properties"), shader_node(PerPixelAdjust))]
fn brightness_contrast_classic<T: Adjust<Color>>( fn brightness_contrast_classic<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -174,12 +172,12 @@ fn brightness_contrast_classic<T: Adjust<Color>>(
// Some further analysis available at: // Some further analysis available at:
// https://geraldbakker.nl/psnumbers/brightness-contrast.html // https://geraldbakker.nl/psnumbers/brightness-contrast.html
#[node_macro::node(name("Brightness/Contrast"), category("Raster: Adjustment"), properties("brightness_contrast_properties"), cfg(feature = "std"))] #[node_macro::node(name("Brightness/Contrast"), category("Raster: Adjustment"), properties("brightness_contrast_properties"), cfg(feature = "std"))]
fn brightness_contrast<T: Adjust<Color>>( fn brightness_contrast<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_ctx: impl Ctx, _ctx: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -255,12 +253,12 @@ fn brightness_contrast<T: Adjust<Color>>(
// Some further analysis available at: // Some further analysis available at:
// https://geraldbakker.nl/psnumbers/levels.html // https://geraldbakker.nl/psnumbers/levels.html
#[node_macro::node(category("Raster: Adjustment"), properties("levels_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), properties("levels_properties"), shader_node(PerPixelAdjust))]
fn levels<T: Adjust<Color>>( fn levels<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut image: T, mut image: T,
@@ -334,12 +332,12 @@ fn levels<T: Adjust<Color>>(
// Works the same for gamma and linear color // Works the same for gamma and linear color
// TODO: Currently the un-List-wrapped `tint` Color is causing a type error. Put this back in the "Raster: Adjustment" category once that's fixed. // TODO: Currently the un-List-wrapped `tint` Color is causing a type error. Put this back in the "Raster: Adjustment" category once that's fixed.
#[node_macro::node(name("Black & White"), category(""), properties("black_and_white_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(name("Black & White"), category(""), properties("black_and_white_properties"), shader_node(PerPixelAdjust))]
fn black_and_white<T: Adjust<Color>>( fn black_and_white<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut image: T, mut image: T,
@@ -417,12 +415,12 @@ fn black_and_white<T: Adjust<Color>>(
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27hue%20%27%20%3D%20Old,saturation%2C%20Photoshop%205.0 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27hue%20%27%20%3D%20Old,saturation%2C%20Photoshop%205.0
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=0%20%3D%20Use%20other.-,Hue/Saturation,-Hue/Saturation%20settings // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=0%20%3D%20Use%20other.-,Hue/Saturation,-Hue/Saturation%20settings
#[node_macro::node(name("Hue/Saturation"), category("Raster: Adjustment"), properties("hue_saturation_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(name("Hue/Saturation"), category("Raster: Adjustment"), properties("hue_saturation_properties"), shader_node(PerPixelAdjust))]
fn hue_saturation<T: Adjust<Color>>( fn hue_saturation<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -449,12 +447,12 @@ fn hue_saturation<T: Adjust<Color>>(
// Aims for interoperable compatibility with: // Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27%20%3D%20Color%20Lookup-,%27nvrt%27%20%3D%20Invert,-%27post%27%20%3D%20Posterize // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27%20%3D%20Color%20Lookup-,%27nvrt%27%20%3D%20Invert,-%27post%27%20%3D%20Posterize
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn invert<T: Adjust<Color>>( fn invert<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -470,12 +468,12 @@ fn invert<T: Adjust<Color>>(
// Aims for interoperable compatibility with: // Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=post%27%20%3D%20Posterize-,%27thrs%27%20%3D%20Threshold,-%27grdm%27%20%3D%20Gradient // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=post%27%20%3D%20Posterize-,%27thrs%27%20%3D%20Threshold,-%27grdm%27%20%3D%20Gradient
#[node_macro::node(category("Raster: Adjustment"), properties("threshold_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), properties("threshold_properties"), shader_node(PerPixelAdjust))]
fn threshold<T: Adjust<Color>>( fn threshold<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut image: T, mut image: T,
@@ -516,12 +514,12 @@ fn threshold<T: Adjust<Color>>(
// When both parameters are set, it is equivalent to running this adjustment twice, with only vibrance set and then only saturation set. // When both parameters are set, it is equivalent to running this adjustment twice, with only vibrance set and then only saturation set.
// (Except for some noise probably due to rounding error.) // (Except for some noise probably due to rounding error.)
#[node_macro::node(category("Raster: Adjustment"), properties("vibrance_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), properties("vibrance_properties"), shader_node(PerPixelAdjust))]
fn vibrance<T: Adjust<Color>>( fn vibrance<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut image: T, mut image: T,
@@ -718,12 +716,12 @@ pub enum DomainWarpType {
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27mixr%27%20%3D%20Channel%20Mixer // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27mixr%27%20%3D%20Channel%20Mixer
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Lab%20color%20only-,Channel%20Mixer,-Key%20is%20%27mixr // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Lab%20color%20only-,Channel%20Mixer,-Key%20is%20%27mixr
#[node_macro::node(category("Raster: Adjustment"), properties("channel_mixer_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), properties("channel_mixer_properties"), shader_node(PerPixelAdjust))]
fn channel_mixer<T: Adjust<Color>>( fn channel_mixer<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut image: T, mut image: T,
@@ -850,12 +848,12 @@ pub enum SelectiveColorChoice {
// Algorithm based on: // Algorithm based on:
// https://blog.pkh.me/p/22-understanding-selective-coloring-in-adobe-photoshop.html // https://blog.pkh.me/p/22-understanding-selective-coloring-in-adobe-photoshop.html
#[node_macro::node(category("Raster: Adjustment"), properties("selective_color_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), properties("selective_color_properties"), shader_node(PerPixelAdjust))]
fn selective_color<T: Adjust<Color>>( fn selective_color<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut image: T, mut image: T,
@@ -994,12 +992,12 @@ fn selective_color<T: Adjust<Color>>(
// https://www.axiomx.com/posterize.htm // https://www.axiomx.com/posterize.htm
// This algorithm produces fully accurate output in relation to the industry standard. // This algorithm produces fully accurate output in relation to the industry standard.
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn posterize<T: Adjust<Color>>( fn posterize<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
@@ -1023,12 +1021,12 @@ fn posterize<T: Adjust<Color>>(
// Algorithm based on: // Algorithm based on:
// https://geraldbakker.nl/psnumbers/exposure.html // https://geraldbakker.nl/psnumbers/exposure.html
#[node_macro::node(category("Raster: Adjustment"), properties("exposure_properties"), shader_node(PerPixelAdjust))] #[node_macro::node(category("Raster: Adjustment"), properties("exposure_properties"), shader_node(PerPixelAdjust))]
fn exposure<T: Adjust<Color>>( fn exposure<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
#[gpu_image] #[gpu_image]
mut input: T, mut input: T,
+11 -9
View File
@@ -1,8 +1,7 @@
//! Not immediately shader compatible due to needing [`GradientStops`] as a param, which needs [`Vec`] //! Not immediately shader compatible due to needing [`GradientStops`] as a param, which needs [`Vec`]
use crate::adjust::Adjust; use crate::adjust::Adjust;
use core_types::list::List; use core_types::{Color, Ctx, ExtractIndex, InjectIndex};
use core_types::{Color, Ctx};
use raster_types::{CPU, Raster}; use raster_types::{CPU, Raster};
use vector_types::GradientStops; use vector_types::GradientStops;
@@ -10,18 +9,21 @@ use vector_types::GradientStops;
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0) // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
#[node_macro::node(category("Raster: Adjustment"))] #[node_macro::node(category("Raster: Adjustment"))]
fn gradient_map<T: Adjust<Color>>( fn gradient_map<T: Adjust<Color> + Clone + Send + Sync + core_types::CacheHash + 'static>(
_: impl Ctx, _: impl Ctx + ExtractIndex + InjectIndex + Copy,
#[implementations( #[implementations(
List<Raster<CPU>>, Raster<CPU>,
List<Color>, Color,
List<GradientStops>, GradientStops,
)] )]
mut image: T, mut image: T,
gradient: List<GradientStops>, gradient: IList<GradientStops>,
reverse: bool, reverse: bool,
) -> T { ) -> T {
let Some(gradient) = gradient.element(0) else { return image }; if gradient.is_empty() {
return image;
}
let gradient = gradient.element_ref(0);
image.adjust(|color| { image.adjust(|color| {
let intensity = color.luminance_rec_709(); let intensity = color.luminance_rec_709();
+224 -174
View File
@@ -1,10 +1,10 @@
use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType}; use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType};
use core_types::ATTR_TRANSFORM; use core_types::attribute::{Attr, Attribute, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform as TransformAttr};
use core_types::attribute::{Attr, Transform as TransformAttr};
use core_types::color::Color; use core_types::color::Color;
use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut}; use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use core_types::context::{Ctx, ExtractFootprint, ExtractIndex, InjectIndex}; use core_types::context::{Ctx, ExtractFootprint, ExtractIndex, InjectIndex};
use core_types::list::{Item, List}; use core_types::extent::{LevelIn, ListIn, ValueIn};
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
use core_types::math::bbox::Bbox; use core_types::math::bbox::Bbox;
use core_types::transform::Transform; use core_types::transform::Transform;
use dyn_any::DynAny; use dyn_any::DynAny;
@@ -31,199 +31,249 @@ impl From<std::io::Error> for Error {
} }
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: List<Raster<CPU>>) -> List<Raster<CPU>> { pub fn sample_image(ctx: impl Ctx + ExtractFootprint, (image, lane_transform): (Raster<CPU>, Attr<TransformAttr>)) -> (Raster<CPU>, Attr<TransformAttr>) {
image_frame let image_frame_transform: DAffine2 = *lane_transform;
.into_iter()
.filter_map(|row| {
let image_frame_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let (image, mut attributes) = row.into_parts();
// Resize the image using the image crate // Resize the image using the image crate
let data = bytemuck::cast_vec(image.data.clone()); let data = bytemuck::cast_vec(image.data.clone());
let footprint = ctx.footprint(); let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space(); let viewport_bounds = footprint.viewport_bounds_in_local_space();
let image_bounds = Bbox::from_transform(image_frame_transform).to_axis_aligned_bbox(); let image_bounds = Bbox::from_transform(image_frame_transform).to_axis_aligned_bbox();
let intersection = viewport_bounds.intersect(&image_bounds); let intersection = viewport_bounds.intersect(&image_bounds);
let image_size = DAffine2::from_scale(DVec2::new(image.width as f64, image.height as f64)); let image_size = DAffine2::from_scale(DVec2::new(image.width as f64, image.height as f64));
let size = intersection.size(); let size = intersection.size();
let size_px = image_size.transform_vector2(size).as_uvec2(); let size_px = image_size.transform_vector2(size).as_uvec2();
// If the image would not be visible, add nothing. // A culled lane serves a zero-size raster, which renders as nothing.
if size.x <= 0. || size.y <= 0. { if size.x <= 0. || size.y <= 0. {
return None; return (Raster::new_cpu(Image::default()), Attr(image_frame_transform));
} }
let image_buffer = ::image::Rgba32FImage::from_raw(image.width, image.height, data).expect("Failed to convert internal image format into image-rs data type."); let image_buffer = ::image::Rgba32FImage::from_raw(image.width, image.height, data).expect("Failed to convert internal image format into image-rs data type.");
let dynamic_image: ::image::DynamicImage = image_buffer.into(); let dynamic_image: ::image::DynamicImage = image_buffer.into();
let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO); let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO);
let offset_px = image_size.transform_vector2(offset).as_uvec2(); let offset_px = image_size.transform_vector2(offset).as_uvec2();
let cropped = dynamic_image.crop_imm(offset_px.x, offset_px.y, size_px.x, size_px.y); let cropped = dynamic_image.crop_imm(offset_px.x, offset_px.y, size_px.x, size_px.y);
let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length(); let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length();
let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length(); let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length();
let mut new_width = size_px.x; let mut new_width = size_px.x;
let mut new_height = size_px.y; let mut new_height = size_px.y;
// Only downscale the image for now // Only downscale the image for now
let resized = if new_width < image.width || new_height < image.height { let resized = if new_width < image.width || new_height < image.height {
new_width = viewport_resolution_x as u32; new_width = viewport_resolution_x as u32;
new_height = viewport_resolution_y as u32; new_height = viewport_resolution_y as u32;
// TODO: choose filter based on quality requirements // TODO: choose filter based on quality requirements
cropped.resize_exact(new_width, new_height, ::image::imageops::Triangle) cropped.resize_exact(new_width, new_height, ::image::imageops::Triangle)
} else { } else {
cropped cropped
}; };
let buffer = resized.to_rgba32f(); let buffer = resized.to_rgba32f();
let buffer = buffer.into_raw(); let buffer = buffer.into_raw();
let vec = bytemuck::cast_vec(buffer); let vec = bytemuck::cast_vec(buffer);
let image = Image { let image = Image {
width: new_width, width: new_width,
height: new_height, height: new_height,
data: vec, data: vec,
base64_string: None, base64_string: None,
}; };
// we need to adjust the offset if we truncate the offset calculation // we need to adjust the offset if we truncate the offset calculation
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size); let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
attributes.insert(ATTR_TRANSFORM, new_transform);
Some(Item::from_parts(Raster::new_cpu(image), attributes)) (Raster::new_cpu(image), Attr(new_transform))
})
.collect()
} }
#[node_macro::node(category("Raster: Channels"))] #[node_macro::node(category("Raster: Channels"), extent(combine_channels_extent))]
pub fn combine_channels( pub fn combine_channels<'e>(
_: impl Ctx, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
_primary: (), _primary: (),
#[expose] red: List<Raster<CPU>>, #[expose] red: IList<Raster<CPU>>,
#[expose] green: List<Raster<CPU>>, #[expose] green: IList<Raster<CPU>>,
#[expose] blue: List<Raster<CPU>>, #[expose] blue: IList<Raster<CPU>>,
#[expose] alpha: List<Raster<CPU>>, #[expose] alpha: IList<Raster<CPU>>,
) -> List<Raster<CPU>> { ) -> Result<
IList<(
Raster<CPU>,
Attr<'e, TransformAttr>,
Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>,
Attr<'e, OpacityFill>,
Attr<'e, ClippingMask>,
Attr<'e, EditorLayerPath>,
)>,
Interrupt,
> {
let lane = ctx.innermost_index() as usize;
let max_len = red.len().max(green.len()).max(blue.len()).max(alpha.len()); let max_len = red.len().max(green.len()).max(blue.len()).max(alpha.len());
let red = red.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len); if lane >= max_len {
let green = green.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len); return Err(GraphError::past_end().into());
let blue = blue.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len); }
let alpha = alpha.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
red.zip(green) // Zero-size lanes and lanes past a shorter channel's end contribute nothing
.zip(blue) fn pick<'l>(list: &'l core_types::node::List<'_, Raster<CPU>>, lane: usize) -> Option<&'l Raster<CPU>> {
.zip(alpha) (lane < list.len()).then(|| list.element_ref(lane)).filter(|i| i.width > 0 && i.height > 0)
.filter_map(|(((red, green), blue), alpha)| { }
// Turn any default zero-sized image items into None let (red_el, green_el, blue_el, alpha_el) = (pick(&red, lane), pick(&green, lane), pick(&blue, lane), pick(&alpha, lane));
let red = red.filter(|i| i.element().width > 0 && i.element().height > 0);
let green = green.filter(|i| i.element().width > 0 && i.element().height > 0);
let blue = blue.filter(|i| i.element().width > 0 && i.element().height > 0);
let alpha = alpha.filter(|i| i.element().width > 0 && i.element().height > 0);
// Get this item's transform and alpha blending mode from the first non-empty channel // This lane's transform and blending come from the first non-empty channel
let attributes = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| i.attributes().clone())?; let attr_source = [(red_el.is_some(), &red), (green_el.is_some(), &green), (blue_el.is_some(), &blue), (alpha_el.is_some(), &alpha)]
.into_iter()
.find_map(|(present, list)| present.then_some(list.lane(lane)));
// Get the common width and height of the channels, which must have equal dimensions // The channels must have equal dimensions; an unusable lane serves a
let channel_dimensions = [ // zero-size raster, which renders as nothing (the legacy form dropped it)
red.as_ref().map(|r| (r.element().width, r.element().height)), let channel_dimensions = [
green.as_ref().map(|g| (g.element().width, g.element().height)), red_el.map(|r| (r.width, r.height)),
blue.as_ref().map(|b| (b.element().width, b.element().height)), green_el.map(|g| (g.width, g.height)),
alpha.as_ref().map(|a| (a.element().width, a.element().height)), blue_el.map(|b| (b.width, b.height)),
]; alpha_el.map(|a| (a.width, a.height)),
if channel_dimensions.iter().all(Option::is_none) ];
|| channel_dimensions let mismatched = channel_dimensions
.iter() .iter()
.flatten() .flatten()
.any(|&(x, y)| channel_dimensions.iter().flatten().any(|&(other_x, other_y)| x != other_x || y != other_y)) .any(|&(x, y)| channel_dimensions.iter().flatten().any(|&(other_x, other_y)| x != other_x || y != other_y));
{ let (Some(source), Some(&(width, height)), false) = (attr_source, channel_dimensions.iter().flatten().next(), mismatched) else {
return None; return Ok((
Raster::new_cpu(Image::default()),
Attr(DAffine2::IDENTITY),
Attr(<BlendModeAttr as Attribute>::default()),
Attr(1.),
Attr(1.),
Attr(false),
Attr(<EditorLayerPath as Attribute>::default()),
));
};
// Create a new image for the output element
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_el.and_then(|r| r.get_pixel(x, y)) {
image_pixel.set_red(r.l().cast_linear_channel());
} else {
image_pixel.set_red(Channel::from_linear(0.));
} }
let &(width, height) = channel_dimensions.iter().flatten().next()?; if let Some(g) = green_el.and_then(|g| g.get_pixel(x, y)) {
image_pixel.set_green(g.l().cast_linear_channel());
// Create a new image for the output element } else {
let mut image = Image::new(width, height, Color::TRANSPARENT); image_pixel.set_green(Channel::from_linear(0.));
// 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.element().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.element().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.element().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.element().get_pixel(x, y)) {
image_pixel.set_alpha(a.l().cast_linear_channel());
} else {
image_pixel.set_alpha(Channel::from_linear(1.));
}
}
} }
if let Some(b) = blue_el.and_then(|b| b.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_el.and_then(|a| a.get_pixel(x, y)) {
image_pixel.set_alpha(a.l().cast_linear_channel());
} else {
image_pixel.set_alpha(Channel::from_linear(1.));
}
}
}
Some(Item::from_parts(Raster::new_cpu(image), attributes)) // The layer path re-parks into the arena so the borrow outlives the batch
}) let layer_path: Vec<core_types::uuid::NodeId> = source.attr::<EditorLayerPath>().to_vec();
.collect() let (layer_path, _) = ctx.arena().alloc(layer_path).ok_or(GraphError {
kind: core_types::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
})?;
Ok((
Raster::new_cpu(image),
Attr(source.attr::<TransformAttr>()),
Attr(source.attr::<BlendModeAttr>()),
Attr(source.attr::<Opacity>()),
Attr(source.attr::<OpacityFill>()),
Attr(source.attr::<ClippingMask>()),
Attr(layer_path.as_slice()),
))
}
/// The combined level's count is the longest channel's; a lower-bound channel
/// keeps the result a lower bound too, and consumers drain to past-end.
fn combine_channels_extent(
_primary: ValueIn<'_, ()>,
red: ListIn<'_, Raster<CPU>>,
green: ListIn<'_, Raster<CPU>>,
blue: ListIn<'_, Raster<CPU>>,
alpha: ListIn<'_, Raster<CPU>>,
level: LevelIn,
) -> GPoll<Extent> {
match level.top() {
true => red.total().zip(green.total()).zip(blue.total()).zip(alpha.total()).map(|(((red, green), blue), alpha)| {
let totals = [red, green, blue, alpha];
let bound = totals
.iter()
.map(|extent| match extent {
Extent::Exactly(count) | Extent::AtLeast(count) => *count,
Extent::Free => 0,
})
.max()
.unwrap_or(0);
match totals.iter().all(|extent| matches!(extent, Extent::Exactly(_))) {
true => Extent::Exactly(bound),
false => Extent::AtLeast(bound),
}
}),
false => GPoll::Final(Extent::Exactly(1)),
}
} }
#[node_macro::node(category("Raster"))] #[node_macro::node(category("Raster"))]
pub fn mask( pub fn mask(
_: impl Ctx, _: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// The image to be masked. /// The image to be masked.
image: List<Raster<CPU>>, (mut image, lane_transform): (Raster<CPU>, Attr<TransformAttr>),
/// The stencil to be used for masking. /// The stencil to be used for masking.
#[expose] #[expose]
stencil: List<Raster<CPU>>, stencil: IList<Raster<CPU>>,
) -> List<Raster<CPU>> { ) -> (Raster<CPU>, Attr<TransformAttr>) {
// TODO: Figure out what it means to support multiple stencil items? // TODO: Figure out what it means to support multiple stencil items?
let Some(stencil) = stencil.into_iter().next() else { if stencil.is_empty() {
// No stencil provided so we return the original image // No stencil provided so we return the original image
return image; return (image, Attr(*lane_transform));
}; }
let stencil_size = DVec2::new(stencil.element().width as f64, stencil.element().height as f64); let stencil_element = stencil.element_ref(0);
let stencil_transform: DAffine2 = stencil.lane(0).attr::<TransformAttr>();
let stencil_size = DVec2::new(stencil_element.width as f64, stencil_element.height as f64);
image let image_size = DVec2::new(image.width as f64, image.height as f64);
.into_iter() let mask_size = stencil_transform.scale_magnitudes();
.filter_map(|mut row| {
let image_size = DVec2::new(row.element().width as f64, row.element().height as f64);
let stencil_transform: DAffine2 = stencil.attribute_cloned_or_default(ATTR_TRANSFORM);
let mask_size = stencil_transform.scale_magnitudes();
if mask_size == DVec2::ZERO { // A degenerate stencil serves a zero-size raster, which renders as
return None; // nothing (the legacy form dropped the lane)
} if mask_size == DVec2::ZERO {
return (Raster::new_cpu(Image::default()), Attr(*lane_transform));
}
// Transforms a point from the background image to the foreground image // Transforms a point from the background image to the foreground image
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); let transform_attribute: DAffine2 = *lane_transform;
let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size); let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size);
let stencil_transform_inverse = stencil_transform.inverse(); let stencil_transform_inverse = stencil_transform.inverse();
for y in 0..row.element().height { for y in 0..image.height {
for x in 0..row.element().width { for x in 0..image.width {
let image_point = DVec2::new(x as f64, y as f64); let image_point = DVec2::new(x as f64, y as f64);
let mask_point = bg_to_fg.transform_point2(image_point); let mask_point = bg_to_fg.transform_point2(image_point);
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point); let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
let mask_point = stencil_transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE)); let mask_point = stencil_transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_transform.inverse()).transform_point2(mask_point); let mask_point = (DAffine2::from_scale(stencil_size) * stencil_transform.inverse()).transform_point2(mask_point);
let image_pixel = row.element_mut().data_mut().get_pixel_mut(x, y).unwrap(); let image_pixel = image.data_mut().get_pixel_mut(x, y).unwrap();
let mask_pixel = stencil.element().sample(mask_point); let mask_pixel = stencil_element.sample(mask_point);
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel()); *image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
} }
} }
Some(row) (image, Attr(transform_attribute))
})
.collect()
} }
/// The per-lane extend, shared with the brush's plain callers. /// The per-lane extend, shared with the brush's plain callers.
@@ -348,7 +398,7 @@ pub fn noise_pattern(
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_jitter")] #[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_jitter")]
#[default(1.)] #[default(1.)]
cellular_jitter: f64, cellular_jitter: f64,
) -> List<Raster<CPU>> { ) -> (Raster<CPU>, Attr<TransformAttr>) {
let footprint = ctx.footprint(); let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space(); let viewport_bounds = footprint.viewport_bounds_in_local_space();
@@ -364,9 +414,9 @@ pub fn noise_pattern(
size = intersection.size(); size = intersection.size();
} }
// If the image would not be visible, return an empty image // A culled pattern serves a zero-size raster, which renders as nothing
if size.x <= 0. || size.y <= 0. { if size.x <= 0. || size.y <= 0. {
return List::new(); return (Raster::new_cpu(Image::default()), Attr(DAffine2::IDENTITY));
} }
let transform = DAffine2::from_translation(offset) * DAffine2::from_scale(size); let transform = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
@@ -412,7 +462,7 @@ pub fn noise_pattern(
} }
} }
return List::new_from_item(Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform)); return (Raster::new_cpu(image), Attr(transform));
} }
}; };
noise.set_noise_type(Some(noise_type)); noise.set_noise_type(Some(noise_type));
@@ -470,11 +520,11 @@ pub fn noise_pattern(
} }
} }
List::new_from_item(Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform)) (Raster::new_cpu(image), Attr(transform))
} }
#[node_macro::node(category("Raster: Pattern"))] #[node_macro::node(category("Raster: Pattern"))]
pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> List<Raster<CPU>> { pub fn mandelbrot(ctx: impl Ctx + ExtractFootprint, _primary: ()) -> (Raster<CPU>, Attr<TransformAttr>) {
let footprint = ctx.footprint(); let footprint = ctx.footprint();
let viewport_bounds = footprint.viewport_bounds_in_local_space(); let viewport_bounds = footprint.viewport_bounds_in_local_space();
@@ -484,9 +534,9 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> List<Raster<CPU>> {
let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO); let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO);
// If the image would not be visible, return an empty image // A culled pattern serves a zero-size raster, which renders as nothing
if size.x <= 0. || size.y <= 0. { if size.x <= 0. || size.y <= 0. {
return List::new(); return (Raster::new_cpu(Image::default()), Attr(DAffine2::IDENTITY));
} }
let scale = footprint.scale(); let scale = footprint.scale();
@@ -508,14 +558,14 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> List<Raster<CPU>> {
} }
} }
List::new_from_item( (
Item::new_from_element(Raster::new_cpu(Image { Raster::new_cpu(Image {
width, width,
height, height,
data, data,
..Default::default() ..Default::default()
})) }),
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(offset) * DAffine2::from_scale(size)), Attr(DAffine2::from_translation(offset) * DAffine2::from_scale(size)),
) )
} }