Files
Graphite/node-graph/libraries/rendering/src/renderer.rs

3142 lines
126 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::render_ext::{PaintTarget, RenderExt};
use crate::to_peniko::{BlendModeExt, ToPenikoColor};
use core_types::CacheHash;
use core_types::attribute::{
Background as BackgroundAttr, BlendMode as BlendModeAttr, Clip, ClippingMask, Dimensions, EditorLayerPath, EditorTextFrame, FontSize, LetterSpacing, LetterTilt, LineHeight, Location, MaxHeight,
MaxWidth, Opacity, OpacityFill, Transform,
};
use core_types::blending::BlendMode;
use core_types::bounds::BoundingBox;
use core_types::bounds::RenderBoundingBox;
use core_types::color::Color;
use core_types::color::SRGBA8;
use core_types::lane::LaneSource;
use core_types::lane::{LeafLane, Single};
use core_types::list::{Item, List};
use core_types::math::quad::Quad;
use core_types::record::{Group, RunView};
use core_types::render_complexity::RenderComplexity;
use core_types::transform::Footprint;
use core_types::uuid::{NodeId, generate_uuid};
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM};
use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource;
use graphic_types::graphic::{PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path};
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{Gradient, GradientType};
use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod};
use graphic_types::vector_types::subpath::Subpath;
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
use graphic_types::{ATTR_FILL, Artboard, Graphic, Vector};
use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts};
use num_traits::Zero;
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::{DrawSettings, OutlinePen};
use skrifa::raw::FontRef as SkrifaFontRef;
use skrifa::{GlyphId, MetadataProvider};
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::hash::Hash;
use std::ops::Deref;
use std::sync::{Arc, LazyLock};
use text_nodes::markers::{Font, TextAlign};
use vector_types::gradient::GradientSpreadMethod;
use vector_types::markers::EditorClickTarget;
use vello::*;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum MaskType {
Clip,
Mask,
}
impl MaskType {
fn to_attribute(self) -> String {
match self {
Self::Mask => "mask".to_string(),
Self::Clip => "clip-path".to_string(),
}
}
fn write_to_defs(self, svg_defs: &mut String, uuid: u64, svg_string: String) {
let id = format!("mask-{uuid}");
match self {
Self::Clip => write!(svg_defs, r##"<clipPath id="{id}">{svg_string}</clipPath>"##).unwrap(),
Self::Mask => write!(svg_defs, r##"<mask id="{id}" mask-type="alpha">{svg_string}</mask>"##).unwrap(),
}
}
}
/// Mutable state used whilst rendering to an SVG
pub struct SvgRender {
pub svg: Vec<SvgSegment>,
pub svg_defs: String,
pub transform: DAffine2,
pub image_data: HashMap<CacheHashWrapper<Image<Color>>, u64>,
indent: usize,
}
impl SvgRender {
pub fn new() -> Self {
Self {
svg: Vec::default(),
svg_defs: String::new(),
transform: DAffine2::IDENTITY,
image_data: HashMap::new(),
indent: 0,
}
}
pub fn indent(&mut self) {
self.svg.push("\n".into());
self.svg.push("\t".repeat(self.indent).into());
}
/// Add an outer `<svg>...</svg>` tag with a `viewBox` and the `<defs />`
pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) {
let (x, y) = bounds_min.into();
let (size_x, size_y) = (bounds_max - bounds_min).into();
let svg_header = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,
defs = &self.svg_defs
);
self.svg_defs = String::new();
self.svg.insert(0, svg_header.into());
self.svg.push("</svg>".into());
}
/// Wraps the SVG with `<svg><g transform="...">...</g></svg>`, which allows for rotation
pub fn wrap_with_transform(&mut self, transform: DAffine2, size: Option<DVec2>) {
let view_box = size
.map(|size| format!("viewBox=\"0 0 {} {}\" width=\"{}\" height=\"{}\"", size.x, size.y, size.x, size.y))
.unwrap_or_default();
let matrix = format_transform_matrix(transform);
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
let svg_header = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" {view_box}><defs>{defs}</defs><g{transform}>"#,
defs = &self.svg_defs
);
self.svg_defs = String::new();
self.svg.insert(0, svg_header.into());
self.svg.push("</g></svg>".into());
}
pub fn leaf_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs)) {
self.indent();
self.svg.push("<".into());
self.svg.push(name.into());
attributes(&mut SvgRenderAttrs(self));
self.svg.push("/>".into());
}
pub fn leaf_node(&mut self, content: impl Into<SvgSegment>) {
self.indent();
self.svg.push(content.into());
}
pub fn parent_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs), inner: impl FnOnce(&mut Self)) {
let name = name.into();
self.indent();
self.svg.push("<".into());
self.svg.push(name.clone());
// Wraps `self` in a newtype (1-tuple) which is then mutated by the `attributes` closure
attributes(&mut SvgRenderAttrs(self));
self.svg.push(">".into());
let length = self.svg.len();
self.indent += 1;
inner(self);
self.indent -= 1;
if self.svg.len() != length {
self.indent();
self.svg.push("</".into());
self.svg.push(name);
self.svg.push(">".into());
} else {
self.svg.pop();
self.svg.push("/>".into());
}
}
}
pub struct SvgRenderOutput {
pub svg: String,
pub svg_defs: String,
pub image_data: HashMap<CacheHashWrapper<Image<Color>>, u64>,
}
impl From<&SvgRenderOutput> for SvgRender {
fn from(value: &SvgRenderOutput) -> Self {
Self {
svg: vec![value.svg.clone().into()],
svg_defs: value.svg_defs.clone(),
transform: DAffine2::IDENTITY,
image_data: value.image_data.clone(),
indent: 0,
}
}
}
impl From<SvgRender> for SvgRenderOutput {
fn from(val: SvgRender) -> Self {
Self {
svg: val.svg.to_svg_string(),
svg_defs: val.svg_defs,
image_data: val.image_data,
}
}
}
impl Default for SvgRender {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, Default)]
pub struct RenderContext {
pub resource_overrides: Vec<(peniko::ImageBrush, Texture)>,
}
#[derive(Default, Clone, Copy, Hash, graphene_hash::CacheHash)]
pub enum RenderOutputType {
#[default]
Svg,
Vello,
}
/// Static state used whilst rendering
#[derive(Default, Clone, CacheHash)]
pub struct RenderParams {
pub render_mode: RenderMode,
pub footprint: Footprint,
#[cache_hash(skip)]
pub scale: f64,
pub render_output_type: RenderOutputType,
pub thumbnail: bool,
/// Are we exporting
pub for_export: bool,
/// Are we generating a mask in this render pass? Used to see if fill should be multiplied with alpha.
pub for_mask: bool,
/// Are we generating a mask for alignment? Used to prevent unnecessary transforms in masks
pub alignment_parent_transform: Option<DAffine2>,
pub aligned_strokes: bool,
pub override_paint_order: bool,
/// Are we rendering for a pattern content
pub inside_pattern: bool,
pub artboard_background: Option<Color>,
/// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode.
pub viewport_zoom: f64,
}
impl RenderParams {
pub fn for_clipper(&self) -> Self {
Self { for_mask: true, ..*self }
}
pub fn for_alignment(&self, transform: DAffine2) -> Self {
Self {
alignment_parent_transform: Some(transform),
..*self
}
}
pub fn for_pattern(&self) -> Self {
Self { inside_pattern: true, ..*self }
}
pub fn to_canvas(&self) -> bool {
!self.for_export && !self.thumbnail && !self.for_mask && !self.inside_pattern
}
}
pub fn format_transform_matrix(transform: DAffine2) -> String {
if transform == DAffine2::IDENTITY {
return String::new();
}
transform.to_cols_array().iter().enumerate().fold("matrix(".to_string(), |val, (i, num)| {
let num = if num.abs() < 1_000_000_000. { (num * 1_000_000_000.).round() / 1_000_000_000. } else { *num };
let num = if num.is_zero() { "0".to_string() } else { num.to_string() };
let comma = if i == 5 { "" } else { "," };
val + &(num + comma)
}) + ")"
}
/// `(max, min)` factors by which a unit vector is stretched under `transform`'s linear part — the
/// principal and minor singular values, equal to the semi-axes of the ellipse a unit circle maps to.
/// Equivalent to `(max(sx, sy), min(sx, sy))` for axis-aligned scales, but accounts for shear.
fn singular_values(transform: DAffine2) -> (f64, f64) {
let m = transform.matrix2;
let a = m.x_axis.x;
let b = m.x_axis.y;
let c = m.y_axis.x;
let d = m.y_axis.y;
// Eigenvalues of MᵀM via the closed form for a 2×2, both are non-negative
let trace = a * a + b * b + c * c + d * d;
let det = a * d - b * c;
let discriminant = (trace * trace - 4. * det * det).max(0.).sqrt();
let largest_eigenvalue = (trace + discriminant) * 0.5;
let smallest_eigenvalue = ((trace - discriminant) * 0.5).max(0.);
(largest_eigenvalue.sqrt(), smallest_eigenvalue.sqrt())
}
pub fn black_or_white_for_best_contrast(background: Option<Color>) -> Color {
let Some(bg) = background else { return core_types::consts::LAYER_OUTLINE_STROKE_COLOR };
let alpha = bg.a();
// Un-premultiply, then encode to gamma sRGB to do the composite in display space.
let (gamma_r, gamma_g, gamma_b) = if alpha > f32::EPSILON {
let [r, g, b, _] = Color::from_rgbaf32_unchecked(bg.r() / alpha, bg.g() / alpha, bg.b() / alpha, alpha).to_gamma_srgb_channels();
(r, g, b)
} else {
(0., 0., 0.)
};
// Composite over black in sRGB space (premultiplied by alpha), then decode to linear for the luminance test.
let composited = Color::from_gamma_srgb_channels(gamma_r * alpha, gamma_g * alpha, gamma_b * alpha, 1.);
let threshold = (1.05 * 0.05f32).sqrt() - 0.05;
if composited.luminance_rec_709() > threshold { Color::BLACK } else { Color::WHITE }
}
pub fn to_transform(transform: DAffine2) -> usvg::Transform {
let cols = transform.to_cols_array();
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
}
fn to_point(p: DVec2) -> kurbo::Point {
kurbo::Point::new(p.x, p.y)
}
fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::Color) {
use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT;
let outline_stroke = kurbo::Stroke {
width: LAYER_OUTLINE_STROKE_WEIGHT / if render_params.viewport_zoom > 0. { render_params.viewport_zoom } else { 1. },
miter_limit: 4.,
join: Join::Miter,
start_cap: Cap::Butt,
end_cap: Cap::Butt,
dash_pattern: Default::default(),
dash_offset: 0.,
};
let outline_color = black_or_white_for_best_contrast(render_params.artboard_background);
let outline_color_peniko = SRGBA8::from(outline_color).to_peniko_color();
(outline_stroke, outline_color_peniko)
}
fn draw_raster_outline(scene: &mut Scene, outline_transform: &DAffine2, render_params: &RenderParams) {
use graphic_types::vector_types::vector::PointId;
let (outline_stroke, outline_color_peniko) = get_outline_styles(render_params);
let mut outline_path = Subpath::<PointId>::new_rectangle(DVec2::ZERO, DVec2::ONE).to_bezpath();
outline_path.apply_affine(Affine::new(outline_transform.to_cols_array()));
scene.stroke(&outline_stroke, Affine::IDENTITY, outline_color_peniko, None, &outline_path);
}
/// Emits an SVG `<path>` element with the resolved fill attribute corresponding to the given fill_graphic.
#[allow(clippy::too_many_arguments)]
fn emit_svg_fill_path(
render: &mut SvgRender,
d: String,
fill_graphic_list: Option<&List<Graphic>>,
item_transform: DAffine2,
element_transform: DAffine2,
applied_stroke_transform: DAffine2,
bounds_matrix: DAffine2,
render_params: &RenderParams,
) {
render.leaf_tag("path", |attributes| {
attributes.push("d", d);
let matrix = format_transform_matrix(element_transform);
if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix);
}
let defs = &mut attributes.0.svg_defs;
let fill_attribute = fill_graphic_list
.map(|list| list.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, render_params, PaintTarget::Fill))
.unwrap_or_else(|| r#" fill="none""#.to_string());
attributes.push_val(fill_attribute);
});
}
/// Whether the affine transform inverts to a finite matrix (a zero, subnormal, or NaN determinant does not).
pub(crate) fn transform_is_invertible(transform: DAffine2) -> bool {
transform.matrix2.determinant().recip().is_finite()
}
/// Maps a gradient's `transform` into the frame handed to the renderer: radial keeps the full matrix (so a
/// non-uniform transform makes an ellipse), while linear is reduced to the equivalent non-sheared gradient line (the
/// axis projected onto the band normal) so the iso-color bands keep following a sheared transform, which Vello can
/// represent since it stores only two endpoints.
pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientType) -> DAffine2 {
match gradient_type {
GradientType::Radial => transform,
GradientType::Linear => {
let axis = transform.matrix2.x_axis;
let band_normal = transform.matrix2.y_axis.perp();
let line = if band_normal.length_squared() > 0. { axis.project_onto(band_normal) } else { axis };
DAffine2 {
matrix2: DMat2::from_cols(line, line.perp()),
translation: transform.translation,
}
}
}
}
fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
let stops = gradient_list.element(0)?;
let gradient_type: GradientType = gradient_list.attr::<GradientTypeAttr>(0);
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
let spread_method: GradientSpreadMethod = gradient_list.attr::<SpreadMethod>(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(SRGBA8::from(color).to_peniko_color()),
});
}
// The unit gradient is placed by the desheared frame so a non-uniform transform produces the intended ellipse
let (start, end, gradient_to_device) = (DVec2::ZERO, DVec2::X, gradient_placement(multiplied_transform * gradient_transform, gradient_type));
let brush = peniko::Brush::Gradient(peniko::Gradient {
kind: match gradient_type {
GradientType::Linear => peniko::LinearGradientPosition {
start: to_point(start),
end: to_point(end),
}
.into(),
GradientType::Radial => peniko::RadialGradientPosition {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(start),
end_radius: start.distance(end) 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()
});
Some((brush, gradient_to_device))
}
// TODO: Click targets can be removed from the render output, since the vector data is available in the vector modify data from Monitor nodes.
// This will require that the transform for child layers into that layer space be calculated, or it could be returned from the RenderOutput instead of click targets.
#[derive(Debug, Default, Clone, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderMetadata {
pub upstream_footprints: HashMap<NodeId, Footprint>,
pub local_transforms: HashMap<NodeId, DAffine2>,
pub first_element_source_id: HashMap<NodeId, Option<NodeId>>,
pub click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
/// Source-geometry outlines for hover/selection overlays, separate from `click_targets` so
/// nodes with an `editor:click_target` override still outline the precise geometry.
pub outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
/// Per-layer text frame from row 0's `editor:text_frame` attribute.
/// The Text tool composes this with `transform_to_viewport(layer)` to position its drag cage.
pub text_frames: HashMap<NodeId, DAffine2>,
pub clip_targets: HashSet<NodeId>,
pub vector_data: HashMap<NodeId, Arc<Vector>>,
/// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read it.
#[cfg_attr(feature = "serde", serde(skip))]
pub fill_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
/// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read it.
#[cfg_attr(feature = "serde", serde(skip))]
pub stroke_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
pub backgrounds: Vec<Background>,
}
impl RenderMetadata {
pub fn apply_transform(&mut self, transform: DAffine2) {
for value in self.upstream_footprints.values_mut() {
value.transform = transform * value.transform;
}
}
/// Merge another RenderMetadata into this one.
/// Values from `other` take precedence for duplicate keys.
pub fn merge(&mut self, other: &RenderMetadata) {
// Destructure Self to get errors when new fields are added to the struct
let RenderMetadata {
upstream_footprints,
local_transforms,
first_element_source_id,
click_targets,
outlines,
text_frames,
clip_targets,
vector_data,
fill_attributes,
stroke_attributes,
backgrounds,
} = self;
upstream_footprints.extend(other.upstream_footprints.iter());
local_transforms.extend(other.local_transforms.iter());
first_element_source_id.extend(other.first_element_source_id.iter());
click_targets.extend(other.click_targets.iter().map(|(k, v)| (*k, v.clone())));
outlines.extend(other.outlines.iter().map(|(k, v)| (*k, v.clone())));
text_frames.extend(other.text_frames.iter());
clip_targets.extend(other.clip_targets.iter());
vector_data.extend(other.vector_data.iter().map(|(id, data)| (*id, data.clone())));
fill_attributes.extend(other.fill_attributes.iter().map(|(id, data)| (*id, data.clone())));
stroke_attributes.extend(other.stroke_attributes.iter().map(|(id, data)| (*id, data.clone())));
// TODO: Find a better non O(n^2) way to merge backgrounds
for background in &other.backgrounds {
if !backgrounds.contains(background) {
backgrounds.push(background.clone());
}
}
}
}
#[derive(Debug, Default, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct Background {
pub location: DVec2,
pub dimensions: DVec2,
}
// TODO: Rename to "Graphical"
pub trait Render: BoundingBox + RenderComplexity {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams);
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams);
/// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection.
fn add_upstream_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
/// Like `add_upstream_click_targets` but for visual outlines. `List<Vector>` overrides this to ignore `editor:click_target` so outlines reflect the actual geometry.
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
self.add_upstream_click_targets(outlines);
}
// TODO: Store all click targets in a vec which contains the AABB, click target, and path
// fn add_click_targets(&self, click_targets: &mut Vec<([DVec2; 2], ClickTarget, Vec<NodeId>)>, current_path: Option<NodeId>) {}
/// Recursively iterate over data in the render (including nested layer stacks upstream of a vector node, in the case of a boolean operation) to collect the footprints, click targets, and vector modify.
fn collect_metadata(&self, _metadata: &mut RenderMetadata, _footprint: Footprint, _element_id: Option<NodeId>) {}
fn contains_artboard(&self) -> bool {
false
}
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {}
}
impl Render for Graphic<'_> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
match self {
Graphic::Graphic(list) => list.render_svg(render, render_params),
Graphic::Vector(vector) => render_vector_svg(&Single(vector), render, render_params),
Graphic::RasterCPU(raster) => render_raster_cpu_svg(&Single(raster), render, render_params),
Graphic::RasterGPU(_) => (),
Graphic::Color(color) => render_color_svg(&Single(color), render, render_params),
Graphic::Gradient(gradient) => render_gradient_svg(&Single(gradient), render, render_params),
Graphic::Text(text) => render_text_svg(&Single(text), render, render_params),
Graphic::Group(group) => render_group_svg(group, PaintReach::NONE, render, render_params),
}
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
match self {
Graphic::Graphic(list) => list.render_to_vello(scene, transform, context, render_params),
Graphic::Vector(vector) => render_vector_vello(&Single(vector), scene, transform, context, render_params),
Graphic::RasterCPU(raster) => render_raster_cpu_vello(&Single(raster), scene, transform, render_params),
Graphic::RasterGPU(raster) => render_raster_gpu_vello(&Single(raster), scene, transform, context, render_params),
Graphic::Color(color) => render_color_vello(&Single(color), scene, render_params),
Graphic::Gradient(gradient) => render_gradient_vello(&Single(gradient), scene, transform, render_params),
Graphic::Text(text) => render_text_vello(&Single(text), scene, transform, render_params),
Graphic::Group(group) => render_group_vello(group, PaintReach::NONE, scene, transform, context, render_params),
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_element_metadata(self, PaintReach::NONE, DAffine2::IDENTITY, None, metadata, footprint, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_element_upstream_click_targets(self, PaintReach::NONE, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_element_upstream_outline_targets(self, PaintReach::NONE, outlines)
}
fn contains_artboard(&self) -> bool {
match self {
Graphic::Graphic(list) => list.contains_artboard(),
_ => false,
}
}
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
match self {
Graphic::Graphic(list) => list.new_ids_from_hash(reference),
Graphic::Vector(vector) => vector.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()),
_ => (),
}
}
}
fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) {
match element {
Graphic::Vector(vector) if reach.applies() => render_vector_svg(&PaintOverlay::new(&Single(vector), reach.paint), render, render_params),
Graphic::Graphic(inner) => render_graphic_svg_with(inner, reach.nested(), render, render_params),
Graphic::Group(group) => render_group_svg(group, reach, render, render_params),
_ => element.render_svg(render, render_params),
}
}
fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
match element {
Graphic::Vector(vector) if reach.applies() => render_vector_vello(&PaintOverlay::new(&Single(vector), reach.paint), scene, transform, context, render_params),
Graphic::Graphic(inner) => render_graphic_vello_with(inner, reach.nested(), scene, transform, context, render_params),
Graphic::Group(group) => render_group_vello(group, reach, scene, transform, context, render_params),
_ => element.render_to_vello(scene, transform, context, render_params),
}
}
fn element_can_reduce_to_clip_path<'a>(element: &'a Graphic, reach: PaintReach<'a>) -> bool {
match element {
Graphic::Vector(vector) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&Single(vector), reach.paint)),
Graphic::Group(group) => match RunView::<Vector>::new(&group.content) {
Some(run) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&run, reach.paint)),
Some(run) => vector_can_reduce_to_clip_path(&run),
None => false,
},
_ => element.can_reduce_to_clip_path(),
}
}
fn collect_element_metadata<'a>(
element: &'a Graphic,
reach: PaintReach<'a>,
lane_transform: DAffine2,
lane_source: Option<NodeId>,
metadata: &mut RenderMetadata,
footprint: Footprint,
element_id: Option<NodeId>,
) {
if let Some(element_id) = element_id {
metadata.upstream_footprints.insert(element_id, footprint);
match element {
Graphic::Group(group) => collect_group_row_metadata(group, metadata, element_id),
Graphic::Graphic(_) => {}
// A leaf's layer identity and transform ride its containing lane.
Graphic::Vector(_) => {
metadata.first_element_source_id.insert(element_id, lane_source);
metadata.local_transforms.insert(element_id, lane_transform);
}
_ => {
metadata.local_transforms.insert(element_id, lane_transform);
}
}
}
match element {
Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id),
Graphic::Vector(vector) if reach.applies() => collect_vector_metadata(&PaintOverlay::new(&Single(vector), reach.paint), metadata, footprint, element_id),
Graphic::Vector(vector) => collect_vector_metadata(&Single(vector), metadata, footprint, element_id),
Graphic::RasterCPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id),
Graphic::RasterGPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id),
Graphic::Color(_) => {}
Graphic::Gradient(_) => {}
Graphic::Text(text) => collect_text_metadata(&Single(text), metadata, footprint, element_id),
Graphic::Group(group) => collect_group_metadata(group, reach, metadata, footprint, element_id),
}
}
/// The id-level metadata the legacy lowering exposed for a group element: a
/// bare typed run serves its lane-0 transform (and source id for vectors) as
/// the layer's local transform, matching the typed list the conversion made.
fn collect_group_row_metadata(group: &Group, metadata: &mut RenderMetadata, element_id: NodeId) {
fn lane_zero_transform<T: dyn_any::StaticTypeSized>(item: &core_types::record::GroupItem) -> Option<DAffine2> {
RunView::<T>::new(item).map(|run| run.attr::<Transform>(0))
}
let item = &group.content;
if group.row.is_some() || item.is_empty() || item.typed_lanes::<Graphic>().is_some() {
return;
}
if let Some(run) = RunView::<Vector>::new(item) {
let layer_path: &[NodeId] = run.attr::<EditorLayerPath>(0);
metadata.first_element_source_id.insert(element_id, layer_path.last().copied());
metadata.local_transforms.insert(element_id, run.attr::<Transform>(0));
return;
}
let transform = None
.or_else(|| lane_zero_transform::<Raster<CPU>>(item))
.or_else(|| lane_zero_transform::<Raster<GPU>>(item))
.or_else(|| lane_zero_transform::<Color>(item))
.or_else(|| lane_zero_transform::<Gradient>(item))
.or_else(|| lane_zero_transform::<String>(item));
if let Some(transform) = transform {
metadata.local_transforms.insert(element_id, transform);
}
}
fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) {
match element {
Graphic::Graphic(list) => add_graphic_upstream_click_targets_with(list, reach.nested(), click_targets),
Graphic::Vector(vector) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(&Single(vector), reach.paint), click_targets),
Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), click_targets),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets),
Graphic::Color(_) | Graphic::Gradient(_) => {}
Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), click_targets),
Graphic::Group(group) => add_group_upstream_click_targets(group, reach, click_targets),
}
}
fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) {
match element {
Graphic::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach.nested(), outlines),
Graphic::Vector(vector) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(&Single(vector), reach.paint), outlines),
Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), outlines),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines),
Graphic::Color(_) | Graphic::Gradient(_) => {}
Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), outlines),
Graphic::Group(group) => add_group_upstream_outline_targets(group, reach, outlines),
}
}
/// The native group render: the run dispatches on its element type into the
/// generic bodies; an unknown element type renders as nothing.
fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) {
let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) {
render_graphic_svg_with(&run, reach.into_group_graphics(), render, render_params)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => render_vector_svg(&PaintOverlay::new(&run, reach.paint), render, render_params),
false => render_vector_svg(&run, render, render_params),
}
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
render_raster_cpu_svg(&run, render, render_params)
} else if item.typed_lanes::<Raster<GPU>>().is_some() {
} else if let Some(run) = RunView::<Color>::new(item) {
render_color_svg(&run, render, render_params)
} else if let Some(run) = RunView::<Gradient>::new(item) {
render_gradient_svg(&run, render, render_params)
} else if let Some(run) = RunView::<String>::new(item) {
render_text_svg(&run, render, render_params)
}
}
fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) {
render_graphic_vello_with(&run, reach.into_group_graphics(), scene, transform, context, render_params)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => render_vector_vello(&PaintOverlay::new(&run, reach.paint), scene, transform, context, render_params),
false => render_vector_vello(&run, scene, transform, context, render_params),
}
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
render_raster_cpu_vello(&run, scene, transform, render_params)
} else if let Some(run) = RunView::<Raster<GPU>>::new(item) {
render_raster_gpu_vello(&run, scene, transform, context, render_params)
} else if let Some(run) = RunView::<Color>::new(item) {
render_color_vello(&run, scene, render_params)
} else if let Some(run) = RunView::<Gradient>::new(item) {
render_gradient_vello(&run, scene, transform, render_params)
} else if let Some(run) = RunView::<String>::new(item) {
render_text_vello(&run, scene, transform, render_params)
}
}
/// Collects a group as its legacy lowering did: a typed run behaves as the
/// typed variant the conversion produced, so a caller's element id passes
/// through to the typed body unchanged.
fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) {
collect_graphic_metadata_with(&run, reach.into_group_graphics(), metadata, footprint, element_id)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => collect_vector_metadata(&PaintOverlay::new(&run, reach.paint), metadata, footprint, element_id),
false => collect_vector_metadata(&run, metadata, footprint, element_id),
}
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
collect_raster_metadata(&run, metadata, footprint, element_id)
} else if let Some(run) = RunView::<Raster<GPU>>::new(item) {
collect_raster_metadata(&run, metadata, footprint, element_id)
} else if item.typed_lanes::<Color>().is_some() || item.typed_lanes::<Gradient>().is_some() {
} else if let Some(run) = RunView::<String>::new(item) {
collect_text_metadata(&run, metadata, footprint, element_id)
}
}
fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) {
let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) {
add_graphic_upstream_click_targets_with(&run, reach.into_group_graphics(), click_targets)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => add_vector_upstream_click_targets(&PaintOverlay::new(&run, reach.paint), click_targets),
false => add_vector_upstream_click_targets(&run, click_targets),
}
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
add_raster_upstream_click_targets(click_targets)
} else if let Some(run) = RunView::<String>::new(item) {
add_text_upstream_click_targets(&run, click_targets)
}
}
fn add_group_upstream_outline_targets<'a>(group: &'a Group, reach: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) {
let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) {
add_graphic_upstream_outline_targets_with(&run, reach.into_group_graphics(), outlines)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => add_vector_upstream_outline_targets(&PaintOverlay::new(&run, reach.paint), outlines),
false => add_vector_upstream_outline_targets(&run, outlines),
}
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
add_raster_upstream_click_targets(outlines)
} else if let Some(run) = RunView::<String>::new(item) {
add_text_upstream_click_targets(&run, outlines)
}
}
/// Reads the artboard metadata for the item at `index`.
fn read_artboard_attributes<S: LaneSource>(source: &S, index: usize) -> (DVec2, DVec2, Color, bool) {
let location: DVec2 = source.attr::<Location>(index);
let dimensions: DVec2 = source.attr::<Dimensions>(index);
let background: Color = source.attr::<BackgroundAttr>(index);
let clip: bool = source.attr::<Clip>(index);
(location, dimensions, background, clip)
}
fn render_artboard_svg<'a, S: LaneSource<Element = Artboard<'a>>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
for index in 0..source.lane_count() {
let Some(content) = source.element(index).map(Artboard::as_graphic_list) else { continue };
let (location, dimensions, background, clip) = read_artboard_attributes(source, index);
let x = location.x.min(location.x + dimensions.x);
let y = location.y.min(location.y + dimensions.y);
let width = dimensions.x.abs();
let height = dimensions.y.abs();
// Background
render.leaf_tag("rect", |attributes| {
attributes.push("fill", format!("#{}", SRGBA8::from(background).to_rgb_hex()));
if background.a() < 1. {
attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string());
}
attributes.push("x", x.to_string());
attributes.push("y", y.to_string());
attributes.push("width", width.to_string());
attributes.push("height", height.to_string());
});
// Artwork
render.parent_tag(
// SVG group tag
"g",
// Group tag attributes
|attributes| {
let matrix = format_transform_matrix(DAffine2::from_translation(location));
if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix);
}
if clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" /></clipPath>"##,
dimensions.x, dimensions.y,
)
.unwrap();
attributes.push("clip-path", selector);
}
},
// Artwork content
|render| {
let mut render_params = render_params.clone();
render_params.artboard_background = Some(background);
content.render_svg(render, &render_params);
},
);
}
}
fn render_artboard_vello<'a, S: LaneSource<Element = Artboard<'a>>>(source: &S, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
use vello::peniko;
for index in 0..source.lane_count() {
let Some(content) = source.element(index).map(Artboard::as_graphic_list) else { continue };
let (location, dimensions, background, clip) = read_artboard_attributes(source, index);
let [a, b] = [location, location + dimensions];
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
let artboard_transform = kurbo::Affine::new(transform.to_cols_array());
let color = SRGBA8::from(background).to_peniko_color();
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect);
scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect);
scene.pop_layer();
if clip {
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect);
}
// Since the content's transform is right multiplied in when rendering the content, we just need to right multiply by the artboard offset here.
let child_transform = transform * DAffine2::from_translation(location);
let mut render_params = render_params.clone();
render_params.artboard_background = Some(background);
content.render_to_vello(scene, child_transform, context, &render_params);
if clip {
scene.pop_layer();
}
}
}
fn collect_artboard_metadata<'a, S: LaneSource<Element = Artboard<'a>>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint) {
for index in 0..source.lane_count() {
let Some(content) = source.element(index).map(Artboard::as_graphic_list) else { continue };
let (location, dimensions, _background, clip) = read_artboard_attributes(source, index);
let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let element_id = layer_path.last().copied();
if let Some(element_id) = element_id {
let subpath = Subpath::new_rectangle(DVec2::ZERO, dimensions);
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, DAffine2::from_translation(location));
if clip {
metadata.clip_targets.insert(element_id);
}
}
metadata.backgrounds.push(Background { location, dimensions });
let mut child_footprint = footprint;
child_footprint.transform *= DAffine2::from_translation(location);
content.collect_metadata(metadata, child_footprint, None);
}
}
fn add_artboard_upstream_click_targets<'a, S: LaneSource<Element = Artboard<'a>>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
for index in 0..source.lane_count() {
let dimensions: DVec2 = source.attr::<Dimensions>(index);
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions);
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
}
}
impl Render for List<Artboard<'_>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_artboard_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_artboard_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
collect_artboard_metadata(self, metadata, footprint)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_artboard_upstream_click_targets(self, click_targets)
}
fn contains_artboard(&self) -> bool {
!self.is_empty()
}
}
fn render_graphic_svg<'e, S: LaneSource<Element = Graphic<'e>>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
render_graphic_svg_with(source, PaintReach::NONE, render, render_params)
}
fn render_graphic_svg_with<'a, 'e, S: LaneSource<Element = Graphic<'e>>>(source: &'a S, inherited: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) {
let paint_columns = PaintColumns::new(source);
let mut mask_state = None;
for index in 0..source.lane_count() {
let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let element = source.element(index).unwrap();
let reach = inherited.for_lane(&paint_columns, index);
render.parent_tag(
"g",
|attributes| {
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix);
}
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if blend_mode != BlendMode::default() {
attributes.push("style", blend_mode.render());
}
let next_clips = index + 1 < source.lane_count() && source.element(index + 1).unwrap().had_clip_enabled();
if next_clips && mask_state.is_none() {
let uuid = generate_uuid();
let mask_type = if element_can_reduce_to_clip_path(element, reach) { MaskType::Clip } else { MaskType::Mask };
mask_state = Some((uuid, mask_type));
let mut svg = SvgRender::new();
render_element_svg(element, reach, &mut svg, &render_params.for_clipper());
write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap();
mask_type.write_to_defs(&mut attributes.0.svg_defs, uuid, svg.svg.to_svg_string());
} else if let Some((uuid, mask_type)) = mask_state {
if !next_clips {
mask_state = None;
}
let id = format!("mask-{uuid}");
let selector = format!("url(#{id})");
attributes.push(mask_type.to_attribute(), selector);
}
},
|render| {
render_element_svg(element, reach, render, render_params);
},
);
}
}
fn render_graphic_vello<'e, S: LaneSource<Element = Graphic<'e>>>(source: &S, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_graphic_vello_with(source, PaintReach::NONE, scene, transform, context, render_params)
}
fn render_graphic_vello_with<'a, 'e, S: LaneSource<Element = Graphic<'e>>>(
source: &'a S,
inherited: PaintReach<'a>,
scene: &mut Scene,
transform: DAffine2,
context: &mut RenderContext,
render_params: &RenderParams,
) {
let paint_columns = PaintColumns::new(source);
let mut mask_element_and_transform = None;
for index in 0..source.lane_count() {
let item_transform: DAffine2 = source.attr::<Transform>(index);
let transform = transform * item_transform;
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let element = source.element(index).unwrap();
let reach = inherited.for_lane(&paint_columns, index);
let mut layer = false;
let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal,
_ => blend_mode_attr.to_peniko(),
};
let mut bounds = RenderBoundingBox::None;
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. || (render_params.render_mode != RenderMode::Outline && blend_mode_attr != BlendMode::default()) {
bounds = element.bounding_box(transform, true);
if let RenderBoundingBox::Rectangle(bounds) = bounds {
scene.push_layer(
peniko::Fill::NonZero,
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
opacity,
kurbo::Affine::IDENTITY,
&kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
);
layer = true;
}
}
let next_clips = index + 1 < source.lane_count() && source.element(index + 1).unwrap().had_clip_enabled();
if next_clips && mask_element_and_transform.is_none() {
mask_element_and_transform = Some((element, transform, reach));
render_element_vello(element, reach, scene, transform, context, render_params);
} else if let Some((mask_element, transform_mask, mask_reach)) = mask_element_and_transform {
if !next_clips {
mask_element_and_transform = None;
}
if !layer {
bounds = element.bounding_box(transform, true);
}
if let RenderBoundingBox::Rectangle(bounds) = bounds {
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
render_element_vello(mask_element, mask_reach, scene, transform_mask, context, &render_params.for_clipper());
scene.push_layer(
peniko::Fill::NonZero,
peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcIn),
1.,
kurbo::Affine::IDENTITY,
&rect,
);
}
render_element_vello(element, reach, scene, transform, context, render_params);
if matches!(bounds, RenderBoundingBox::Rectangle(_)) {
scene.pop_layer();
scene.pop_layer();
}
} else {
render_element_vello(element, reach, scene, transform, context, render_params);
}
if layer {
scene.pop_layer();
}
}
}
fn collect_graphic_metadata<'e, S: LaneSource<Element = Graphic<'e>>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_graphic_metadata_with(source, PaintReach::NONE, metadata, footprint, element_id)
}
fn collect_graphic_metadata_with<'a, 'e, S: LaneSource<Element = Graphic<'e>>>(
source: &'a S,
inherited: PaintReach<'a>,
metadata: &mut RenderMetadata,
footprint: Footprint,
element_id: Option<NodeId>,
) {
let paint_columns = PaintColumns::new(source);
for index in 0..source.lane_count() {
let item_transform: DAffine2 = source.attr::<Transform>(index);
let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied();
let element = source.element(index).unwrap();
let reach = inherited.for_lane(&paint_columns, index);
let mut footprint = footprint;
footprint.transform *= item_transform;
if let Some(element_id) = layer {
collect_element_metadata(element, reach, item_transform, layer, metadata, footprint, Some(element_id));
} else {
// Recurse through anonymous wrapper items to reach nested content with editor:layer_path tags
collect_element_metadata(element, reach, item_transform, layer, metadata, footprint, None);
}
}
if let Some(element_id) = element_id {
let mut all_upstream_click_targets = Vec::new();
let mut all_upstream_outlines = Vec::new();
for index in 0..source.lane_count() {
let item_transform: DAffine2 = source.attr::<Transform>(index);
let element = source.element(index).unwrap();
let reach = inherited.for_lane(&paint_columns, index);
let mut new_click_targets = Vec::new();
add_element_upstream_click_targets(element, reach, &mut new_click_targets);
for click_target in new_click_targets.iter_mut() {
click_target.apply_transform(item_transform)
}
all_upstream_click_targets.extend(new_click_targets);
let mut new_outlines = Vec::new();
add_element_upstream_outline_targets(element, reach, &mut new_outlines);
for outline in new_outlines.iter_mut() {
outline.apply_transform(item_transform)
}
all_upstream_outlines.extend(new_outlines);
}
metadata.click_targets.insert(element_id, all_upstream_click_targets.into_iter().map(|x| x.into()).collect());
metadata.outlines.insert(element_id, all_upstream_outlines.into_iter().map(|x| x.into()).collect());
}
}
fn add_graphic_upstream_click_targets<'e, S: LaneSource<Element = Graphic<'e>>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
add_graphic_upstream_click_targets_with(source, PaintReach::NONE, click_targets)
}
fn add_graphic_upstream_click_targets_with<'a, 'e, S: LaneSource<Element = Graphic<'e>>>(source: &'a S, inherited: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) {
let paint_columns = PaintColumns::new(source);
for index in 0..source.lane_count() {
let item_transform: DAffine2 = source.attr::<Transform>(index);
let element = source.element(index).unwrap();
let reach = inherited.for_lane(&paint_columns, index);
let mut new_click_targets = Vec::new();
add_element_upstream_click_targets(element, reach, &mut new_click_targets);
for click_target in new_click_targets.iter_mut() {
click_target.apply_transform(item_transform)
}
click_targets.extend(new_click_targets);
}
}
fn add_graphic_upstream_outline_targets<'e, S: LaneSource<Element = Graphic<'e>>>(source: &S, outlines: &mut Vec<ClickTarget>) {
add_graphic_upstream_outline_targets_with(source, PaintReach::NONE, outlines)
}
fn add_graphic_upstream_outline_targets_with<'a, 'e, S: LaneSource<Element = Graphic<'e>>>(source: &'a S, inherited: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) {
let paint_columns = PaintColumns::new(source);
for index in 0..source.lane_count() {
let item_transform: DAffine2 = source.attr::<Transform>(index);
let element = source.element(index).unwrap();
let reach = inherited.for_lane(&paint_columns, index);
let mut new_outlines = Vec::new();
add_element_upstream_outline_targets(element, reach, &mut new_outlines);
for outline in new_outlines.iter_mut() {
outline.apply_transform(item_transform)
}
outlines.extend(new_outlines);
}
}
fn graphic_contains_artboard<'e, S: LaneSource<Element = Graphic<'e>>>(source: &S) -> bool {
(0..source.lane_count()).any(|index| source.element(index).is_some_and(|element| element.contains_artboard()))
}
impl Render for List<Graphic<'_>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_graphic_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_graphic_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_graphic_metadata(self, metadata, footprint, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_graphic_upstream_click_targets(self, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_graphic_upstream_outline_targets(self, outlines)
}
fn contains_artboard(&self) -> bool {
graphic_contains_artboard(self)
}
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
let (elements, layers) = self.element_and_attribute_slices_mut::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH);
for (element, layer) in elements.iter_mut().zip(layers.iter()) {
element.new_ids_from_hash(layer.last().copied());
}
}
}
fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
for index in 0..source.lane_count() {
let Some(vector) = source.element(index) else { continue };
let item_transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
let applied_stroke_transform = set_stroke_transform.unwrap_or(item_transform);
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
let element_transform = set_stroke_transform.map(|stroke_transform| item_transform * stroke_transform.inverse());
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
let layer_bounds = vector.bounding_box().unwrap_or_default();
let transformed_bounds = vector.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
let stroke_layer_bounds = vector.stroke_inclusive_bounding_box_with_transform(DAffine2::IDENTITY).unwrap_or(layer_bounds);
let bounds_matrix = DAffine2::from_scale_angle_translation(layer_bounds[1] - layer_bounds[0], 0., layer_bounds[0]);
let stroke_bounds_matrix = DAffine2::from_scale_angle_translation(stroke_layer_bounds[1] - stroke_layer_bounds[0], 0., stroke_layer_bounds[0]);
let mut path = String::new();
for mut bezpath in vector.stroke_bezpath_iter() {
bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
path.push_str(bezpath.to_svg().as_str());
}
let mask_type = if vector.stroke.as_ref().map(|x| x.align) == Some(StrokeAlign::Inside) {
MaskType::Clip
} else {
MaskType::Mask
};
let fill_graphic_list = paint_graphics::<Fill, _>(source, index);
let fill_graphic = fill_graphic_list.and_then(|l| l.element(0));
let stroke_graphic_list = paint_graphics::<Stroke, _>(source, index);
let stroke_graphic = stroke_graphic_list.and_then(|l| l.element(0));
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
let can_draw_aligned_stroke = path_is_closed
&& vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
&& stroke_graphic.is_some_and(|graphic| !graphic.is_fully_transparent());
let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip);
let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order;
let wants_stroke_below = vector.stroke.as_ref().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
let override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
let use_face_fill = vector.use_face_fill();
if needs_separate_alignment_fill && !wants_stroke_below {
emit_svg_fill_path(
render,
path.clone(),
fill_graphic_list,
item_transform,
element_transform,
applied_stroke_transform,
bounds_matrix,
render_params,
);
}
let push_id = needs_separate_alignment_fill.then_some({
let id = format!("alignment-{}", generate_uuid());
let mut cloned_vector = vector.clone();
cloned_vector.stroke = None;
// The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior.
// The wrapping SVG group (above) handles the user-set opacity.
let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
let vector_item = List::new_from_item(mask_item);
(id, mask_type, vector_item)
});
if use_face_fill {
for mut face_path in vector.construct_faces().filter(|face| face.area() >= 0.) {
face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
let face_d = face_path.to_svg();
emit_svg_fill_path(
render,
face_d,
fill_graphic_list,
item_transform,
element_transform,
applied_stroke_transform,
bounds_matrix,
render_params,
);
}
}
render.leaf_tag("path", |attributes| {
attributes.push("d", path.clone());
let matrix = format_transform_matrix(element_transform);
if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix);
}
let defs = &mut attributes.0.svg_defs;
if let Some((ref id, mask_type, ref vector_item)) = push_id {
let mut svg = SvgRender::new();
vector_item.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
let stroke = vector.stroke.as_ref().unwrap();
// `push_id` is only `Some` when `can_draw_aligned_stroke`, which is gated on `path_is_closed`
let (largest_scale, _) = singular_values(applied_stroke_transform);
let inflation = stroke.max_aabb_inflation(true) * largest_scale;
let quad = Quad::from_box(transformed_bounds).inflate(inflation);
let (x, y) = quad.top_left().into();
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
write!(defs, r##"{}"##, svg.svg_defs).unwrap();
let rect = format!(r##"<rect x="{x}" y="{y}" width="{width}" height="{height}" fill="white" />"##);
match mask_type {
MaskType::Clip => write!(defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg.svg.to_svg_string()).unwrap(),
MaskType::Mask => write!(
defs,
r##"<mask id="{id}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" x="{x}" y="{y}" width="{width}" height="{height}">{}{}</mask>"##,
rect,
svg.svg.to_svg_string()
)
.unwrap(),
}
}
let mut render_params = render_params.clone();
render_params.aligned_strokes = can_draw_aligned_stroke;
render_params.override_paint_order = override_paint_order;
let stroke_shape_attribute = vector
.stroke
.as_ref()
.map(|stroke| {
if stroke_graphic_list.is_some_and(is_paint_present) {
stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke)
} else {
String::new()
}
})
.unwrap_or_default();
// Need to avoid generating only paint attribute, otherwise SVG uses 1px width stroke as a fallback
let stroke_visible = vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent());
let stroke_attribute = if stroke_visible {
stroke_graphic_list
.map(|list| {
// Gradient should align with the fill path bbox so that a shared gradient lines up across fill and stroke.
// Only clipping-based paints need the stroke-inclusive bbox.
let paint_bounds = match list.element(0) {
Some(Graphic::Color(_)) | Some(Graphic::Gradient(_)) => bounds_matrix,
_ => stroke_bounds_matrix,
};
list.render(defs, item_transform, element_transform, applied_stroke_transform, paint_bounds, &render_params, PaintTarget::Stroke)
})
.unwrap_or_else(|| r#" stroke="none""#.to_string())
} else {
String::new()
};
let fill_attribute = if needs_separate_alignment_fill || use_face_fill {
r#" fill="none""#.to_string()
} else {
fill_graphic_list
.map(|list| list.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Fill))
.unwrap_or_else(|| r#" fill="none""#.to_string())
};
if let Some((id, mask_type, _)) = push_id {
let selector = format!("url(#{id})");
attributes.push(mask_type.to_attribute(), selector);
}
attributes.push_val(fill_attribute);
attributes.push_val(stroke_shape_attribute);
attributes.push_val(stroke_attribute);
if vector.is_branching() && !use_face_fill {
attributes.push("fill-rule", "evenodd");
}
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if blend_mode_attr != BlendMode::default() {
attributes.push("style", blend_mode_attr.render());
}
});
// When splitting passes and stroke is below, draw the fill after the stroke.
if needs_separate_alignment_fill && wants_stroke_below {
emit_svg_fill_path(
render,
path.clone(),
fill_graphic_list,
item_transform,
element_transform,
applied_stroke_transform,
bounds_matrix,
render_params,
);
}
}
}
fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for index in 0..source.lane_count() {
use graphic_types::vector_types::vector;
let Some(element) = source.element(index) else { continue };
let item_transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let multiplied_transform = parent_transform * item_transform;
let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
let mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
let mut element_transform = set_stroke_transform
.map(|stroke_transform| multiplied_transform * stroke_transform.inverse())
.unwrap_or(DAffine2::IDENTITY);
if let Some(alignment_transform) = render_params.alignment_parent_transform {
applied_stroke_transform = alignment_transform;
element_transform = if transform_is_invertible(alignment_transform) {
multiplied_transform * alignment_transform.inverse()
} else {
multiplied_transform
};
}
let layer_bounds = element.bounding_box().unwrap_or_default();
let mut path = kurbo::BezPath::new();
for mut bezpath in element.stroke_bezpath_iter() {
bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
for element in bezpath {
path.push(element);
}
}
let fill_graphic_list = paint_graphics::<Fill, _>(source, index);
let stroke_graphic_list = paint_graphics::<Stroke, _>(source, index);
// If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal,
_ => blend_mode_attr.to_peniko(),
};
let mut layer = false;
// Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path).
// Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
let stroke = element.stroke.as_ref();
let stroke_fully_transparent = stroke_graphic_list.is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent()));
let can_draw_aligned_stroke = !stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. || blend_mode_attr != BlendMode::default() {
layer = true;
// `max_aabb_inflation` is in `applied_stroke_transform`-space; `layer_bounds` is path-local and `push_layer` re-applies `multiplied_transform`.
// Divide by the smaller axial scale to cover the stroke in both axes after Vello's transform. Skip on a degenerate transform.
let (_, smallest_scale) = singular_values(applied_stroke_transform);
let stroke_inflation = stroke.map_or(0., |s| s.max_aabb_inflation(can_draw_aligned_stroke));
let inflate_amount = if smallest_scale > 0. { stroke_inflation / smallest_scale } else { 0. };
let quad = Quad::from_box(layer_bounds).inflate(inflate_amount);
let layer_bounds = quad.bounding_box();
scene.push_layer(
peniko::Fill::NonZero,
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
opacity,
kurbo::Affine::new(multiplied_transform.to_cols_array()),
&kurbo::Rect::new(layer_bounds[0].x, layer_bounds[0].y, layer_bounds[1].x, layer_bounds[1].y),
);
}
let use_layer = can_draw_aligned_stroke;
let wants_stroke_below = stroke.is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow);
let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| {
let Some(fill_graphic) = fill_graphic_list else { return };
for paint_index in 0..fill_graphic.len() {
let Some(paint) = fill_graphic.element(paint_index) else { continue };
match paint {
Graphic::Color(color) => {
let fill = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color());
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
}
Graphic::Gradient(gradient) => {
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(&LeafLane::new(fill_graphic, paint_index, gradient), &multiplied_transform) else {
continue;
};
let inverse_element_transform = if transform_is_invertible(element_transform) {
element_transform.inverse()
} else {
Default::default()
};
let brush_transform = kurbo::Affine::new((inverse_element_transform * gradient_to_device).to_cols_array());
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), path);
}
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) | Graphic::Group(_) => {
scene.push_clip_layer(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), path);
paint.render_to_vello(scene, multiplied_transform, context, render_params);
scene.pop_layer();
}
};
}
};
// Branching vectors without regions (e.g. mesh grids) need face-by-face fill rendering.
let use_face_fill = element.use_face_fill();
let do_fill = |scene: &mut Scene, context: &mut RenderContext| {
if use_face_fill {
for mut face_path in element.construct_faces().filter(|face| face.area() >= 0.) {
face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
let mut kurbo_path = kurbo::BezPath::new();
for element in face_path {
kurbo_path.push(element);
}
do_fill_path(scene, context, &kurbo_path, peniko::Fill::NonZero);
}
} else if element.is_branching() {
do_fill_path(scene, context, &path, peniko::Fill::EvenOdd);
} else {
do_fill_path(scene, context, &path, peniko::Fill::NonZero);
}
};
let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| {
let Some(stroke_graphic_list) = stroke_graphic_list else { return };
let Some(stroke) = stroke else { return };
for paint_index in 0..stroke_graphic_list.len() {
let Some(stroke_graphic) = stroke_graphic_list.element(paint_index) else {
continue;
};
let cap = match stroke.cap {
StrokeCap::Butt => Cap::Butt,
StrokeCap::Round => Cap::Round,
StrokeCap::Square => Cap::Square,
};
let join = match stroke.join {
StrokeJoin::Miter => Join::Miter,
StrokeJoin::Bevel => Join::Bevel,
StrokeJoin::Round => Join::Round,
};
let dash_pattern = stroke.dash_lengths.iter().map(|l| l.max(0.)).collect();
let stroke = kurbo::Stroke {
width: stroke.weight * width_scale,
miter_limit: stroke.join_miter_limit,
join,
start_cap: cap,
end_cap: cap,
dash_pattern,
dash_offset: stroke.dash_offset,
};
if stroke.width <= 0. {
continue;
};
match stroke_graphic {
Graphic::Color(color) => {
let brush = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color());
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, None, &path);
}
Graphic::Gradient(gradient) => {
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(&LeafLane::new(stroke_graphic_list, paint_index, gradient), &multiplied_transform) else {
continue;
};
let inverse_element_transform = if transform_is_invertible(element_transform) {
element_transform.inverse()
} else {
Default::default()
};
let brush_transform = kurbo::Affine::new((inverse_element_transform * gradient_to_device).to_cols_array());
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), &path);
}
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) | Graphic::Group(_) => {
let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01);
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &stroked);
stroke_graphic.render_to_vello(scene, multiplied_transform, context, render_params);
scene.pop_layer();
}
};
}
};
// Render the path
match render_params.render_mode {
RenderMode::Outline => {
let (outline_stroke, outline_color_peniko) = get_outline_styles(render_params);
scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color_peniko, None, &path);
}
_ => {
if use_layer {
let mut cloned_element = element.clone();
cloned_element.stroke = None;
// The mask must draw at full alpha so `SrcOut` fully zeroes the path interior.
// The outer opacity/blend layer (above) handles the user-set opacity.
let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
let vector_list = List::new_from_item(mask_item);
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
// This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed
let inflation = stroke.map_or(0., |stroke| stroke.max_aabb_inflation(true));
let (largest_scale, _) = singular_values(applied_stroke_transform);
let quad = Quad::from_box(bounds).inflate(inflation * largest_scale);
let bounds = quad.bounding_box();
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
let compose = if stroke.is_some_and(|x| x.align == StrokeAlign::Outside) {
peniko::Compose::SrcOut
} else {
peniko::Compose::SrcIn
};
if wants_stroke_below {
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
vector_list.render_to_vello(scene, parent_transform, context, &render_params.for_alignment(applied_stroke_transform));
scene.push_layer(peniko::Fill::NonZero, peniko::BlendMode::new(peniko::Mix::Normal, compose), 1., kurbo::Affine::IDENTITY, &rect);
do_stroke(scene, 2., context);
scene.pop_layer();
scene.pop_layer();
do_fill(scene, context);
} else {
// Fill first (unclipped), then stroke (clipped) above
do_fill(scene, context);
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
vector_list.render_to_vello(scene, parent_transform, context, &render_params.for_alignment(applied_stroke_transform));
scene.push_layer(peniko::Fill::NonZero, peniko::BlendMode::new(peniko::Mix::Normal, compose), 1., kurbo::Affine::IDENTITY, &rect);
do_stroke(scene, 2., context);
scene.pop_layer();
scene.pop_layer();
}
} else {
// Non-aligned strokes or open paths: default order behavior
enum Op {
Fill,
Stroke,
}
let order = match stroke.is_some_and(|stroke| !stroke.paint_order.is_default()) {
true => [Op::Stroke, Op::Fill],
false => [Op::Fill, Op::Stroke], // Default
};
for operation in &order {
match operation {
Op::Fill => do_fill(scene, context),
Op::Stroke => do_stroke(scene, 1., context),
}
}
}
}
}
// If we pushed a layer for opacity or a blend mode, we need to pop it
if layer {
scene.pop_layer();
}
}
}
fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
// Aggregate all items' targets per element_id so multi-item lists (e.g. 'Text' node with "Separate Glyphs" active) produce hit areas for every glyph.
// Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`.
let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
let item_zero_inverse = if transform_is_invertible(item_zero_transform) {
item_zero_transform.inverse()
} else {
DAffine2::IDENTITY
};
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
let mut accumulated_outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
for index in 0..source.lane_count() {
let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index);
let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied();
if let Some(element_id) = caller_element_id.or(layer) {
// When recovering element_id from the item's editor:layer_path tag (because the caller
// passed None), also store the transform metadata that Graphic::collect_metadata
// normally provides but skipped due to the None element_id.
if caller_element_id.is_none() {
metadata.upstream_footprints.entry(element_id).or_insert(footprint);
metadata.local_transforms.entry(element_id).or_insert(item_zero_transform);
}
// Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes)
let click_target_vector = source.attr::<EditorClickTarget>(index).unwrap_or(element);
let item_relative_transform = item_zero_inverse * transform;
let mut click_targets_unwrapped = Vec::new();
extend_targets_from_vector(&mut click_targets_unwrapped, source, index, click_target_vector, item_relative_transform);
accumulated_click_targets.entry(element_id).or_default().extend(click_targets_unwrapped.into_iter().map(Arc::new));
// Outlines always use source geometry so the visual outline reflects actual letterforms
let mut outlines_unwrapped = Vec::new();
extend_targets_from_vector(&mut outlines_unwrapped, source, index, element, item_relative_transform);
accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new));
// Source geometry (not the click-target override) so editing tools work on letterforms.
// Recorded together with `vector_data` from the same (first) row so stroke geometry stays consistent with the paint.
// Only item 0 is recorded since editing tools can only target a single item currently.
// If that row has no paint attribute, none is recorded.
if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) {
e.insert(Arc::new(element.clone()));
if let Some(fill_graphic) = source.attr::<Fill>(index).filter(|list| is_paint_present(list)) {
metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.clone()));
}
if let Some(stroke_graphic) = source.attr::<Stroke>(index).filter(|list| is_paint_present(list)) {
metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.clone()));
}
}
// Surface `editor:text_frame` for the Text tool's drag cage
if let Some(frame) = source.try_attr::<EditorTextFrame>(index) {
metadata.text_frames.entry(element_id).or_insert(frame);
}
}
// If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation,
// Flatten Path, Morph, or any other destructive merge), recurse into that snapshot so the editor can
// surface the original child layers' click targets.
if let Some(upstream_nested_layers) = source.attr::<EditorMergedLayers>(index).filter(|layers| !layers.is_empty()) {
let mut upstream_footprint = footprint;
upstream_footprint.transform *= transform;
upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None);
}
}
// Overwrite with the full accumulated set (not just item 0's contribution)
for (element_id, targets) in accumulated_click_targets {
metadata.click_targets.insert(element_id, targets);
}
for (element_id, targets) in accumulated_outlines {
metadata.outlines.insert(element_id, targets);
}
}
fn add_vector_upstream_click_targets<S: LaneSource<Element = Vector>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
for index in 0..source.lane_count() {
let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index);
// Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes)
let vector = source.attr::<EditorClickTarget>(index).unwrap_or(element);
extend_targets_from_vector(click_targets, source, index, vector, transform);
}
}
fn add_vector_upstream_outline_targets<S: LaneSource<Element = Vector>>(source: &S, outlines: &mut Vec<ClickTarget>) {
// Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms
for index in 0..source.lane_count() {
let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index);
extend_targets_from_vector(outlines, source, index, element, transform);
}
}
impl Render for List<Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_vector_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_vector_vello(self, scene, parent_transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_vector_metadata(self, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_vector_upstream_click_targets(self, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_vector_upstream_outline_targets(self, outlines)
}
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
for vector in self.iter_element_values_mut() {
vector.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default());
}
}
}
/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work
/// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append.
fn extend_targets_from_vector<S: LaneSource<Element = Vector>>(targets: &mut Vec<ClickTarget>, source: &S, index: usize, geometry: &Vector, transform: DAffine2) {
let filled = has_paint::<Fill, _>(source, index);
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect();
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed());
// Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side,
// so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths.
let stroke_width = geometry.stroke.as_ref().map_or(0., |stroke| {
if stroke.align.is_not_centered() && all_subpaths_closed {
stroke.weight * 2.
} else {
stroke.weight
}
});
if filled {
for subpath in &mut subpaths {
subpath.set_closed(true);
}
}
if !subpaths.is_empty() {
let mut click_target = ClickTarget::new_with_compound_path(subpaths, stroke_width);
click_target.apply_transform(transform);
targets.push(click_target);
}
for click_target in extend_free_point_targets(geometry, transform) {
targets.push(click_target);
}
}
fn extend_free_point_targets(vector: &Vector, transform: DAffine2) -> impl Iterator<Item = ClickTarget> + '_ {
// Mark every point index touched by a segment endpoint in one `O(points + segments)` pass, avoiding a per-point `any_connected` scan
let mut connected = vec![false; vector.point_domain.len()];
for &point_index in vector.segment_domain.start_point().iter().chain(vector.segment_domain.end_point()) {
connected[point_index] = true;
}
vector.point_domain.ids().iter().enumerate().filter_map(move |(point_index, &point_id)| {
if connected[point_index] {
return None;
}
let anchor = vector.point_domain.position_from_id(point_id).unwrap_or_default();
let mut click_target = ClickTarget::new_with_free_point(FreePoint::new(point_id, anchor));
click_target.apply_transform(transform);
Some(click_target)
})
}
fn render_raster_cpu_svg<S: LaneSource<Element = Raster<CPU>>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
for index in 0..source.lane_count() {
let Some(image) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
if image.data.is_empty() {
continue;
}
if render_params.to_canvas() {
let mut image_copy = image.clone();
image_copy.data_mut().map_pixels(|p| p.to_unassociated_alpha());
let id = *render.image_data.entry(CacheHashWrapper(image_copy.into_data())).or_insert_with(generate_uuid);
render.parent_tag(
"foreignObject",
|attributes| {
let size = DVec2::new(image.width as f64, image.height as f64);
let matrix = transform * DAffine2::from_scale(1. / size);
let matrix = format_transform_matrix(matrix);
if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix);
}
attributes.push("width", size.x.to_string());
attributes.push("height", size.y.to_string());
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if blend_mode_attr != BlendMode::default() {
attributes.push("style", blend_mode_attr.render());
}
},
|render| {
render.leaf_tag(
"img", // Must be a self-closing (void element) tag, so we can't use `div` or `span`, for example
|attributes| {
attributes.push("data-canvas-placeholder", id.to_string());
},
)
},
);
} else {
let base64_string = image.base64_string.clone().unwrap_or_else(|| {
use base64::Engine;
let output = image.to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
base64_string
});
render.leaf_tag("image", |attributes| {
attributes.push("width", "1");
attributes.push("height", "1");
attributes.push("preserveAspectRatio", "none");
attributes.push("href", base64_string);
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push(ATTR_TRANSFORM, matrix);
}
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if blend_mode_attr != BlendMode::default() {
attributes.push("style", blend_mode_attr.render());
}
});
}
}
}
fn render_raster_cpu_vello<S: LaneSource<Element = Raster<CPU>> + BoundingBox>(source: &S, scene: &mut Scene, transform: DAffine2, render_params: &RenderParams) {
for index in 0..source.lane_count() {
let Some(image) = source.element(index) else { continue };
if image.data.is_empty() {
continue;
}
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let mut layer = false;
if (opacity < 1. || (render_params.render_mode != RenderMode::Outline && blend_mode_attr != BlendMode::default()))
&& let RenderBoundingBox::Rectangle(bounds) = source.bounding_box(transform, false)
{
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &rect);
layer = true;
}
let transform_attribute: DAffine2 = source.attr::<Transform>(index);
if let RenderMode::Outline = render_params.render_mode {
let outline_transform: DAffine2 = transform * transform_attribute;
draw_raster_outline(scene, &outline_transform, render_params);
if layer {
scene.pop_layer();
}
continue;
}
let image_transform = transform * transform_attribute * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
let image_brush = peniko::ImageBrush::new(peniko::ImageData {
data: image.to_flat_u8().0.into(),
format: peniko::ImageFormat::Rgba8,
width: image.width,
height: image.height,
alpha_type: peniko::ImageAlphaType::Alpha,
})
.with_extend(peniko::Extend::Repeat);
scene.draw_image(&image_brush, kurbo::Affine::new(image_transform.to_cols_array()));
if layer {
scene.pop_layer();
}
}
}
fn collect_raster_metadata<S: LaneSource>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return };
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one item of the `List<Raster<...>>`
if source.lane_count() > 0 {
let transform: DAffine2 = source.attr::<Transform>(0);
metadata.local_transforms.insert(element_id, transform);
// The snapshot's children already match `footprint`, so `transform` (the rasterization area) must not be applied.
if let Some(upstream_nested_layers) = source.attr::<EditorMergedLayers>(0).filter(|layers| !layers.is_empty()) {
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}
}
}
fn add_raster_upstream_click_targets(click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
}
impl Render for List<Raster<CPU>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_raster_cpu_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) {
render_raster_cpu_vello(self, scene, transform, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_raster_metadata(self, metadata, footprint, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_raster_upstream_click_targets(click_targets)
}
}
static LAZY_ARC_VEC_ZERO_U8: LazyLock<Arc<Vec<u8>>> = LazyLock::new(|| Arc::new(Vec::new()));
fn render_raster_gpu_vello<S: LaneSource<Element = Raster<GPU>> + BoundingBox>(source: &S, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for index in 0..source.lane_count() {
let Some(raster) = source.element(index) else { continue };
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let clip_attr: bool = source.attr::<ClippingMask>(index);
let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal,
_ => blend_mode_attr.to_peniko(),
};
let mut layer = false;
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let any_nondefault = blend_mode_attr != BlendMode::default() || opacity < 1. || clip_attr;
if (render_params.render_mode != RenderMode::Outline && any_nondefault)
&& let RenderBoundingBox::Rectangle(bounds) = source.bounding_box(transform, true)
{
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &rect);
layer = true;
}
let transform_attribute: DAffine2 = source.attr::<Transform>(index);
if let RenderMode::Outline = render_params.render_mode {
let outline_transform = transform * transform_attribute;
draw_raster_outline(scene, &outline_transform, render_params);
if layer {
scene.pop_layer();
}
continue;
}
let width = raster.data().width();
let height = raster.data().height();
let image = peniko::ImageBrush::new(peniko::ImageData {
data: peniko::Blob::new(LAZY_ARC_VEC_ZERO_U8.deref().clone()),
format: peniko::ImageFormat::Rgba8,
width,
height,
alpha_type: peniko::ImageAlphaType::Alpha,
})
.with_extend(peniko::Extend::Repeat);
let image_transform = transform * transform_attribute * DAffine2::from_scale(1. / DVec2::new(width as f64, height as f64));
scene.draw_image(&image, kurbo::Affine::new(image_transform.to_cols_array()));
context.resource_overrides.push((image, raster.texture.clone()));
if layer {
scene.pop_layer()
}
}
}
impl Render for List<Raster<GPU>> {
fn render_svg(&self, _render: &mut SvgRender, _render_params: &RenderParams) {
log::warn!("tried to render texture as an svg");
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_raster_gpu_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_raster_metadata(self, metadata, footprint, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_raster_upstream_click_targets(click_targets)
}
}
// Since colors and gradients are technically infinitely big, we have to implement
// workarounds for rendering them correctly in a way which still allows us
// to cache the intermediate render data (SVG string/Vello scene).
// For SVG, this is is achived by creating a truly giant rectangle.
// For Vello, we create a layer with a placeholder transform which we
// later replace with the current viewport transform before each render.
fn render_color_svg<S: LaneSource<Element = Color>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
{
for index in 0..source.lane_count() {
let Some(color) = source.element(index) else { continue };
let blend_mode: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
render.leaf_tag("polyline", |attributes| {
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
// 1e7 stays under that limit while still being far larger than any practical document extent.
const MAX: f64 = 1e7;
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
attributes.push("fill", format!("#{}", SRGBA8::from(*color).to_rgb_hex()));
if color.a() < 1. {
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
}
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if blend_mode != BlendMode::default() {
attributes.push("style", blend_mode.render());
}
});
}
}
}
fn render_color_vello<S: LaneSource<Element = Color>>(source: &S, scene: &mut Scene, render_params: &RenderParams) {
{
use vello::peniko;
for index in 0..source.lane_count() {
let Some(color) = source.element(index) else { continue };
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let vello_color = SRGBA8::from(*color).to_peniko_color();
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
let mut layer = false;
if opacity < 1. || blend_mode_attr != BlendMode::default() {
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect);
layer = true;
}
scene.fill(peniko::Fill::NonZero, kurbo::Affine::scale(f64::INFINITY), vello_color, None, &rect);
if layer {
scene.pop_layer();
}
}
}
}
impl Render for List<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_color_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_color_vello(self, scene, render_params)
}
}
fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
// For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`.
// The viewBox crops the overshoot. Canvas rendering keeps the polyline path since Chrome rejects rects larger than ~20 million.
let thumbnail_rect = if render_params.thumbnail {
let truncated_size = render_params.footprint.resolution.as_dvec2();
let margin = DVec2::ONE;
Some((render_params.footprint.transform.translation - margin / 2., truncated_size + margin))
} else {
None
};
for index in 0..source.lane_count() {
let Some(gradient) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let spread_method: GradientSpreadMethod = source.attr::<SpreadMethod>(index);
let gradient_type: GradientType = source.attr::<GradientTypeAttr>(index);
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
render.leaf_tag(tag, |attributes| {
if let Some((min, size)) = thumbnail_rect {
attributes.push("x", min.x.to_string());
attributes.push("y", min.y.to_string());
attributes.push("width", size.x.to_string());
attributes.push("height", size.y.to_string());
} else {
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
// 1e7 stays under that limit while still being far larger than any practical document extent.
const MAX: f64 = 1e7;
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
}
let mut stop_string = String::new();
for (position, color, original_midpoint) in gradient.interpolated_samples() {
let _ = write!(stop_string, r##"<stop offset="{}" stop-color="#{}""##, position, SRGBA8::from(color).to_rgb_hex());
if color.a() < 1. {
let _ = write!(stop_string, r#" stop-opacity="{}""#, color.a());
}
if let Some(midpoint) = original_midpoint {
let _ = write!(stop_string, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.);
}
stop_string.push_str(" />");
}
// render_thumbnail already added the footprint transform
let gradient_transform = if render_params.thumbnail { transform } else { render_params.footprint.transform * transform };
let gradient_transform_matrix = format_transform_matrix(gradient_transform);
let gradient_transform_attribute = if gradient_transform_matrix.is_empty() {
String::new()
} else {
format!(r#" gradientTransform="{gradient_transform_matrix}""#)
};
let gradient_id = generate_uuid();
let spread_method_attribute = if spread_method == GradientSpreadMethod::Pad {
String::new()
} else {
format!(r#" spreadMethod="{}""#, spread_method.svg_name())
};
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied
match gradient_type {
GradientType::Linear => {
let _ = write!(
&mut attributes.0.svg_defs,
r#"<linearGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</linearGradient>"#
);
}
GradientType::Radial => {
let _ = write!(
&mut attributes.0.svg_defs,
r#"<radialGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</radialGradient>"#
);
}
}
attributes.push("fill", format!("url('#{gradient_id}')"));
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if blend_mode != BlendMode::default() {
attributes.push("style", blend_mode.render());
}
});
}
}
fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &mut Scene, parent_transform: DAffine2, render_params: &RenderParams) {
use vello::peniko;
if let RenderMode::Outline = render_params.render_mode {
return;
}
for index in 0..source.lane_count() {
let Some(gradient) = source.element(index) else { continue };
let spread_method: GradientSpreadMethod = source.attr::<SpreadMethod>(index);
let gradient_type: GradientType = source.attr::<GradientTypeAttr>(index);
let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let gradient_transform = parent_transform * transform;
let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let mut stops: peniko::ColorStops = peniko::ColorStops::new();
for (position, color, _) in gradient.interpolated_samples() {
stops.push(peniko::ColorStop {
offset: position as f32,
color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color()),
})
}
let extend = match spread_method {
GradientSpreadMethod::Pad => peniko::Extend::Pad,
GradientSpreadMethod::Reflect => peniko::Extend::Reflect,
GradientSpreadMethod::Repeat => peniko::Extend::Repeat,
};
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied.
// For radial, the unit-radius circle at the origin scales out to the line's length once the brush transform applies.
let kind = match gradient_type {
GradientType::Linear => peniko::LinearGradientPosition {
start: to_point(DVec2::ZERO),
end: to_point(DVec2::X),
}
.into(),
GradientType::Radial => peniko::RadialGradientPosition {
start_center: to_point(DVec2::ZERO),
start_radius: 0.,
end_center: to_point(DVec2::ZERO),
end_radius: 1.,
}
.into(),
};
let fill = peniko::Brush::Gradient(peniko::Gradient {
kind,
stops,
extend,
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
..Default::default()
});
let brush_transform = kurbo::Affine::new(gradient_placement(gradient_transform, gradient_type).to_cols_array());
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
let mut layer = false;
if opacity < 1. || blend_mode_attr != BlendMode::default() {
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
// See implementation in `List<Color>` for more detail
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect);
layer = true;
}
// Encode shape and brush manually instead of Scene.fill(), which would multiply brush_transform by the path transform
scene.encoding_mut().encode_transform(vello_encoding::Transform::from_kurbo(&kurbo::Affine::scale(f64::INFINITY)));
scene.encoding_mut().encode_fill_style(peniko::Fill::NonZero);
scene.encoding_mut().encode_shape(&rect, true);
scene.encoding_mut().encode_transform(vello_encoding::Transform::from_kurbo(&brush_transform));
scene.encoding_mut().swap_last_path_tags();
scene.encoding_mut().encode_brush(&fill, 1.);
if layer {
scene.pop_layer();
}
}
}
impl Render for List<Gradient> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_gradient_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_gradient_vello(self, scene, parent_transform, render_params)
}
}
/// Builds a `kurbo::BezPath` from a glyph outline, baking in the glyph origin (`ox`, `oy`) and faux-italic shear (`tilt_tan`).
struct GlyphOutlinePen<'a> {
path: &'a mut BezPath,
ox: f64,
oy: f64,
tilt_tan: f64,
}
impl GlyphOutlinePen<'_> {
#[inline]
fn px(&self, x: f32, y: f32) -> f64 {
self.ox + x as f64 + (y as f64 * self.tilt_tan)
}
#[inline]
fn py(&self, y: f32) -> f64 {
self.oy - y as f64
}
}
impl OutlinePen for GlyphOutlinePen<'_> {
fn move_to(&mut self, x: f32, y: f32) {
self.path.move_to((self.px(x, y), self.py(y)));
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.line_to((self.px(x, y), self.py(y)));
}
fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
self.path.quad_to((self.px(cx, cy), self.py(cy)), (self.px(x, y), self.py(y)));
}
fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) {
self.path.curve_to((self.px(cx1, cy1), self.py(cy1)), (self.px(cx2, cy2), self.py(cy2)), (self.px(x, y), self.py(y)));
}
fn close(&mut self) {
self.path.close_path();
}
}
/// Draws each glyph of `glyph_run` into a `BezPath` (with the run's position and faux-italic `tilt_tan` baked in)
/// and calls `emit` for each non-empty glyph. Zero-geometry glyphs advance by `space_extra` for justified spacing.
fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, ()>, x_offset: f32, space_extra: f32, tilt_tan: f64, mut emit: impl FnMut(&BezPath)) {
let mut run_x = glyph_run.offset() + x_offset;
let run_y = glyph_run.baseline();
let run = glyph_run.run();
let font = run.font();
let font_size_pts = run.font_size();
let normalized_coords: Vec<NormalizedCoord> = run.normalized_coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect();
let Ok(font_ref) = SkrifaFontRef::from_index(font.data.as_ref(), font.index) else { return };
let outlines = font_ref.outline_glyphs();
let mut bez_path = BezPath::new();
for glyph in glyph_run.glyphs() {
let ox = (run_x + glyph.x) as f64;
let oy = (run_y - glyph.y) as f64;
run_x += glyph.advance;
let Some(outline) = outlines.get(GlyphId::from(glyph.id)) else { continue };
let settings = DrawSettings::unhinted(Size::new(font_size_pts), LocationRef::new(&normalized_coords));
bez_path.truncate(0);
let path = &mut bez_path;
let mut pen = GlyphOutlinePen { path, ox, oy, tilt_tan };
if outline.draw(settings, &mut pen).is_ok() && !bez_path.elements().is_empty() {
emit(&bez_path);
} else if space_extra != 0. && glyph.advance > 0. {
run_x += space_extra;
}
}
}
/// Lays out the text item at `index` and returns its local size and transform. The `BoundingBox` trait can't do
/// this since a bare `String` carries no typography, so click-target and bounding-box computation share this. Falls back to an em
/// square if the font isn't registered yet.
fn text_item_size_and_transform<S: LaneSource<Element = String>>(source: &S, index: usize) -> Option<(DVec2, DAffine2)> {
let text = source.element(index)?;
let font: Resource = {
let f = source.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
};
let font_size: f64 = source.attr::<FontSize>(index);
let line_height: f64 = source.attr::<LineHeight>(index);
let letter_spacing: f64 = source.attr::<LetterSpacing>(index);
let max_width: Option<f64> = source.attr::<MaxWidth>(index);
let max_height: Option<f64> = source.attr::<MaxHeight>(index);
let align: text_nodes::TextAlign = source.attr::<TextAlign>(index);
let transform: DAffine2 = source.attr::<Transform>(index);
let typesetting = text_nodes::TypesettingConfig {
font_size,
line_height_ratio: line_height,
letter_spacing,
letter_tilt: 0.,
max_width,
max_height,
align,
};
let (width, height) = text_nodes::TextContext::with_thread_local(|ctx| {
ctx.layout_text(text, &font, typesetting).map(|layout| {
let w = max_width.unwrap_or_else(|| layout.width() as f64);
let h = max_height.unwrap_or_else(|| layout.height() as f64);
(w, h)
})
})
.unwrap_or((font_size, font_size));
Some((DVec2::new(width, height), transform))
}
/// Union bounding box of a styled text source, laid out per item. The `BoundingBox` trait returns `None` for `List<String>`
/// (a bare `String` has no extent), so text-layer thumbnails and bounds use this instead. Each item is laid out under `outer_transform`.
pub fn text_list_bounding_box<S: LaneSource<Element = String>>(source: &S, outer_transform: DAffine2) -> RenderBoundingBox {
let mut bounds: Option<[DVec2; 2]> = None;
for index in 0..source.lane_count() {
let Some((size, transform)) = text_item_size_and_transform(source, index) else { continue };
let full_transform = outer_transform * transform;
for corner in [DVec2::ZERO, DVec2::new(size.x, 0.), DVec2::new(0., size.y), size] {
let point = full_transform.transform_point2(corner);
bounds = Some(match bounds {
Some([min, max]) => [min.min(point), max.max(point)],
None => [point, point],
});
}
}
match bounds {
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
None => RenderBoundingBox::None,
}
}
/// Like `List<Graphic>::thumbnail_bounding_box`, but lays out `Graphic::Text` items, which the `BoundingBox` trait reports as `None`.
/// Used for layer thumbnails so text layers (whose content is a `List<Graphic>` wrapping the text) frame their content.
pub fn graphic_list_bounding_box<'e, S: LaneSource<Element = Graphic<'e>>>(source: &S, transform: DAffine2) -> RenderBoundingBox {
let mut combined: Option<[DVec2; 2]> = None;
let mut any_infinite = false;
for index in 0..source.lane_count() {
let item_transform = transform * source.attr::<Transform>(index);
let Some(graphic) = source.element(index) else { continue };
let bounds = match graphic {
Graphic::Text(text) => text_list_bounding_box(&Single(text), item_transform),
Graphic::Graphic(sub_list) => graphic_list_bounding_box(sub_list, item_transform),
other => other.thumbnail_bounding_box(item_transform, true),
};
match bounds {
RenderBoundingBox::None => {}
RenderBoundingBox::Infinite => any_infinite = true,
RenderBoundingBox::Rectangle([min, max]) => {
combined = Some(match combined {
Some([existing_min, existing_max]) => [existing_min.min(min), existing_max.max(max)],
None => [min, max],
})
}
}
}
match (combined, any_infinite) {
(Some(bounds), _) => RenderBoundingBox::Rectangle(bounds),
(None, true) => RenderBoundingBox::Infinite,
(None, false) => RenderBoundingBox::None,
}
}
fn render_text_svg<S: LaneSource<Element = String>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
for index in 0..source.lane_count() {
let Some(text) = source.element(index) else { continue };
if text.is_empty() {
continue;
}
let transform: DAffine2 = source.attr::<Transform>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let font: Resource = {
let f = source.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
};
let font_size: f64 = source.attr::<FontSize>(index);
let line_height: f64 = source.attr::<LineHeight>(index);
let letter_spacing: f64 = source.attr::<LetterSpacing>(index);
let max_width: Option<f64> = source.attr::<MaxWidth>(index);
let max_height: Option<f64> = source.attr::<MaxHeight>(index);
let letter_tilt: f64 = source.attr::<LetterTilt>(index);
let align: text_nodes::TextAlign = source.attr::<TextAlign>(index);
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let typesetting = text_nodes::TypesettingConfig {
font_size,
line_height_ratio: line_height,
letter_spacing,
letter_tilt,
max_width,
max_height,
align,
};
let mut glyph_paths: Vec<String> = Vec::new();
text_nodes::TextContext::with_thread_local(|ctx| {
let Some(layout) = ctx.layout_text(text, &font, typesetting) else { return };
let tilt_tan = letter_tilt.to_radians().tan();
text_nodes::for_each_styled_glyph_run(&layout, text, typesetting, |glyph_run, x_offset, space_extra| {
draw_glyph_run_to_bezpaths(glyph_run, x_offset, space_extra, tilt_tan, |bez_path| {
glyph_paths.push(bez_path.to_svg());
});
});
});
if glyph_paths.is_empty() {
continue;
}
// Wrap all glyph <path> elements in a <g> with the item's transform/opacity/blend-mode.
render.parent_tag(
"g",
|attributes| {
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if opacity < 1. {
attributes.push("opacity", opacity.to_string());
}
if blend_mode_attr != BlendMode::default() {
attributes.push("style", blend_mode_attr.render());
}
},
|render| {
for path_d in glyph_paths {
render.leaf_tag("path", |attributes| {
attributes.push("d", path_d);
if let RenderMode::Outline = render_params.render_mode {
attributes.push("fill", "none");
attributes.push("stroke", "black");
attributes.push("stroke-width", "1");
} else {
attributes.push("fill", "black");
attributes.push("fill-rule", "nonzero");
}
});
}
},
);
}
}
fn render_text_vello<S: LaneSource<Element = String>>(source: &S, scene: &mut Scene, transform: DAffine2, render_params: &RenderParams) {
for index in 0..source.lane_count() {
let Some(text) = source.element(index) else { continue };
if text.is_empty() {
continue;
}
let item_transform: DAffine2 = source.attr::<Transform>(index);
let font: Resource = {
let f = source.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
};
let font_size: f64 = source.attr::<FontSize>(index);
let line_height: f64 = source.attr::<LineHeight>(index);
let letter_spacing: f64 = source.attr::<LetterSpacing>(index);
let max_width: Option<f64> = source.attr::<MaxWidth>(index);
let max_height: Option<f64> = source.attr::<MaxHeight>(index);
let letter_tilt: f64 = source.attr::<LetterTilt>(index);
let align: text_nodes::TextAlign = source.attr::<TextAlign>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let typesetting = text_nodes::TypesettingConfig {
font_size,
line_height_ratio: line_height,
letter_spacing,
letter_tilt,
max_width,
max_height,
align,
};
let affine = Affine::new((transform * item_transform).to_cols_array());
text_nodes::TextContext::with_thread_local(|ctx| {
let Some(layout) = ctx.layout_text(text, &font, typesetting) else { return };
let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default();
if needs_layer {
let alignment_width = max_width.map(|w| w as f32).unwrap_or_else(|| layout.full_width());
let blending = peniko::BlendMode::new(blend_mode_attr.to_peniko(), peniko::Compose::SrcOver);
let padding = font_size;
let bounds = kurbo::Rect::new(-padding, -padding, alignment_width as f64 + padding, layout.height() as f64 + padding);
let transformed_bounds = affine.transform_rect_bbox(bounds);
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &transformed_bounds);
}
let tilt_tan = letter_tilt.to_radians().tan();
text_nodes::for_each_styled_glyph_run(&layout, text, typesetting, |glyph_run, x_offset, space_extra| {
draw_glyph_run_to_bezpaths(glyph_run, x_offset, space_extra, tilt_tan, |bez_path| {
if let RenderMode::Outline = render_params.render_mode {
let (outline_stroke, outline_color) = get_outline_styles(render_params);
scene.stroke(&outline_stroke, affine, outline_color, None, bez_path);
} else {
scene.fill(peniko::Fill::NonZero, affine, peniko::Color::BLACK, None, bez_path);
}
});
});
if needs_layer {
scene.pop_layer();
}
});
}
}
fn collect_text_metadata<S: LaneSource<Element = String>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
// Click targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`.
let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
let item_zero_inverse = if item_zero_transform.matrix2.determinant() != 0. {
item_zero_transform.inverse()
} else {
DAffine2::IDENTITY
};
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
for index in 0..source.lane_count() {
let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied();
let Some(element_id) = caller_element_id.or(layer) else { continue };
// When recovering element_id from the item's tag (caller passed None), also store the transform metadata.
if caller_element_id.is_none() {
metadata.upstream_footprints.entry(element_id).or_insert(footprint);
metadata.local_transforms.entry(element_id).or_insert(item_zero_transform);
}
let Some((size, item_transform)) = text_item_size_and_transform(source, index) else { continue };
let subpath = Subpath::new_rectangle(DVec2::ZERO, size);
let mut target = ClickTarget::new_with_subpath(subpath, 0.);
target.apply_transform(item_zero_inverse * item_transform);
accumulated_click_targets.entry(element_id).or_default().push(Arc::new(target));
}
// One rectangle per text item, reused for the selection outline (there's no letterform geometry to outline at this stage).
for (element_id, targets) in accumulated_click_targets {
metadata.outlines.insert(element_id, targets.clone());
metadata.click_targets.insert(element_id, targets);
}
}
fn add_text_upstream_click_targets<S: LaneSource<Element = String>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
for index in 0..source.lane_count() {
let Some((size, transform)) = text_item_size_and_transform(source, index) else { continue };
let subpath = Subpath::new_rectangle(DVec2::ZERO, size);
let mut target = ClickTarget::new_with_subpath(subpath, 0.);
target.apply_transform(transform);
click_targets.push(target);
}
}
impl Render for List<String> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_text_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_text_vello(self, scene, transform, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_text_metadata(self, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_text_upstream_click_targets(self, click_targets)
}
}
impl Render for RunView<'_, Graphic<'_>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_graphic_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_graphic_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_graphic_metadata(self, metadata, footprint, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_graphic_upstream_click_targets(self, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_graphic_upstream_outline_targets(self, outlines)
}
fn contains_artboard(&self) -> bool {
graphic_contains_artboard(self)
}
}
impl Render for RunView<'_, Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_vector_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_vector_vello(self, scene, parent_transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_vector_metadata(self, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_vector_upstream_click_targets(self, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_vector_upstream_outline_targets(self, outlines)
}
}
impl Render for RunView<'_, Raster<CPU>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_raster_cpu_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) {
render_raster_cpu_vello(self, scene, transform, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_raster_metadata(self, metadata, footprint, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_raster_upstream_click_targets(click_targets)
}
}
impl Render for RunView<'_, Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_color_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_color_vello(self, scene, render_params)
}
}
impl Render for RunView<'_, Gradient> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_gradient_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_gradient_vello(self, scene, parent_transform, render_params)
}
}
impl Render for RunView<'_, Artboard<'_>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_artboard_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_artboard_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
collect_artboard_metadata(self, metadata, footprint)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_artboard_upstream_click_targets(self, click_targets)
}
fn contains_artboard(&self) -> bool {
self.lane_count() > 0
}
}
impl Render for RunView<'_, String> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_text_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_text_vello(self, scene, transform, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_text_metadata(self, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_text_upstream_click_targets(self, click_targets)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SvgSegment {
Slice(&'static str),
String(String),
}
impl From<String> for SvgSegment {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<&'static str> for SvgSegment {
fn from(value: &'static str) -> Self {
Self::Slice(value)
}
}
pub trait RenderSvgSegmentList {
fn to_svg_string(&self) -> String;
}
impl RenderSvgSegmentList for Vec<SvgSegment> {
fn to_svg_string(&self) -> String {
let mut result = String::new();
for segment in self.iter() {
result.push_str(match segment {
SvgSegment::Slice(x) => x,
SvgSegment::String(x) => x,
});
}
result
}
}
pub struct SvgRenderAttrs<'a>(&'a mut SvgRender);
impl SvgRenderAttrs<'_> {
pub fn push_complex(&mut self, name: impl Into<SvgSegment>, value: impl FnOnce(&mut SvgRender)) {
self.0.svg.push(" ".into());
self.0.svg.push(name.into());
self.0.svg.push("=\"".into());
value(self.0);
self.0.svg.push("\"".into());
}
pub fn push(&mut self, name: impl Into<SvgSegment>, value: impl Into<SvgSegment>) {
self.push_complex(name, move |renderer| renderer.svg.push(value.into()));
}
pub fn push_val(&mut self, value: impl Into<SvgSegment>) {
self.0.svg.push(value.into());
}
}
#[cfg(test)]
mod group_walk_tests {
use super::*;
use core_types::record::{FieldWrite, RunBuilder, element_write_hashed};
use graphic_types::markers::Fill;
use graphic_types::vector_types::vector::PointId;
fn unit_square_at(corner: DVec2) -> Vector {
Vector::from_subpath(Subpath::<PointId>::new_rectangle(corner, corner + DVec2::ONE))
}
fn color_paint() -> List<Graphic<'static>> {
List::new_from_element(Graphic::Color(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap()))
}
fn rendered_svg(render: impl FnOnce(&mut SvgRender)) -> (String, String) {
let mut svg_render = SvgRender::new();
render(&mut svg_render);
let output: SvgRenderOutput = svg_render.into();
(output.svg, output.svg_defs)
}
#[test]
fn a_vector_run_group_renders_its_rows_without_layer_wrappers() {
let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(3., 1.))];
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 2).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
builder.push(vectors[1].clone()).unwrap();
let item = builder.finish();
let group = Group { row: None, content: item };
let params = RenderParams::default();
let native = rendered_svg(|render| Graphic::Group(group.clone()).render_svg(render, &params));
let expected = "\n<path d=\"M0,0 L1,0 L1,1 L0,1 L0,0 Z\" fill=\"#e77c9b\"/>\n<path d=\"M3,1 L4,1 L4,2 L3,2 L3,1 Z\" fill=\"none\"/>";
assert_eq!(native, (expected.to_string(), String::new()));
}
#[test]
fn lane_paint_on_a_graphic_run_reaches_vector_interiors() {
let paint = color_paint();
let inner = Graphic::Vector(unit_square_at(DVec2::ZERO));
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Graphic>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let lane = builder.push(inner.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
let item = builder.finish();
let group = Group { row: None, content: item };
let params = RenderParams::default();
let native = rendered_svg(|render| Graphic::Group(group.clone()).render_svg(render, &params));
let legacy = rendered_svg(|render| Graphic::Graphic(graphic_types::graphic::group_to_legacy_list(&group)).render_svg(render, &params));
assert!(native.0.contains(r##"fill="#"##), "the lane's fill paint must reach the vector interior: {}", native.0);
assert_eq!(native, legacy);
}
#[test]
fn a_group_collects_its_lane_metadata_for_the_caller() {
let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO)];
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
let item = builder.finish();
let group = Group { row: None, content: item };
let footprint = Footprint::default();
let caller = NodeId(9);
let mut native = RenderMetadata::default();
Graphic::Group(group.clone()).collect_metadata(&mut native, footprint, Some(caller));
assert!(native.click_targets.get(&caller).is_some_and(|targets| !targets.is_empty()));
assert!(native.outlines.get(&caller).is_some_and(|targets| !targets.is_empty()));
assert!(native.local_transforms.contains_key(&caller));
assert!(native.upstream_footprints.contains_key(&caller));
assert_eq!(native.vector_data.get(&caller).map(|vector| vector.as_ref()), Some(&vectors[0]));
assert!(native.fill_attributes.get(&caller).is_some_and(|fill| matches!(fill.element(0), Some(Graphic::Color(_)))));
}
#[test]
fn a_group_serves_its_legacy_lowerings_click_targets() {
let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(2., 2.))];
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 2).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
builder.push(vectors[1].clone()).unwrap();
let item = builder.finish();
let group = Group { row: None, content: item };
let mut native = Vec::new();
Graphic::Group(group.clone()).add_upstream_click_targets(&mut native);
let mut legacy = Vec::new();
graphic_types::graphic::group_to_legacy_list(&group).add_upstream_click_targets(&mut legacy);
assert!(!native.is_empty());
assert_eq!(native, legacy);
let mut native_outlines = Vec::new();
Graphic::Group(group).add_upstream_outline_targets(&mut native_outlines);
assert_eq!(native_outlines, legacy);
}
}