Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions node-graph/libraries/core-types/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ pub const ATTR_SPREAD_METHOD: &str = "spread_method";
/// Gradient's `GradientType` (`Linear` or `Radial`).
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";

/// List<Graphic> data for fill.
pub const ATTR_FILL_GRAPHIC: &str = "fill_graphic";

// ========================
// TRAIT: AnyAttributeValue
// ========================
Expand Down
107 changes: 105 additions & 2 deletions node-graph/libraries/graphic-types/src/graphic.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::list::List;
use core_types::list::{Item, List};
use core_types::ops::ListConvert;
use core_types::render_complexity::RenderComplexity;
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};
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)]
Expand Down Expand Up @@ -169,6 +170,24 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
output
}

/// Converts a `Fill` enum into the `List<Graphic>` representation used as paint storage.
/// TODO: Remove once all paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
pub fn fill_to_graphic_list(fill: &Fill) -> Option<List<Graphic>> {
match fill {
Fill::None => None,
Fill::Solid(color) => Some(List::new_from_element((*color).into())),
Fill::Gradient(gradient) => {
let gradient_row = Item::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_list = List::new_from_item(gradient_row);

Some(List::new_from_element(Graphic::Gradient(gradient_list)))
}
}
}

/// Maps from a concrete element type to its corresponding `Graphic` enum variant,
/// enabling type-directed casting of typed `List`s from a `Graphic` value.
pub trait TryFromGraphic: Clone + Sized {
Expand Down Expand Up @@ -337,6 +356,14 @@ impl Graphic {
_ => false,
}
}

pub fn is_opaque(&self) -> bool {
match self {
Graphic::Color(list) => list.element(0).is_some_and(|color| color.is_opaque()),
Graphic::Gradient(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.a() >= 1. - f32::EPSILON)),
_ => false,
}
}
}

impl BoundingBox for Graphic {
Expand Down Expand Up @@ -462,3 +489,79 @@ impl<T: Clone> OmitIndex for List<T> {
self.omit_index(self.len() - index)
}
}

#[cfg(test)]
mod graphic_is_opaque_tests {
use vector_types::{GradientSpreadMethod, GradientStop};

use super::*;

fn color_graphic(alpha: f64) -> Graphic {
let color = Color::from_rgbaf32(1.0, 0.0, 0.0, alpha as f32).unwrap();
Graphic::Color(List::new_from_element(color))
}

fn gradient_graphic(gradient: GradientStops) -> Graphic {
let mut gradient_list = List::new_from_element(gradient);
gradient_list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Pad);
Graphic::Gradient(gradient_list)
}

#[test]
fn opaque_color_is_opaque() {
let g = color_graphic(1.0);
assert!(g.is_opaque());
}

#[test]
fn transparent_color_is_not_opaque() {
let g = color_graphic(0.5);
assert!(!g.is_opaque());
}

#[test]
fn vector_is_not_opaque() {
let g = Graphic::Vector(List::default());
assert!(!g.is_opaque());
}

#[test]
fn gradient_with_all_opaque_stops_is_opaque() {
let color_1 = Color::from_rgbaf32(1.0, 0.0, 0.0, 1.).unwrap();
let color_2 = Color::from_rgbaf32(1.0, 0.0, 0.0, 1.).unwrap();
let gradient = GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: color_1,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: color_2,
},
]);
let g = gradient_graphic(gradient);
assert!(g.is_opaque());
}

#[test]
fn gradient_with_transparent_stop_is_not_opaque() {
let color_1 = Color::from_rgbaf32(1.0, 0.0, 0.0, 0.5).unwrap();
let color_2 = Color::from_rgbaf32(1.0, 0.0, 0.0, 1.).unwrap();
let gradient = GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: color_1,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: color_2,
},
]);
let g = gradient_graphic(gradient);
assert!(!g.is_opaque());
}
}
179 changes: 133 additions & 46 deletions node-graph/libraries/rendering/src/render_ext.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,78 @@
use crate::renderer::{RenderParams, format_transform_matrix};
use crate::{Render, RenderSvgSegmentList, SvgRender};
use core_types::color::SRGBA8;
use core_types::list::List;
use core_types::uuid::generate_uuid;
use glam::DAffine2;
use graphic_types::vector_types::gradient::{Gradient, GradientType};
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use core_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
use glam::{DAffine2, DVec2};
use graphic_types::Graphic;
use graphic_types::vector_types::gradient::GradientType;
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write;
use vector_types::GradientStops;
use vector_types::gradient::GradientSpreadMethod;

pub trait RenderExt {
type Output;
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, render_params: &RenderParams) -> Self::Output;
#[allow(clippy::too_many_arguments)]
fn render(
&self,
svg_defs: &mut String,
item_transform: DAffine2,
element_transform: DAffine2,
stroke_transform: DAffine2,
bounds: DAffine2,
transformed_bounds: DAffine2,
render_params: &RenderParams,
) -> Self::Output;
}

impl RenderExt for List<Color> {
type Output = String;

fn render(
&self,
_svg_defs: &mut String,
_item_transform: DAffine2,
_element_transform: DAffine2,
_stroke_transform: DAffine2,
_bounds: DAffine2,
_transformed_bounds: DAffine2,
_render_params: &RenderParams,
) -> Self::Output {
let Some(color) = self.element(0) else { return r#" fill="none""#.to_string() };

let mut result = format!(r##" fill="#{}""##, SRGBA8::from(*color).to_rgb_hex());
if color.a() < 1. {
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}

result
}
}

impl RenderExt for Gradient {
impl RenderExt for List<GradientStops> {
type Output = u64;

/// Adds the gradient def through mutating the first argument, returning the gradient ID.
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, _render_params: &RenderParams) -> Self::Output {
fn render(
&self,
svg_defs: &mut String,
_item_transform: DAffine2,
element_transform: DAffine2,
stroke_transform: DAffine2,
bounds: DAffine2,
transformed_bounds: DAffine2,
_render_params: &RenderParams,
) -> Self::Output {
let mut stop = String::new();
for (position, color, original_midpoint) in self.stops.interpolated_samples() {

let Some(stops) = self.element(0) else { return 0 };
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
let gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);

for (position, color, original_midpoint) in stops.interpolated_samples() {
stop.push_str("<stop");
if position != 0. {
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
Expand All @@ -33,9 +87,9 @@ impl RenderExt for Gradient {
stop.push_str(" />")
}

let transform_points = element_transform * stroke_transform * bounds;
let start = transform_points.transform_point2(self.start);
let end = transform_points.transform_point2(self.end);
let transform_points = element_transform * stroke_transform * bounds * gradient_transform;
let start = transform_points.transform_point2(DVec2::ZERO);
let end = transform_points.transform_point2(DVec2::X);

let gradient_transform = if transformed_bounds.matrix2.determinant() != 0. {
transformed_bounds.inverse()
Expand All @@ -49,15 +103,15 @@ impl RenderExt for Gradient {
format!(r#" gradientTransform="{gradient_transform}""#)
};

let spread_method = if self.spread_method == GradientSpreadMethod::Pad {
let spread_method = if spread_method == GradientSpreadMethod::Pad {
String::new()
} else {
format!(r#" spreadMethod="{}""#, self.spread_method.svg_name())
format!(r#" spreadMethod="{}""#, spread_method.svg_name())
};

let gradient_id = generate_uuid();

match self.gradient_type {
match gradient_type {
GradientType::Linear => {
let _ = write!(
svg_defs,
Expand All @@ -79,35 +133,14 @@ impl RenderExt for Gradient {
}
}

impl RenderExt for Fill {
type Output = String;

/// Renders the fill, adding necessary defs through mutating the first argument.
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, render_params: &RenderParams) -> Self::Output {
match self {
Self::None => r#" fill="none""#.to_string(),
Self::Solid(color) => {
let mut result = format!(r##" fill="#{}""##, SRGBA8::from(*color).to_rgb_hex());
if color.a() < 1. {
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
result
}
Self::Gradient(gradient) => {
let gradient_id = gradient.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
format!(r##" fill="url('#{gradient_id}')""##)
}
}
}
}

impl RenderExt for Stroke {
type Output = String;

/// Provide the SVG attributes for the stroke.
fn render(
&self,
_svg_defs: &mut String,
_item_transform: DAffine2,
_element_transform: DAffine2,
_stroke_transform: DAffine2,
_bounds: DAffine2,
Expand Down Expand Up @@ -165,18 +198,72 @@ impl RenderExt for Stroke {
}
}

impl RenderExt for PathStyle {
impl RenderExt for List<Graphic> {
type Output = String;

/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
#[allow(clippy::too_many_arguments)]
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, render_params: &RenderParams) -> String {
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
let stroke_attribute = self
.stroke
.as_ref()
.map(|stroke| stroke.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params))
.unwrap_or_default();
format!("{fill_attribute}{stroke_attribute}")
fn render(
&self,
svg_defs: &mut String,
item_transform: DAffine2,
element_transform: DAffine2,
stroke_transform: DAffine2,
bounds: DAffine2,
transformed_bounds: DAffine2,
render_params: &RenderParams,
) -> Self::Output {
let fill_graphic = self.element(0);

match fill_graphic {
Some(Graphic::Color(color_list)) => color_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params),
Some(Graphic::Gradient(gradient_list)) => {
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
format!(r##" fill="url(#{gradient_id})""##)
}
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) => {
render_svg_fill_pattern(svg_defs, self, item_transform, bounds, render_params)
.map(|id| format!(r##" fill="url(#{id})""##))
.unwrap_or_else(|| r#" fill="none""#.to_string())
}
None => r#" fill="none""#.to_string(),
}
}
}

/// Emits an SVG `<pattern>` paint server into `svg_defs` that renders the given graphic list as the fill content, and returns the pattern ID.
/// Currently, this function is only used for clipping-based filling, not for tiling.
fn render_svg_fill_pattern(svg_defs: &mut String, fill_graphic_list: &List<Graphic>, item_transform: DAffine2, bounds: DAffine2, render_params: &RenderParams) -> Option<String> {
let min = bounds.transform_point2(DVec2::ZERO);
let max = bounds.transform_point2(DVec2::ONE);
let size = max - min;
if size.x <= 0. || size.y <= 0. {
return None;
}

// Render the pattern content recursively
let mut content = SvgRender::new();
fill_graphic_list.render_svg(&mut content, &render_params.for_pattern());

// Unwrap the inner def element
write!(svg_defs, "{}", content.svg_defs).unwrap();

let pattern_transform = item_transform * DAffine2::from_translation(min);
let transform_str = format_transform_matrix(pattern_transform);
let transform_attr = if transform_str.is_empty() {
String::new()
} else {
format!(r#" patternTransform="{transform_str}""#)
};

let pattern_id = format!("pattern-{}", generate_uuid());
write!(
svg_defs,
r##"<pattern id="{pattern_id}" patternUnits="userSpaceOnUse" x="0" y="0" width="{}" height="{}"{transform_attr}>"##,
size.x, size.y,
)
.unwrap();

let content_shift = format_transform_matrix(DAffine2::from_translation(-min));
write!(svg_defs, r##"<g transform="{content_shift}">{}</g></pattern>"##, content.svg.to_svg_string()).unwrap();

Some(pattern_id)
}
Loading
Loading