From b216cbd6d38676f35fb38d8148d6beb1f234b613 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Wed, 6 May 2026 17:19:57 +0900 Subject: [PATCH 1/2] Add conversion from Fill to Table --- .../libraries/graphic-types/src/graphic.rs | 21 ++++++++++++- .../libraries/vector-types/src/gradient.rs | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 701d275c3c..221987cd33 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -5,7 +5,7 @@ use core_types::ops::TableConvert; use core_types::render_complexity::RenderComplexity; use core_types::table::{Table, TableRow}; use core_types::uuid::NodeId; -use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color}; +use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color}; use dyn_any::DynAny; use glam::DAffine2; use raster_types::{CPU, GPU, Raster}; @@ -13,6 +13,7 @@ use vector_types::GradientStops; // use vector_types::Vector; pub use vector_types::Vector; +use vector_types::vector::style::Fill; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. #[derive(Clone, Debug, CacheHash, PartialEq, DynAny)] @@ -193,6 +194,24 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic output } +/// Converts a `Fill` enum into the `Table` representation used as paint storage. +/// TODO: Remove once all paint sources flow through `Table` directly without going through the `Fill` enum. +pub fn fill_to_paint(fill: &Fill) -> Option> { + match fill { + Fill::None => None, + Fill::Solid(color) => Some(Table::new_from_element((*color).into())), + Fill::Gradient(gradient) => { + let gradient_row = TableRow::new_from_element(gradient.stops.clone()) + .with_attribute(ATTR_TRANSFORM, gradient.to_transform()) + .with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type) + .with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method); + let gradient_table = Table::new_from_row(gradient_row); + + Some(Table::new_from_element(Graphic::Gradient(gradient_table))) + } + } +} + /// Maps from a concrete element type to its corresponding `Graphic` enum variant, /// enabling type-directed casting of typed `Table`s from a `Graphic` value. pub trait TryFromGraphic: Clone + Sized { diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index a286ccf922..a60e25c67f 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -523,6 +523,12 @@ impl Gradient { Some(index) } + + /// Builds the affine that places the gradient endpoints at `start` and `end` when applied to canonical gradient space (0,0) -> (1,0) + pub fn to_transform(&self) -> DAffine2 { + let direction = self.end - self.start; + DAffine2::from_cols(direction, direction.perp(), self.start) + } } // TODO: Eventually remove this migration document upgrade code @@ -556,3 +562,27 @@ impl core_types::bounds::BoundingBox for GradientStops { core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)]) } } + +#[cfg(test)] +mod tests { + use super::*; + use glam::DVec2; + + fn linear_gradient(start: DVec2, end: DVec2) -> Gradient { + Gradient { start, end, ..Default::default() } + } + + #[test] + fn to_transform_roundtrip() { + let cases = [(DVec2::ZERO, DVec2::X), (DVec2::new(10., 20.), DVec2::new(50., 30.)), (DVec2::new(-5., -5.), DVec2::new(5., 3.))]; + + for (start, end) in cases { + let transform = linear_gradient(start, end).to_transform(); + let recovered_start = transform.transform_point2(DVec2::ZERO); + let recovered_end = transform.transform_point2(DVec2::X); + + assert!((recovered_start - start).length() < 1e-10); + assert!((recovered_end - end).length() < 1e-10); + } + } +} From 7a0243336c07d528a62ddd9bb685b12f0b9fd136 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Thu, 7 May 2026 23:30:56 +0900 Subject: [PATCH 2/2] Refactor Vector vello renderer for Gradient / Color --- .../libraries/rendering/src/renderer.rs | 135 ++++++++++-------- 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 9c166d6c4f..07fc574fbd 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -17,6 +17,7 @@ use core_types::{ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use graphene_hash::CacheHashWrapper; +use graphic_types::graphic::fill_to_paint; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::subpath::Subpath; @@ -1186,70 +1187,86 @@ impl Render for Table { let use_layer = can_draw_aligned_stroke; let wants_stroke_below = stroke.as_ref().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); - // Closures to avoid duplicated fill/stroke drawing logic - let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match element.style.fill() { - Fill::Solid(color) => { - let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); - scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path); - } - Fill::Gradient(gradient) => { - let mut stops = peniko::ColorStops::new(); - for (position, color, _) in gradient.stops.interpolated_samples() { - stops.push(peniko::ColorStop { - offset: position as f32, - color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])), - }); - } - - let bounds = element.nonzero_bounding_box(); - let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); - - let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. { - parent_transform.inverse() - } else { - Default::default() - }; - let mod_points = inverse_parent_transform * multiplied_transform * bound_transform; + // TODO: This conversion is only necessary during the transition period from Fill to Table + let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| { + let Some(paint_table) = fill_to_paint(element.style.fill()) else { + return; + }; - let start = mod_points.transform_point2(gradient.start); - let end = mod_points.transform_point2(gradient.end); + for paint_idx in 0..paint_table.len() { + let Some(paint) = paint_table.element(paint_idx) else { continue }; + match paint { + Graphic::Color(table) => { + let Some(color) = table.element(0) else { continue }; - let fill = peniko::Brush::Gradient(peniko::Gradient { - kind: match gradient.gradient_type { - GradientType::Linear => peniko::LinearGradientPosition { - start: to_point(start), - end: to_point(end), - } - .into(), - GradientType::Radial => { - let radius = start.distance(end); - peniko::RadialGradientPosition { - start_center: to_point(start), - start_radius: 0., - end_center: to_point(start), - end_radius: radius as f32, - } - .into() + let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); + scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path); + } + Graphic::Gradient(stops_table) => { + let Some(stops) = stops_table.element(0) else { continue }; + let gradient_type: GradientType = stops_table.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0); + let gradient_transform: DAffine2 = stops_table.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let spread_method: GradientSpreadMethod = stops_table.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0); + + let mut peniko_stops = peniko::ColorStops::new(); + for (position, color, _) in stops.interpolated_samples() { + peniko_stops.push(peniko::ColorStop { + offset: position as f32, + color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])), + }); } - }, - extend: match gradient.spread_method { - GradientSpreadMethod::Pad => peniko::Extend::Pad, - GradientSpreadMethod::Reflect => peniko::Extend::Reflect, - GradientSpreadMethod::Repeat => peniko::Extend::Repeat, - }, - stops, - interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied, - ..Default::default() - }); - let inverse_element_transform = if element_transform.matrix2.determinant() != 0. { - element_transform.inverse() - } else { - Default::default() + + let bounds = element.nonzero_bounding_box(); + let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); + + let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. { + parent_transform.inverse() + } else { + Default::default() + }; + let mod_points = inverse_parent_transform * multiplied_transform * bound_transform * gradient_transform; + + let start = mod_points.transform_point2(DVec2::ZERO); + let end = mod_points.transform_point2(DVec2::X); + + let fill = peniko::Brush::Gradient(peniko::Gradient { + kind: match gradient_type { + GradientType::Linear => peniko::LinearGradientPosition { + start: to_point(start), + end: to_point(end), + } + .into(), + GradientType::Radial => { + let radius = start.distance(end); + peniko::RadialGradientPosition { + start_center: to_point(start), + start_radius: 0., + end_center: to_point(start), + end_radius: radius as f32, + } + .into() + } + }, + extend: match spread_method { + GradientSpreadMethod::Pad => peniko::Extend::Pad, + GradientSpreadMethod::Reflect => peniko::Extend::Reflect, + GradientSpreadMethod::Repeat => peniko::Extend::Repeat, + }, + stops: peniko_stops, + interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied, + ..Default::default() + }); + let inverse_element_transform = if element_transform.matrix2.determinant() != 0. { + element_transform.inverse() + } else { + Default::default() + }; + let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array()); + scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), path); + } + _ => todo!(), }; - let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array()); - scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), path); } - Fill::None => {} }; // Branching vectors without regions (e.g. mesh grids) need face-by-face fill rendering.