Clean up document message wrappers around proto nodes so they're now used directly (#4101)

* Rename the 'Identity' node to 'Passthrough' internally

* Rename the 'Memoize'  node to 'Cache' internally

* Let skip_impl proto nodes auto-generate as document node definitions

* Remove the wrapper 'Passthrough' node from document_node_definitions.rs

* Remove the wrapper 'Cache' node from document_node_definitions.rs

* Remove the wrapper 'Monitor' node from document_node_definitions.rs

* Remove the wrapper 'Noise Pattern' node from document_node_definitions.rs

* Remove the wrapper 'Brush' node from document_node_definitions.rs

* Remove the wrapper 'Transform' node from document_node_definitions.rs

* Code review improvements

* Rename Cache node back to Memoize

* More code review
This commit is contained in:
Keavon Chambers
2026-05-03 19:26:36 -07:00
committed by GitHub
parent b27b4c6be7
commit 21e5e06b0b
29 changed files with 408 additions and 662 deletions

View File

@@ -187,30 +187,34 @@ pub fn blend_with_mode(background: TableRow<Raster<CPU>>, foreground: TableRow<R
/// Generates the brush strokes painted with the Brush tool as a raster image.
/// If an input image is supplied, strokes are drawn on top of it, expanding bounds as needed.
#[node_macro::node(category(""))]
#[node_macro::node(category("Raster"))]
async fn brush(
_: impl Ctx,
/// Optional raster content that may be drawn onto.
mut image: Table<Raster<CPU>>,
mut background: Table<Raster<CPU>>,
/// The list of brush stroke paths drawn by the Brush tool, with each including both its coordinates and styles.
strokes: Table<BrushStroke>,
trace: Table<BrushStroke>,
/// Internal cache data used to accelerate rendering of the brush content.
cache: BrushCache,
) -> Table<Raster<CPU>> {
if image.is_empty() {
image.push(TableRow::default());
if background.is_empty() {
background.push(TableRow::default());
}
// TODO: Find a way to handle more than one item
let table_row = image.clone_row(0).expect("Expected the one item we just pushed");
let table_row = background.clone_row(0).expect("Expected the one item we just pushed");
let bounds = Table::new_from_row(table_row.clone()).bounding_box(DAffine2::IDENTITY, false);
let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] };
let image_bbox = AxisAlignedBbox { start, end };
let stroke_bbox = strokes.iter_element_values().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
let bbox = if image_bbox.size().length() < 0.1 { stroke_bbox } else { stroke_bbox.union(&image_bbox) };
let background_bbox = AxisAlignedBbox { start, end };
let stroke_bbox = trace.iter_element_values().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
let bbox = if background_bbox.size().length() < 0.1 {
stroke_bbox
} else {
stroke_bbox.union(&background_bbox)
};
let background_bounds = bbox.to_transform();
let mut draw_strokes: Vec<_> = strokes
let mut draw_strokes: Vec<_> = trace
.iter_element_values()
.filter(|&s| !matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore))
.cloned()
@@ -278,12 +282,12 @@ async fn brush(
actual_image = blend_with_mode(actual_image, stroke_texture, stroke.style.blend_mode, (stroke.style.color.a() * 100.) as f64);
}
let has_erase_or_restore_strokes = strokes.iter_element_values().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore));
let has_erase_or_restore_strokes = trace.iter_element_values().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore));
if has_erase_or_restore_strokes {
let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE);
let mut erase_restore_mask = TableRow::new_from_element(Raster::new_cpu(opaque_image)).with_attribute(ATTR_TRANSFORM, background_bounds);
for stroke in strokes.into_iter().map(|row| row.into_element()) {
for stroke in trace.into_iter().map(|row| row.into_element()) {
let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() {
let tex = create_brush_texture(&stroke.style).await;
@@ -320,15 +324,15 @@ async fn brush(
let clip: bool = actual_image.attribute_cloned_or_default(ATTR_CLIPPING_MASK);
let layer: Table<NodeId> = actual_image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
*image.element_mut(0).unwrap() = actual_image.into_element();
image.set_attribute(ATTR_TRANSFORM, 0, transform);
image.set_attribute(ATTR_BLEND_MODE, 0, blend_mode);
image.set_attribute(ATTR_OPACITY, 0, opacity);
image.set_attribute(ATTR_OPACITY_FILL, 0, fill);
image.set_attribute(ATTR_CLIPPING_MASK, 0, clip);
image.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer);
*background.element_mut(0).unwrap() = actual_image.into_element();
background.set_attribute(ATTR_TRANSFORM, 0, transform);
background.set_attribute(ATTR_BLEND_MODE, 0, blend_mode);
background.set_attribute(ATTR_OPACITY, 0, opacity);
background.set_attribute(ATTR_OPACITY_FILL, 0, fill);
background.set_attribute(ATTR_CLIPPING_MASK, 0, clip);
background.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer);
image
background
}
pub fn blend_image_closure(foreground: TableRow<Raster<CPU>>, mut background: TableRow<Raster<CPU>>, map_fn: impl Fn(Color, Color) -> Color) -> TableRow<Raster<CPU>> {

View File

@@ -6,15 +6,19 @@ use std::hash::Hasher;
use std::sync::Arc;
use std::sync::Mutex;
/// Caches the output of a given node called with a specific input.
/// Helps speed up repeated renders in a computationally-heavy part of the node graph.
///
/// A cache miss occurs when the Option is None. In this case, the node evaluates the inner node and memoizes (stores) the result.
///
/// A cache hit occurs when the Option is Some and has a stored hash matching the hash of the call argument. In this case, the node returns the cached value without re-evaluating the inner node.
///
/// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache.
#[node_macro::node(category(""), path(graphene_core::memo), skip_impl)]
async fn memo<I: CacheHash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, node: impl Node<I, Output = T>) -> T {
/// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed.
#[node_macro::node(category("General"), path(graphene_core::memo), skip_impl)]
async fn memoize<I: CacheHash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, content: impl Node<I, Output = T>) -> T {
// Caches the output of a given node called with a specific input.
//
// A cache miss occurs when the Option is None. In this case, the node evaluates the inner node and memoizes (stores) the result.
//
// A cache hit occurs when the Option is Some and has a stored hash matching the hash of the call argument. In this case, the node returns the cached value without re-evaluating the inner node.
//
// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache.
let mut hasher = DefaultHasher::new();
input.cache_hash(&mut hasher);
let hash = hasher.finish();
@@ -23,23 +27,23 @@ async fn memo<I: CacheHash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data
return data;
}
let value = node.eval(input).await;
let value = content.eval(input).await;
*cache.lock().unwrap() = Some((hash, value.clone()));
value
}
type MonitorValue<I, T> = Arc<Mutex<Option<Arc<IORecord<I, T>>>>>;
/// Caches the output of the last graph evaluation for introspection.
#[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), skip_impl)]
/// The Monitor node is used by the editor to access the data flowing through it.
#[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"), skip_impl)]
async fn monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>(
input: I,
#[allow(clippy::type_complexity)]
#[data]
io: MonitorValue<I, T>,
node: impl Node<I, Output = T>,
content: impl Node<I, Output = T>,
) -> T {
let output = node.eval(input.clone()).await;
let output = content.eval(input.clone()).await;
*io.lock().unwrap() = Some(Arc::new(IORecord { input, output: output.clone() }));
output
}

View File

@@ -4,12 +4,10 @@ use std::marker::PhantomData;
// Re-export TypeNode from core-types for convenience
pub use core_types::ops::TypeNode;
// TODO: Rename to "Passthrough" and make this the node that users use, not the one defined in document_node_definitions.rs
/// Passes-through the input value without changing it.
/// This is useful for rerouting wires for organization purposes.
#[node_macro::node(category(""), skip_impl)]
fn identity<'i, T: 'i + Send>(value: T) -> T {
value
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
#[node_macro::node(category("General"), skip_impl)]
fn passthrough<'i, T: 'i + Send>(_: impl Ctx, content: T) -> T {
content
}
#[node_macro::node(category(""), skip_impl)]
@@ -27,7 +25,7 @@ mod test {
use super::*;
#[test]
pub fn identity_node() {
assert_eq!(identity(&4), &4);
pub fn passthrough_node() {
assert_eq!(passthrough((), &4), &4);
}
}

View File

@@ -49,7 +49,7 @@ const TX: f32 = 0.1;
// Paper: <https://www.researchgate.net/publication/220182411_Single_Image_Haze_Removal_Using_Dark_Channel_Prior>
// TODO: Make this algorithm work with negative strength values
fn dehaze_image(image: DynamicImage, strength: f64) -> DynamicImage {
// TODO: Break out this pair of steps into its own node, with a memoize node which caches the pair of outputs, so the strength can be adjusted without recomputing these two steps.
// TODO: Break out this pair of steps into its own node, with a Memoize node which caches the pair of outputs, so the strength can be adjusted without recomputing these two steps.
let dark_channel = compute_dark_channel(&image);
let atmospheric_light = estimate_atmospheric_light(&image, &dark_channel);

View File

@@ -293,25 +293,40 @@ pub fn image(_: impl Ctx, _primary: (), image: Image<Color>) -> Table<Raster<CPU
Table::new_from_element(Raster::new_cpu(image))
}
/// Generates customizable procedural noise patterns.
#[node_macro::node(category("Raster: Pattern"))]
#[allow(clippy::too_many_arguments)]
pub fn noise_pattern(
ctx: impl ExtractFootprint + Ctx,
_primary: (),
clip: bool,
#[default(true)] clip: bool,
seed: u32,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_scale")]
#[default(10.)]
scale: f64,
noise_type: NoiseType,
domain_warp_type: DomainWarpType,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_noise_type")] noise_type: NoiseType,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_domain_warp_type")] domain_warp_type: DomainWarpType,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_domain_warp_amplitude")]
#[default(100.)]
domain_warp_amplitude: f64,
fractal_type: FractalType,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_type")] fractal_type: FractalType,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_octaves")]
#[default(3)]
fractal_octaves: u32,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_lacunarity")]
#[default(2.)]
fractal_lacunarity: f64,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_gain")]
#[default(0.5)]
fractal_gain: f64,
fractal_weighted_strength: f64,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_weighted_strength")] fractal_weighted_strength: f64,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_ping_pong_strength")]
#[default(2.)]
fractal_ping_pong_strength: f64,
cellular_distance_function: CellularDistanceFunction,
cellular_return_type: CellularReturnType,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_distance_function")] cellular_distance_function: CellularDistanceFunction,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_return_type")] cellular_return_type: CellularReturnType,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_jitter")]
#[default(1.)]
cellular_jitter: f64,
) -> Table<Raster<CPU>> {
let footprint = ctx.footprint();

View File

@@ -10,7 +10,7 @@ use graphic_types::raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
/// Applies the specified transform to the input value, which may be a graphic type or another transform.
#[node_macro::node(category(""))]
#[node_macro::node(category("Math: Transform"))]
async fn transform<T: ApplyTransform + 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint,
#[implementations(
@@ -24,10 +24,12 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
Context -> Table<GradientStops>,
)]
content: impl Node<Context<'static>, Output = T>,
translation: DVec2,
rotation: f64,
#[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2,
#[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64,
#[widget(ParsedWidgetOverride::Custom = "transform_scale")]
#[default(1., 1.)]
scale: DVec2,
skew: DVec2,
#[widget(ParsedWidgetOverride::Custom = "transform_skew")] skew: DVec2,
) -> T {
let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation);
let skew = DAffine2::from_cols_array(&[1., skew.y.to_radians().tan(), skew.x.to_radians().tan(), 1., 0., 0.]);