From 985f85baecf381718c75853c15844707d08fec4f Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Tue, 8 Sep 2026 15:06:05 +0000 Subject: [PATCH] Adapt the merged tree to our rank model and paint attributes --- .../document/document_message_handler.rs | 29 +- .../utility_types/network_interface.rs | 3 +- .../network_interface/mutations.rs | 11 - .../network_interface/structure.rs | 11 +- .../interpreted-executor/src/node_registry.rs | 4 +- .../libraries/rendering/src/render_ext.rs | 139 +-- .../libraries/rendering/src/renderer.rs | 1051 ++--------------- 7 files changed, 148 insertions(+), 1100 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 4925fa070d..0b3783c154 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1518,19 +1518,13 @@ impl MessageHandler> for DocumentMes .collect(); self.network_interface.update_vector_data(layer_vector_data); } - DocumentMessage::UpdateAppearanceAttributes { appearance_attributes } => { - // Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers - let layer_appearance_attributes = appearance_attributes - .into_iter() - .filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id)) - .filter_map(|(node_id, attrs)| { - self.network_interface.is_layer(&node_id, &[]).then(|| { - let layer = LayerNodeIdentifier::new(node_id, &self.network_interface); - (layer, attrs) - }) - }) - .collect(); - self.network_interface.update_appearance_attributes(layer_appearance_attributes); + DocumentMessage::UpdateFillAttributes { fill_attributes } => { + let layer_fill_attributes = self.layer_keyed(fill_attributes); + self.network_interface.update_fill_attributes(layer_fill_attributes); + } + DocumentMessage::UpdateStrokeAttributes { stroke_attributes } => { + let layer_stroke_attributes = self.layer_keyed(stroke_attributes); + self.network_interface.update_stroke_attributes(layer_stroke_attributes); } DocumentMessage::Undo => { if self.network_interface.transaction_status() != TransactionStatus::Finished { @@ -1806,6 +1800,15 @@ impl MessageHandler> for DocumentMes } impl DocumentMessageHandler { + /// Rekeys a per-node attribute snapshot by layer, dropping nodes that are not layers. + fn layer_keyed(&self, attributes: HashMap) -> HashMap { + attributes + .into_iter() + .filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id)) + .filter_map(|(node_id, attrs)| self.network_interface.is_layer(&node_id, &[]).then(|| (LayerNodeIdentifier::new(node_id, &self.network_interface), attrs))) + .collect() + } + /// Build a document handler from a `.gdd` working copy. pub fn from_storage(interface: NodeNetworkInterface, storage: document_format::GddV1, name: String, path: Option) -> Self { let mut document = Self { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 50a11fb034..4505a68885 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -40,7 +40,8 @@ use graph_craft::Type; use graph_craft::application_io::resource::ResourceId; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; -use graphene_std::Appearance; +use graphene_std::Graphic; +use graphene_std::list::List; use graphene_std::ContextDependencies; use graphene_std::math::quad::Quad; use graphene_std::transform::Footprint; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs index 1b2b873985..dee89dd087 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs @@ -650,17 +650,6 @@ impl NodeNetworkInterface { node.call_argument = call_argument; } - pub fn set_context_features(&mut self, node_id: &NodeId, network_path: &[NodeId], context_features: ContextDependencies) { - let Some(network) = self.network_mut(network_path) else { - log::error!("Could not get nested network in set_context_features"); - return; - }; - let Some(node) = network.nodes.get_mut(node_id) else { - log::error!("Could not get node in set_context_features"); - return; - }; - node.context_features = context_features; - } /// Lightweight version of `set_input` for bulk import operations. /// Directly sets the input without `is_acyclic` checks, `load_structure`, position conversions, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/structure.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/structure.rs index 72003336d7..bef026381f 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/structure.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/structure.rs @@ -186,8 +186,13 @@ impl NodeNetworkInterface { self.document_metadata.layer_vector_data = new_layer_vector_data; } - /// Update the per-layer `ATTR_APPEARANCE` snapshot. - pub fn update_appearance_attributes(&mut self, new_layer_appearance_attributes: HashMap>) { - self.document_metadata.layer_appearance_attributes = new_layer_appearance_attributes; + /// Update the per-layer `ATTR_FILL` snapshot. + pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap>>>) { + self.document_metadata.layer_fill_attributes = new_layer_fill_attributes; + } + + /// Update the per-layer `ATTR_STROKE` snapshot. + pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap>>>) { + self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes; } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index b17d949a13..8fe328381a 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -81,9 +81,9 @@ fn node_registry() -> HashMap> { ); // The path flattening's plain vector rows, served under its identifier. node_types.extend( - graphene_std::vector::flatten_path_vector_entries() + graphene_std::vector::combine_paths_vector_entries() .into_iter() - .map(|entry| (graphene_std::vector::flatten_path::IDENTIFIER.clone(), entry)), + .map(|entry| (graphene_std::vector::combine_paths::IDENTIFIER.clone(), entry)), ); // The solidify's plain vector rows, served under its identifier. node_types.extend( diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index be65f804ee..74da9ddcb3 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -1,7 +1,4 @@ -use crate::renderer::{ - ClearGuardPlacement, ItemRef, RenderParams, composite_paint_colors, faded_paint_color, format_transform_matrix, gradient_placement, gradient_settings_from_item, spread_adjusted_samples, - transform_is_invertible, -}; +use crate::renderer::{RenderParams, format_transform_matrix, gradient_placement, transform_is_invertible}; use crate::{Render, RenderSvgSegmentList, SvgRender}; use core_types::Color; use core_types::attribute::Transform; @@ -10,12 +7,12 @@ use core_types::list::List; use core_types::uuid::generate_uuid; use glam::{DAffine2, DVec2}; use graphic_types::Graphic; -use graphic_types::vector_types::gradient::GradientForm; -use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientSpread as GradientSpreadAttr}; +use graphic_types::vector_types::gradient::GradientType; +use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod}; use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use std::fmt::Write; -use vector_types::Gradient; -use vector_types::gradient::GradientSpread; +use vector_types::GradientStops; +use vector_types::gradient::GradientSpreadMethod; #[derive(Copy, Clone, PartialEq)] pub enum PaintTarget { @@ -79,106 +76,17 @@ impl RenderExt for List { _element_transform: DAffine2, _stroke_transform: DAffine2, _bounds: DAffine2, - render_params: &RenderParams, + _render_params: &RenderParams, target: PaintTarget, ) -> Self::Output { render_color_paint(self, target) } } -/// Adds one gradient item's def into `svg_defs` and returns the gradient ID, or `None` when the item is absent. -/// `for_mask` keeps the fill opacity at full, as [`ItemRef::paint_opacity`] explains. -fn render_gradient_paint(item: Option>, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2, for_mask: bool) -> Option { - let mut stop = String::new(); +impl RenderExt for List { + type Output = u64; - let item = item?; - let stops = item.element()?; - let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); - let local_gradient_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - let settings = gradient_settings_from_item(item); - - let (mut samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder); - - let paint_opacity = item.paint_opacity(for_mask); - if paint_opacity < 1. { - for (_, color, _) in &mut samples { - *color = color.with_alpha(color.a() * paint_opacity); - } - } - - for (position, color, original_midpoint) in samples { - stop.push_str("") - } - - // A gradient with no stops paints as solid black, matching `Gradient::evaluate` (a stopless def would otherwise render as no paint per the SVG spec) - if stop.is_empty() { - stop.push_str(r##""); - } - - // Need to cancel out the element's transform as it is already applied to the path itself. - let element_transform_inverse = if transform_is_invertible(element_transform) { - element_transform.inverse() - } else { - DAffine2::IDENTITY - }; - - let document_transform = item_transform * local_gradient_transform; - - let placement = gradient_placement(document_transform, gradient_form); - let gradient_transform = format_transform_matrix(element_transform_inverse * placement); - let gradient_transform = if gradient_transform.is_empty() { - String::new() - } else { - format!(r#" gradientTransform="{gradient_transform}""#) - }; - - let gradient_spread = if matches!(settings.spread, GradientSpread::Pad | GradientSpread::Clear) { - String::new() - } else { - format!(r#" spreadMethod="{}""#, settings.spread.svg_name()) - }; - - let gradient_id = generate_uuid(); - - match gradient_form { - GradientForm::Linear => { - let _ = write!( - svg_defs, - r#"{}"#, - gradient_id, stop - ); - } - GradientForm::Radial => { - let _ = write!( - svg_defs, - r#"{}"#, - gradient_id, stop - ); - } - } - - Some(gradient_id) -} - -impl RenderExt for List { - type Output = Option; - - /// Adds the gradient def through mutating the first argument, returning the gradient ID, or `None` when the list is empty. + /// Adds the gradient def through mutating the first argument, returning the gradient ID. fn render( &self, svg_defs: &mut String, @@ -186,7 +94,7 @@ impl RenderExt for List { element_transform: DAffine2, _stroke_transform: DAffine2, _bounds: DAffine2, - render_params: &RenderParams, + _render_params: &RenderParams, _target: PaintTarget, ) -> Self::Output { render_gradient_paint(self, svg_defs, item_transform, element_transform) @@ -195,14 +103,14 @@ impl RenderExt for List { /// Adds the gradient def through mutating `svg_defs`, returning the gradient /// ID, over any gradient lane source. -pub fn render_gradient_paint>(source: &S, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2) -> u64 { +pub fn render_gradient_paint>(source: &S, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2) -> u64 { let mut stop = String::new(); { let Some(stops) = source.element(0) else { return 0 }; - let gradient_type: GradientForm = source.attr::(0); + let gradient_type: GradientType = source.attr::(0); let local_gradient_transform: DAffine2 = source.attr::(0); - let spread_method: GradientSpread = source.attr::(0); + let spread_method: GradientSpreadMethod = source.attr::(0); for (position, color, original_midpoint) in stops.interpolated_samples() { stop.push_str(" format!(r#" gradientTransform="{gradient_transform}""#) }; - let spread_method = if spread_method == GradientSpread::Pad { + let spread_method = if spread_method == GradientSpreadMethod::Pad { String::new() } else { format!(r#" spreadMethod="{}""#, spread_method.svg_name()) @@ -245,14 +153,14 @@ pub fn render_gradient_paint let gradient_id = generate_uuid(); match gradient_type { - GradientForm::Linear => { + GradientType::Linear => { let _ = write!( svg_defs, r#"{}"#, gradient_id, stop ); } - GradientForm::Radial => { + GradientType::Radial => { let _ = write!( svg_defs, r#"{}"#, @@ -268,7 +176,7 @@ pub fn render_gradient_paint impl RenderExt for Stroke { type Output = String; - /// Provide the shape-related SVG attributes for the stroke. The paint-related attributes for the stroke are generated from `Graphic::render` with `PaintTarget::Stroke`. + /// Provide the shape-related SVG attributes for the stroke. The paint-related attributes for the stroke are generated from `List.render` with `PaintTarget::Stroke`. fn render( &self, _svg_defs: &mut String, @@ -294,6 +202,7 @@ impl RenderExt for Stroke { let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join); let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit); let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align); + let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow); // Render the needed stroke attributes let mut attributes = String::new(); @@ -318,7 +227,7 @@ impl RenderExt for Stroke { if let Some(stroke_join_miter_limit) = stroke_join_miter_limit { let _ = write!(&mut attributes, r#" stroke-miterlimit="{stroke_join_miter_limit}""#); } - if render_params.stroke_below { + if paint_order.is_some() { let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#); } attributes @@ -338,6 +247,7 @@ impl RenderExt for List> { render_params: &RenderParams, target: PaintTarget, ) -> Self::Output { + let fill_graphic = self.element(0); let paint_attr = target.paint_attr(); match fill_graphic { @@ -346,7 +256,7 @@ impl RenderExt for List> { let gradient_id = render_gradient_paint(&core_types::lane::LeafLane::new(self, 0, gradient), svg_defs, item_transform, element_transform); format!(r##" {paint_attr}="url(#{gradient_id})""##) } - Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::GraphicList(_)) | Some(Graphic::Text(_)) | Some(Graphic::Group(_)) => { + Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) | Some(Graphic::Group(_)) => { let bounds = if target == PaintTarget::Stroke { // To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly. let inverse = |len: f64| if len > 0. { 1. / len } else { 0. }; @@ -361,13 +271,14 @@ impl RenderExt for List> { .map(|id| format!(r##" {paint_attr}="url(#{id})""##)) .unwrap_or_else(|| format!(r#" {paint_attr}="none""#)) } + None => format!(r#" {paint_attr}="none""#), } } } -/// Emits an SVG `` paint server into `svg_defs` that renders the given graphic as the paint content, and returns the pattern ID. +/// Emits an SVG `` paint server into `svg_defs` that renders the given graphic list as the paint content, and returns the pattern ID. /// Currently, this function is only used for clipping-based filling and stroking, not considering tiling yet. -fn render_svg_pattern(svg_defs: &mut String, paint: &Graphic, stroke_transform: DAffine2, bounds: DAffine2, render_params: &RenderParams) -> Option { +fn render_svg_pattern(svg_defs: &mut String, fill_graphic_list: &List, stroke_transform: DAffine2, bounds: DAffine2, render_params: &RenderParams) -> Option { let min = bounds.transform_point2(DVec2::ZERO); let max = bounds.transform_point2(DVec2::ONE); let size = max - min; @@ -377,7 +288,7 @@ fn render_svg_pattern(svg_defs: &mut String, paint: &Graphic, stroke_transform: // Render the pattern content recursively let mut content = SvgRender::new(); - paint.render_svg(&mut content, &render_params.for_pattern()); + fill_graphic_list.render_svg(&mut content, &render_params.for_pattern()); // Unwrap the inner def element write!(svg_defs, "{}", content.svg_defs).unwrap(); diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index d85d2cdafe..7a04057534 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -26,9 +26,9 @@ 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, GradientForm}; -use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientSpread as GradientSpreadAttr}; -use graphic_types::vector_types::vector::algorithms::shapes::rectangle_bezpath; +use graphic_types::vector_types::gradient::{GradientStops, 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}; @@ -44,134 +44,10 @@ use std::hash::Hash; use std::ops::Deref; use std::sync::{Arc, LazyLock}; use text_nodes::markers::{Font, TextAlign}; -use vector_types::gradient::GradientSpread; +use vector_types::gradient::GradientSpreadMethod; use vector_types::markers::EditorClickTarget; use vello::*; -/// A borrowed view of one item of ranked content: one index of a `List`'s attributes, or a lone `Item` reading its own envelope. -/// Lets the per-item render logic serve both the list impls and the `Graphic` leaf variants without cloning. -pub(crate) enum ItemRef<'a, T> { - ListItem(&'a List, usize), - Item(&'a Item), -} - -impl Copy for ItemRef<'_, T> {} -impl Clone for ItemRef<'_, T> { - fn clone(&self) -> Self { - *self - } -} - -impl<'a, T> ItemRef<'a, T> { - pub(crate) fn element(self) -> Option<&'a T> { - match self { - ItemRef::ListItem(list, index) => list.element(index), - ItemRef::Item(item) => Some(item.element()), - } - } - - pub(crate) fn attribute(self, key: &str) -> Option<&'a A> { - match self { - ItemRef::ListItem(list, index) => list.attribute(key, index), - ItemRef::Item(item) => item.attribute(key), - } - } - - pub(crate) fn attribute_cloned_or(self, key: &str, fallback: A) -> A { - match self { - ItemRef::ListItem(list, index) => list.attribute_cloned_or(key, index, fallback), - ItemRef::Item(item) => item.attribute_cloned_or(key, fallback), - } - } - - pub(crate) fn attribute_cloned_or_default(self, key: &str) -> A { - match self { - ItemRef::ListItem(list, index) => list.attribute_cloned_or_default(key, index), - ItemRef::Item(item) => item.attribute_cloned_or_default(key), - } - } - - /// The alpha multiplier this item's opacity attributes apply when it serves as a paint. - /// Fill opacity fades a paint just as opacity does, but a masker drops it so it cannot reach the content clipped to it. - pub(crate) fn paint_opacity(self, for_mask: bool) -> f32 { - let opacity_fill = if for_mask { 1. } else { self.attribute_cloned_or::(ATTR_OPACITY_FILL, 1.) }; - - (self.attribute_cloned_or::(ATTR_OPACITY, 1.) * opacity_fill) as f32 - } - - pub(crate) fn clone_item_attributes(self) -> core_types::list::ItemAttributeValues { - match self { - ItemRef::ListItem(list, index) => list.clone_item_attributes(index), - ItemRef::Item(item) => item.attributes().clone(), - } - } - - /// The last layer ID of the item's `editor:layer_path` tag, if any. - fn layer(self) -> Option { - self.attribute::(ATTR_EDITOR_LAYER_PATH).and_then(|path| path.0.iter_element_values().next_back().copied()) - } -} - -/// The color one paint item contributes, faded by its opacity attributes. -pub(crate) fn faded_paint_color(item: ItemRef<'_, Color>, for_mask: bool) -> Option { - let color = item.element()?; - - Some(color.with_alpha(color.a() * item.paint_opacity(for_mask))) -} - -/// Composites one paint color over the stack beneath it, mixing by the blend mode and then source-over in straight alpha. -fn composite_paint_over(over: Color, under: Color, blend_mode: BlendMode) -> Color { - let (over_alpha, under_alpha) = (over.a(), under.a()); - - // These modes only move the backdrop's alpha, leaving its color alone - match blend_mode { - BlendMode::Erase => return under.with_alpha((under_alpha - over_alpha).clamp(0., 1.)), - BlendMode::Restore => return under.with_alpha((under_alpha + over_alpha).clamp(0., 1.)), - BlendMode::MultiplyAlpha => return under.with_alpha(under_alpha * over_alpha), - _ => {} - } - - let result_alpha = over_alpha + under_alpha * (1. - over_alpha); - if result_alpha <= 0. { - return Color::TRANSPARENT; - } - - // The blend formulas read their backdrop premultiplied - let premultiplied_under = Color::from_rgbaf32_unchecked(under.r() * under_alpha, under.g() * under_alpha, under.b() * under_alpha, under_alpha); - let mixed = apply_blend_mode(over, premultiplied_under, blend_mode); - - // The mode only mixes where the backdrop has coverage, so its alpha interpolates each source channel from the raw color to the mixed color - let source_channel = |over_channel: f32, mixed_channel: f32| over_channel * (1. - under_alpha) + mixed_channel * under_alpha; - - let channel = - |mixed_channel: f32, over_channel: f32, under_channel: f32| (source_channel(over_channel, mixed_channel) * over_alpha + under_channel * under_alpha * (1. - over_alpha)) / result_alpha; - - Color::from_rgbaf32_unchecked( - channel(mixed.r(), over.r(), under.r()), - channel(mixed.g(), over.g(), under.g()), - channel(mixed.b(), over.b(), under.b()), - result_alpha, - ) -} - -/// Flattens a rank-1 color paint into the single color the fast path emits, stacking the items in paint order. -pub(crate) fn composite_paint_colors(list: &List, for_mask: bool) -> Option { - let mut composited = None; - - for index in 0..list.len() { - let item = ItemRef::ListItem(list, index); - let Some(faded) = faded_paint_color(item, for_mask) else { continue }; - - composited = Some(match composited { - // The lowest paint has nothing beneath it, so its blend mode has nothing to act on - None => faded, - Some(under) => composite_paint_over(faded, under, item.attribute_cloned_or_default(ATTR_BLEND_MODE)), - }); - } - - composited -} - #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] enum MaskType { @@ -354,45 +230,28 @@ pub struct RenderParams { /// Are we generating a mask for alignment? Used to prevent unnecessary transforms in masks pub alignment_parent_transform: Option, pub aligned_strokes: bool, - /// Paint the stroke below the fill within the same SVG path element - pub stroke_below: bool, + pub override_paint_order: bool, /// Are we rendering for a pattern content pub inside_pattern: bool, pub artboard_background: Option, /// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode. pub viewport_zoom: f64, - /// The nearest ancestor's appearance, cascading to items that lack their own. - pub inherited_appearance: Option, } impl RenderParams { pub fn for_clipper(&self) -> Self { - Self { for_mask: true, ..self.clone() } + Self { for_mask: true, ..*self } } pub fn for_alignment(&self, transform: DAffine2) -> Self { Self { alignment_parent_transform: Some(transform), - ..self.clone() + ..*self } } pub fn for_pattern(&self) -> Self { - // A paint subtree supplies its own styling, so the painted element's appearance must not cascade into it - Self { - inside_pattern: true, - inherited_appearance: None, - ..self.clone() - } - } - - /// Params for rendering a child item, cascading this item's appearance to descendants lacking their own. - /// Callers only build these when the item carries a declared appearance, so an item without one clones nothing. - pub fn for_child_item(&self, item_appearance: &Appearance) -> Self { - Self { - inherited_appearance: Some(item_appearance.clone()), - ..self.clone() - } + Self { inside_pattern: true, ..*self } } pub fn to_canvas(&self) -> bool { @@ -481,9 +340,11 @@ fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::C } 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 = rectangle_path(DVec2::ZERO, DVec2::ONE); + let mut outline_path = Subpath::::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); @@ -494,7 +355,7 @@ fn draw_raster_outline(scene: &mut Scene, outline_transform: &DAffine2, render_p fn emit_svg_fill_path( render: &mut SvgRender, d: String, - fill_paint: Option<&Graphic>, + fill_graphic_list: Option<&List>, item_transform: DAffine2, element_transform: DAffine2, applied_stroke_transform: DAffine2, @@ -508,21 +369,13 @@ fn emit_svg_fill_path( attributes.push(ATTR_TRANSFORM, matrix); } let defs = &mut attributes.0.svg_defs; - let fill_attribute = fill_paint - .map(|paint| paint.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, render_params, PaintTarget::Fill)) + 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); }); } -/// The whole-ramp settings a gradient item carries beside its element, defaulting each absent one. -pub(crate) fn gradient_settings_from_item(item: ItemRef<'_, Gradient>) -> GradientSettings { - match item { - ItemRef::ListItem(list, index) => GradientSettings::from_list_row_attributes(list, index), - ItemRef::Item(item) => GradientSettings::from_item_attributes(item), - } -} - /// 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() @@ -532,10 +385,10 @@ pub(crate) fn transform_is_invertible(transform: DAffine2) -> bool { /// 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_form: GradientForm) -> DAffine2 { - match gradient_form { - GradientForm::Radial => transform, - GradientForm::Linear => { +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 }; @@ -547,145 +400,32 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_form: GradientFor } } -fn create_peniko_gradient_brush>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { +fn create_peniko_gradient_brush>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { let stops = gradient_list.element(0)?; - let gradient_type: GradientForm = gradient_list.attr::(0); + let gradient_type: GradientType = gradient_list.attr::(0); let gradient_transform: DAffine2 = gradient_list.attr::(0); - let spread_method: GradientSpread = gradient_list.attr::(0); + let spread_method: GradientSpreadMethod = gradient_list.attr::(0); -/// Where a renderer needs the transparent guard stops that emulate the `Clear` spread, which neither SVG nor Vello supports natively. -#[derive(Copy, Clone, PartialEq)] -pub(crate) enum ClearGuardPlacement { - /// Guards share the range ends' exact offsets, resolved against the visible colors by stop order alone. - SvgStopOrder, - /// Guards own the outermost ramp texel at each cleared end, since Vello's pad extension samples those texels for - /// everything beyond the ends and its ramp bake would tie-break a shared-offset guard away. The visible range - /// compresses inward by one texel per cleared end, costing about 0.4% of the ramp's color resolution. - VelloRampTexels, -} - -/// The gradient's renderable samples plus the gradient-space span `(start, end)` the renderer's 0 to 1 offset range must cover, normally the unit interval with the samples unchanged. -/// -/// The `Clear` spread brackets the samples with transparent guard stops placed per `guards`: the pad extension then -/// paints transparency outward while hard stops cut the paint off exactly at the unit range's boundaries. A radial -/// gradient's span still starts at zero, since its sampling distance never goes below the center. -pub(crate) fn spread_adjusted_samples(gradient: &Gradient, settings: GradientSettings, gradient_form: GradientForm, guards: ClearGuardPlacement) -> (GradientSamples, (f64, f64)) { - let samples = gradient.interpolated_samples(settings); - if settings.spread != GradientSpread::Clear { - return (samples, (0., 1.)); - } - - // The remapped offsets where the visible range's ends land, with the guards owning whatever lies outside them - let texel = 1. / (VELLO_GRADIENT_RAMP_TEXELS - 1.); - let (start_offset, end_offset) = match (guards, gradient_form) { - (ClearGuardPlacement::SvgStopOrder, _) => (0., 1.), - (ClearGuardPlacement::VelloRampTexels, GradientForm::Linear) => (texel, 1. - texel), - (ClearGuardPlacement::VelloRampTexels, GradientForm::Radial) => (0., 1. - texel), - }; - let remap = |position: f64| (1. - position) * start_offset + position * end_offset; - - // The geometric span grows to compensate for the compression, keeping the visible range at the unit interval - let scale = 1. / (end_offset - start_offset); - let span = (-start_offset * scale, (1. - start_offset) * scale); - - // A stopless gradient paints solid black, matching `Gradient::evaluate` - let first_color = samples.first().map_or(Color::BLACK, |&(_, color, _)| color); - let last_color = samples.last().map_or(Color::BLACK, |&(_, color, _)| color); - let needs_start_anchor = samples.first().is_none_or(|&(position, ..)| position > 0.); - let needs_end_anchor = samples.last().is_none_or(|&(position, ..)| position < 1.); - - let mut adjusted = Vec::with_capacity(samples.len() + 4); - - // Lead with the transparent guard (linear only, a radial's center is already the sampling minimum), then anchor the visible range's start color - if gradient_form == GradientForm::Linear { - adjusted.push((0., Color::TRANSPARENT, None)); - } - if needs_start_anchor { - adjusted.push((remap(0.), first_color, None)); - } - - adjusted.extend(samples.into_iter().map(|(position, color, midpoint)| (remap(position), color, midpoint))); - - // Anchor the visible range's end color, then cut to the trailing transparent guard - if needs_end_anchor { - adjusted.push((remap(1.), last_color, None)); - } - adjusted.push((1., Color::TRANSPARENT, None)); - - (adjusted, span) -} - -/// Converts a gradient's renderer samples to peniko color stops, duplicating an off-zero first stop at position 0 since Vello ignores the first stop's position and always treats it as 0. -fn peniko_color_stops(samples: &[(f64, Color, Option)]) -> peniko::ColorStops { let mut peniko_stops = peniko::ColorStops::new(); - - for &(position, color, _) in samples { - let color = peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color()); - - if peniko_stops.is_empty() && position > 0. { - peniko_stops.push(peniko::ColorStop { offset: 0., color }); - } - - peniko_stops.push(peniko::ColorStop { offset: position as f32, color }); - } - - // A gradient with no stops paints as solid black, matching `Gradient::evaluate` - if peniko_stops.is_empty() { + for (position, color, _) in stops.interpolated_samples() { peniko_stops.push(peniko::ColorStop { - offset: 0., - color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(Color::BLACK).to_peniko_color()), + offset: position as f32, + color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color()), }); } - peniko_stops -} - -/// The peniko extend mode for a spread; `Clear` rides pad, with the transparent guard stops from `spread_adjusted_samples` doing the clearing. -fn peniko_extend(gradient_spread: GradientSpread) -> peniko::Extend { - match gradient_spread { - GradientSpread::Pad | GradientSpread::Clear => peniko::Extend::Pad, - GradientSpread::Reflect => peniko::Extend::Reflect, - GradientSpread::Repeat => peniko::Extend::Repeat, - } -} - -/// The Vello brush for one gradient item, paired with its placement transform. -/// `for_mask` keeps the fill opacity at full, as [`ItemRef::paint_opacity`] explains. -fn create_peniko_gradient_brush(gradient_item: ItemRef<'_, Gradient>, multiplied_transform: &DAffine2, for_mask: bool) -> Option<(peniko::Brush, DAffine2)> { - let stops = gradient_item.element()?; - - let gradient_form: GradientForm = gradient_item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); - let gradient_transform: DAffine2 = gradient_item.attribute_cloned_or_default(ATTR_TRANSFORM); - let settings = gradient_settings_from_item(gradient_item); - - let (mut samples, span) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::VelloRampTexels); - - let paint_opacity = gradient_item.paint_opacity(for_mask); - if paint_opacity < 1. { - // A stopless ramp gets its black stop downstream, too late to be faded, so it needs one here instead - if samples.is_empty() { - samples.push((0., Color::BLACK, None)); - } - - for (_, color, _) in &mut samples { - *color = color.with_alpha(color.a() * paint_opacity); - } - } - - let peniko_stops = peniko_color_stops(&samples); - // 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::X * span.0, DVec2::X * span.1, gradient_placement(multiplied_transform * gradient_transform, gradient_form)); + 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_form { - GradientForm::Linear => peniko::LinearGradientPosition { + kind: match gradient_type { + GradientType::Linear => peniko::LinearGradientPosition { start: to_point(start), end: to_point(end), } .into(), - GradientForm::Radial => peniko::RadialGradientPosition { + GradientType::Radial => peniko::RadialGradientPosition { start_center: to_point(start), start_radius: 0., end_center: to_point(start), @@ -693,10 +433,13 @@ fn create_peniko_gradient_brush(gradient_item: ItemRef<'_, Gradient>, multiplied } .into(), }, - extend: peniko_extend(settings.spread), + extend: match spread_method { + GradientSpreadMethod::Pad => peniko::Extend::Pad, + GradientSpreadMethod::Reflect => peniko::Extend::Reflect, + GradientSpreadMethod::Repeat => peniko::Extend::Repeat, + }, stops: peniko_stops, - // Straight alpha, keeping parity with the SVG renderer's stop interpolation - interpolation_alpha_space: peniko::InterpolationAlphaSpace::Unpremultiplied, + interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied, ..Default::default() }); @@ -715,12 +458,12 @@ pub struct RenderMetadata { /// 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>>, - /// Per-layer text frame from item 0's `editor:text_frame` attribute. + /// 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, pub clip_targets: HashSet, pub vector_data: HashMap>, - /// Per-layer `ATTR_APPEARANCE` item attribute, exposed so message handlers can read it. + /// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read it. #[cfg_attr(feature = "serde", serde(skip))] pub fill_attributes: HashMap>>>, /// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read it. @@ -749,7 +492,8 @@ impl RenderMetadata { text_frames, clip_targets, vector_data, - appearance_attributes, + fill_attributes, + stroke_attributes, backgrounds, } = self; upstream_footprints.extend(other.upstream_footprints.iter()); @@ -760,7 +504,8 @@ impl RenderMetadata { 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()))); - appearance_attributes.extend(other.appearance_attributes.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 { @@ -784,19 +529,18 @@ pub trait Render: BoundingBox + RenderComplexity { 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. - /// `inherited_appearance` is the nearest ancestor's appearance, cascading to items that lack their own, mirroring the render cascade. - fn add_upstream_click_targets(&self, _click_targets: &mut Vec, _inherited_appearance: Option<&Appearance>) {} + fn add_upstream_click_targets(&self, _click_targets: &mut Vec) {} /// Like `add_upstream_click_targets` but for visual outlines. `List` overrides this to ignore `editor:click_target` so outlines reflect the actual geometry. - fn add_upstream_outline_targets(&self, outlines: &mut Vec, inherited_appearance: Option<&Appearance>) { - self.add_upstream_click_targets(outlines, inherited_appearance); + fn add_upstream_outline_targets(&self, outlines: &mut Vec) { + 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)>, current_path: Option) {} /// 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, _inherited_appearance: Option<&Appearance>) {} + fn collect_metadata(&self, _metadata: &mut RenderMetadata, _footprint: Footprint, _element_id: Option) {} fn contains_artboard(&self) -> bool { false @@ -808,7 +552,7 @@ pub trait Render: BoundingBox + RenderComplexity { impl Render for Graphic<'_> { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { match self { - Graphic::GraphicList(list) => list.render_svg(render, render_params), + 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(_) => (), @@ -821,7 +565,7 @@ impl Render for Graphic<'_> { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { match self { - Graphic::GraphicList(list) => list.render_to_vello(scene, transform, context, render_params), + 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), @@ -846,14 +590,14 @@ impl Render for Graphic<'_> { fn contains_artboard(&self) -> bool { match self { - Graphic::GraphicList(list) => list.contains_artboard(), + Graphic::Graphic(list) => list.contains_artboard(), _ => false, } } fn new_ids_from_hash(&mut self, reference: Option) { match self { - Graphic::GraphicList(list) => list.new_ids_from_hash(reference), + 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()), _ => (), } @@ -863,7 +607,7 @@ impl Render for Graphic<'_> { 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::GraphicList(inner) => render_graphic_svg_with(inner, reach.nested(), 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), } @@ -872,7 +616,7 @@ fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: & 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::GraphicList(inner) => render_graphic_vello_with(inner, reach.nested(), 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), } @@ -903,7 +647,7 @@ fn collect_element_metadata<'a>( metadata.upstream_footprints.insert(element_id, footprint); match element { Graphic::Group(group) => collect_group_row_metadata(group, metadata, element_id), - Graphic::GraphicList(_) => {} + 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); @@ -916,7 +660,7 @@ fn collect_element_metadata<'a>( } match element { - Graphic::GraphicList(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id), + 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), @@ -950,7 +694,7 @@ fn collect_group_row_metadata(group: &Group, metadata: &mut RenderMetadata, elem .or_else(|| lane_zero_transform::>(item)) .or_else(|| lane_zero_transform::>(item)) .or_else(|| lane_zero_transform::(item)) - .or_else(|| lane_zero_transform::(item)) + .or_else(|| lane_zero_transform::(item)) .or_else(|| lane_zero_transform::(item)); if let Some(transform) = transform { metadata.local_transforms.insert(element_id, transform); @@ -959,7 +703,7 @@ fn collect_group_row_metadata(group: &Group, metadata: &mut RenderMetadata, elem fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, click_targets: &mut Vec) { match element { - Graphic::GraphicList(list) => add_graphic_upstream_click_targets_with(list, reach.nested(), click_targets), + 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), @@ -971,7 +715,7 @@ fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReac fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, outlines: &mut Vec) { match element { - Graphic::GraphicList(list) => add_graphic_upstream_outline_targets_with(list, reach.nested(), outlines), + 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), @@ -997,7 +741,7 @@ fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut Sv } else if item.typed_lanes::>().is_some() { } else if let Some(run) = RunView::::new(item) { render_color_svg(&run, render, render_params) - } else if let Some(run) = RunView::::new(item) { + } else if let Some(run) = RunView::::new(item) { render_gradient_svg(&run, render, render_params) } else if let Some(run) = RunView::::new(item) { render_text_svg(&run, render, render_params) @@ -1019,7 +763,7 @@ fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut S render_raster_gpu_vello(&run, scene, transform, context, render_params) } else if let Some(run) = RunView::::new(item) { render_color_vello(&run, scene, render_params) - } else if let Some(run) = RunView::::new(item) { + } else if let Some(run) = RunView::::new(item) { render_gradient_vello(&run, scene, transform, render_params) } else if let Some(run) = RunView::::new(item) { render_text_vello(&run, scene, transform, render_params) @@ -1042,7 +786,7 @@ fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata: collect_raster_metadata(&run, metadata, footprint, element_id) } else if let Some(run) = RunView::>::new(item) { collect_raster_metadata(&run, metadata, footprint, element_id) - } else if item.typed_lanes::().is_some() || item.typed_lanes::().is_some() { + } else if item.typed_lanes::().is_some() || item.typed_lanes::().is_some() { } else if let Some(run) = RunView::::new(item) { collect_text_metadata(&run, metadata, footprint, element_id) } @@ -1186,7 +930,7 @@ fn collect_artboard_metadata<'a, S: LaneSource>>(source: let element_id = layer_path.last().copied(); if let Some(element_id) = element_id { - let subpath = rectangle_bezpath(DVec2::ZERO, dimensions); + 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)); @@ -1206,7 +950,7 @@ fn collect_artboard_metadata<'a, S: LaneSource>>(source: fn add_artboard_upstream_click_targets<'a, S: LaneSource>>(source: &S, click_targets: &mut Vec) { for index in 0..source.lane_count() { let dimensions: DVec2 = source.attr::(index); - let subpath_rectangle = rectangle_bezpath(DVec2::ZERO, dimensions); + let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions); click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); } } @@ -1624,313 +1368,6 @@ fn render_vector_svg>(source: &S, render: &mut S render_params, ); } - }; - }; - - 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() { - 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 { - do_fill_path(scene, context, &path, peniko::Fill::NonZero); - } - }; - - let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| { - let Some(paint) = stroke_paint else { return }; - let Some(stroke) = stroke else { return }; - - 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. { - return; - }; - - let solid_stroke = |scene: &mut Scene, color: Option| { - let Some(color) = color else { return }; - - 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); - }; - let gradient_stroke = |scene: &mut Scene, gradient_item: ItemRef<'_, Gradient>| { - let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(gradient_item, &multiplied_transform, render_params.for_mask) else { - return; - }; - 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); - }; - - match paint { - Graphic::None(_) | Graphic::NoneList(_) => (), - Graphic::Color(item) => solid_stroke(scene, faded_paint_color(ItemRef::Item(item), render_params.for_mask)), - Graphic::ColorList(list) => solid_stroke(scene, composite_paint_colors(list, render_params.for_mask)), - Graphic::Gradient(item) => gradient_stroke(scene, ItemRef::Item(item)), - // Stacked gradients cannot be composited into one brush, so they fall through to the clipped texture path - Graphic::GradientList(list) if list.len() <= 1 => gradient_stroke(scene, ItemRef::ListItem(list, 0)), - // Any other graphic content paints as a texture clipped to the stroked region - Graphic::GradientList(_) - | Graphic::GraphicList(_) - | Graphic::Vector(_) - | Graphic::RasterCPU(_) - | Graphic::RasterGPU(_) - | Graphic::Text(_) - | Graphic::VectorList(_) - | Graphic::RasterCPUList(_) - | Graphic::RasterGPUList(_) - | Graphic::GraphicList(_) - | Graphic::TextList(_) - | Graphic::StrokeList(_) => { - 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); - paint.render_to_vello(scene, multiplied_transform, context, paint_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 cloned_element = element.clone(); - - // 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); - let black_fill = Graphic::ColorList(List::new_from_element(Color::BLACK)); - mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill)); - 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 wants_stroke_below { - 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 clip_layers { - scene.pop_layer(); - scene.pop_layer(); - } - - // If we pushed a layer for opacity or a blend mode, we need to pop it - if layer { - scene.pop_layer(); - } -} - -/// The full metadata pass over a run of vector items. -/// Aggregates all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) produce hit areas for every glyph. -/// Targets are baked relative to the first item carrying each element_id, since that is the transform recorded as its `local_transforms` entry. -fn collect_vector_items_metadata<'a>( - items: impl Iterator>, - metadata: &mut RenderMetadata, - footprint: Footprint, - caller_element_id: Option, - inherited_appearance: Option<&Appearance>, -) { - let mut reference_transforms: HashMap = HashMap::new(); - - let mut accumulated_click_targets: HashMap>> = HashMap::new(); - let mut accumulated_outlines: HashMap>> = HashMap::new(); - - for item in items { - let Some(source) = item.element() else { continue }; - let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - // The item's own appearance wins over one cascading down from an ancestor - let appearance = Appearance::cascade(item.attribute::(ATTR_APPEARANCE), inherited_appearance); - - if let Some(element_id) = caller_element_id.or(item.layer()) { - let reference_transform = *reference_transforms.entry(element_id).or_insert(transform); - let reference_inverse = if transform_is_invertible(reference_transform) { - reference_transform.inverse() - } else { - DAffine2::IDENTITY - }; - - // Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes) - let click_target_vector = item.attribute::(ATTR_EDITOR_CLICK_TARGET).unwrap_or(source); - - let item_relative_transform = reference_inverse * transform; - - let mut click_targets_unwrapped = Vec::new(); - extend_targets_from_vector(&mut click_targets_unwrapped, appearance, 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, appearance, source, 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) item 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 item 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(source.clone())); - - if let Some(appearance) = appearance { - metadata.appearance_attributes.insert(element_id, Arc::new(appearance.clone())); - } - } - - // Surface `editor:text_frame` for the Text tool's drag cage - if let Some(&frame) = item.attribute::(ATTR_EDITOR_TEXT_FRAME) { - 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, - // Combine Paths, Morph, or any other destructive merge), recurse into that snapshot so the editor can - // surface the original child layers' click targets. - let upstream_nested_layers = item.attribute_cloned_or_default::>(ATTR_EDITOR_MERGED_LAYERS); - if !upstream_nested_layers.is_empty() { - let mut upstream_footprint = footprint; - upstream_footprint.transform *= transform; - // Snapshot layers carry their own styling, so the merged result's appearance must not cascade into them - upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None, 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); - } - - // Recovering element_id from `editor:layer_path` means `Graphic::collect_metadata` skipped this transform metadata. - // It lands after the snapshot recursion above so each element keeps the pair its targets were baked against. - if caller_element_id.is_none() { - for (element_id, reference_transform) in reference_transforms { - metadata.upstream_footprints.insert(element_id, footprint); - metadata.local_transforms.insert(element_id, reference_transform); - } - } -} - -/// Collects one vector item's click target into the caller's list, baked through the item's transform. -fn add_vector_item_click_targets(item: ItemRef<'_, Vector>, click_targets: &mut Vec, inherited_appearance: Option<&Appearance>) { - let Some(source) = item.element() else { return }; - let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - let appearance = Appearance::cascade(item.attribute::(ATTR_APPEARANCE), inherited_appearance); - - // Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes) - let vector = item.attribute::(ATTR_EDITOR_CLICK_TARGET).unwrap_or(source); - - extend_targets_from_vector(click_targets, appearance, vector, transform); -} - -/// Like [`add_vector_item_click_targets`] but on source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms. -fn add_vector_item_outline_targets(item: ItemRef<'_, Vector>, outlines: &mut Vec, inherited_appearance: Option<&Appearance>) { - let Some(source) = item.element() else { return }; - let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - let appearance = Appearance::cascade(item.attribute::(ATTR_APPEARANCE), inherited_appearance); - - extend_targets_from_vector(outlines, appearance, source, transform); -} - -impl Render for List { - fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { - let mut clip_mask_state: Option<(u64, MaskType)> = None; - - for index in 0..self.len() { - // A clip-flagged item is masked by its nearest preceding unflagged sibling, which a consecutive run shares - let next_clips = index + 1 < self.len() && self.attribute_cloned_or_default::(ATTR_CLIPPING_MASK, index + 1); - render_vector_item_svg(ItemRef::ListItem(self, index), next_clips, &mut clip_mask_state, render, render_params); } render.leaf_tag("path", |attributes| { @@ -2143,7 +1580,7 @@ fn render_vector_vello>(source: &S, scene: &mut 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::GraphicList(_) | Graphic::Text(_) | Graphic::Group(_) => { + 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(); @@ -2224,7 +1661,7 @@ fn render_vector_vello>(source: &S, scene: &mut scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), &path); } - Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::GraphicList(_) | Graphic::Text(_) | Graphic::Group(_) => { + 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); @@ -2452,19 +1889,18 @@ impl Render for List { } } -/// Build one multi-contour `Path` (non-zero fill rule, so holes like the inside of an "O" work +/// 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>(targets: &mut Vec, source: &S, index: usize, geometry: &Vector, transform: DAffine2) { let filled = has_paint::(source, index); - let mut bezpaths: Vec = geometry.stroke_bezpath_iter().filter(|bezpath| !bezpath.elements().is_empty()).collect(); - let all_contours_closed = bezpaths.iter().all(|bezpath| matches!(bezpath.elements().last(), Some(PathEl::ClosePath))); + let mut subpaths: Vec> = 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 = appearance.and_then(|appearance| appearance.first_coverage_of(Cover::Stroke)).map_or(0., |coverage| { - let stroke = coverage.stroke_params(); - if stroke.align.is_not_centered() && all_contours_closed { + 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 @@ -2472,20 +1908,13 @@ fn extend_targets_from_vector>(targets: &mut Vec }); if filled { - for bezpath in &mut bezpaths { - if !matches!(bezpath.elements().last(), Some(PathEl::ClosePath)) { - bezpath.close_path(); - } + for subpath in &mut subpaths { + subpath.set_closed(true); } } - if !bezpaths.is_empty() { - let mut combined_path = BezPath::new(); - for bezpath in bezpaths { - combined_path.extend(bezpath); - } - - let mut click_target = ClickTarget::new_with_path(combined_path, stroke_width); + 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); } @@ -2656,7 +2085,7 @@ fn render_raster_cpu_vello> + BoundingBox>(s fn collect_raster_metadata(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { let Some(element_id) = element_id else { return }; - let subpath = rectangle_bezpath(DVec2::ZERO, DVec2::ONE); + 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); @@ -2673,7 +2102,7 @@ fn collect_raster_metadata(source: &S, metadata: &mut RenderMetad } fn add_raster_upstream_click_targets(click_targets: &mut Vec) { - let subpath = rectangle_bezpath(DVec2::ZERO, DVec2::ONE); + let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); } @@ -2767,53 +2196,10 @@ impl Render for List> { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { collect_raster_metadata(self, metadata, footprint, element_id) } -} fn add_upstream_click_targets(&self, click_targets: &mut Vec) { add_raster_upstream_click_targets(click_targets) } - - let transform_attribute: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - - 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(); - } - - return; - } - - let width = raster.data().width(); - let height = raster.data().height(); - - let resource_override_index = context.resource_overrides.len(); - // Stable across frames so vello reuses the atlas slot; high bit avoids Blob::new counter ids. - let blob_id = (resource_override_index as u64) << 40 | (width as u64) << 20 | height as u64 | 1 << 63; - let blob = peniko::Blob::from_raw_parts(LAZY_ARC_VEC_ZERO_U8.deref().clone(), blob_id); - let image = peniko::ImageBrush::new(peniko::ImageData { - data: blob, - format: peniko::ImageFormat::Rgba8, - width, - height, - alpha_type: peniko::ImageAlphaType::Alpha, - }) - .with_extend(peniko::Extend::Pad); - 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 { - 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) {} } // Since colors and gradients are technically infinitely big, we have to implement @@ -2896,7 +2282,7 @@ impl Render for List { } } -fn render_gradient_svg>(source: &S, render: &mut SvgRender, render_params: &RenderParams) { +fn render_gradient_svg>(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 { @@ -2913,8 +2299,8 @@ fn render_gradient_svg>(source: &S, render: &m let blend_mode: BlendMode = source.attr::(index); let opacity_attr: f64 = source.attr::(index); let opacity_fill_attr: f64 = source.attr::(index); - let spread_method: GradientSpread = source.attr::(index); - let gradient_type: GradientForm = source.attr::(index); + let spread_method: GradientSpreadMethod = source.attr::(index); + let gradient_type: GradientType = source.attr::(index); let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" }; render.leaf_tag(tag, |attributes| { if let Some((min, size)) = thumbnail_rect { @@ -2952,7 +2338,7 @@ fn render_gradient_svg>(source: &S, render: &m }; let gradient_id = generate_uuid(); - let spread_method_attribute = if spread_method == GradientSpread::Pad { + let spread_method_attribute = if spread_method == GradientSpreadMethod::Pad { String::new() } else { format!(r#" spreadMethod="{}""#, spread_method.svg_name()) @@ -2960,13 +2346,13 @@ fn render_gradient_svg>(source: &S, render: &m // The unit gradient line is the +X unit vector in local space, before the item's transform is applied match gradient_type { - GradientForm::Linear => { + GradientType::Linear => { let _ = write!( &mut attributes.0.svg_defs, r#"{stop_string}"# ); } - GradientForm::Radial => { + GradientType::Radial => { let _ = write!( &mut attributes.0.svg_defs, r#"{stop_string}"# @@ -2988,7 +2374,7 @@ fn render_gradient_svg>(source: &S, render: &m } } -fn render_gradient_vello>(source: &S, scene: &mut Scene, parent_transform: DAffine2, render_params: &RenderParams) { +fn render_gradient_vello>(source: &S, scene: &mut Scene, parent_transform: DAffine2, render_params: &RenderParams) { use vello::peniko; if let RenderMode::Outline = render_params.render_mode { @@ -2997,8 +2383,8 @@ fn render_gradient_vello>(source: &S, scene: & for index in 0..source.lane_count() { let Some(gradient) = source.element(index) else { continue }; - let spread_method: GradientSpread = source.attr::(index); - let gradient_type: GradientForm = source.attr::(index); + let spread_method: GradientSpreadMethod = source.attr::(index); + let gradient_type: GradientType = source.attr::(index); let transform: DAffine2 = source.attr::(index); let blend_mode_attr: BlendMode = source.attr::(index); let opacity_attr: f64 = source.attr::(index); @@ -3017,20 +2403,20 @@ fn render_gradient_vello>(source: &S, scene: & } let extend = match spread_method { - GradientSpread::Pad => peniko::Extend::Pad, - GradientSpread::Reflect => peniko::Extend::Reflect, - GradientSpread::Repeat => peniko::Extend::Repeat, + 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 { - GradientForm::Linear => peniko::LinearGradientPosition { + GradientType::Linear => peniko::LinearGradientPosition { start: to_point(DVec2::ZERO), end: to_point(DVec2::X), } .into(), - GradientForm::Radial => peniko::RadialGradientPosition { + GradientType::Radial => peniko::RadialGradientPosition { start_center: to_point(DVec2::ZERO), start_radius: 0., end_center: to_point(DVec2::ZERO), @@ -3072,7 +2458,7 @@ fn render_gradient_vello>(source: &S, scene: & } } -impl Render for List { +impl Render for List { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { render_gradient_svg(self, render, render_params) } @@ -3080,240 +2466,6 @@ impl Render for List { 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) } - - fn collect_metadata(&self, metadata: &mut RenderMetadata, _footprint: Footprint, element_id: Option, _inherited_appearance: Option<&Appearance>) { - collect_gradient_items_metadata((0..self.len()).map(|index| ItemRef::ListItem(self, index)), metadata, element_id); - } - - fn add_upstream_click_targets(&self, click_targets: &mut Vec, _inherited_appearance: Option<&Appearance>) { - for index in 0..self.len() { - add_gradient_item_click_targets(ItemRef::ListItem(self, index), click_targets); - } - } - - fn add_upstream_outline_targets(&self, outlines: &mut Vec, _inherited_appearance: Option<&Appearance>) { - for index in 0..self.len() { - add_gradient_item_outline_targets(ItemRef::ListItem(self, index), outlines); - } - } -} - -/// Emits one item of gradient content as SVG, painting the thumbnail rect or an infinite-background stand-in. -fn render_gradient_item_svg_with_thumbnail_rect(item: ItemRef<'_, Gradient>, thumbnail_rect: Option<(DVec2, DVec2)>, render: &mut SvgRender, render_params: &RenderParams) { - let Some(gradient) = item.element() else { return }; - let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - let blend_mode: BlendMode = item.attribute_cloned_or_default(ATTR_BLEND_MODE); - let opacity_attr: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.); - let opacity_fill_attr: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); - let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); - let settings = gradient_settings_from_item(item); - 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 (samples, _) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::SvgStopOrder); - - let mut stop_string = String::new(); - for (position, color, original_midpoint) in samples { - let _ = write!(stop_string, r##""); - } - - // 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 gradient_spread_attribute = if matches!(settings.spread, GradientSpread::Pad | GradientSpread::Clear) { - String::new() - } else { - format!(r#" spreadMethod="{}""#, settings.spread.svg_name()) - }; - - // The unit gradient line is the +X unit vector in local space, before the item's transform is applied - match gradient_form { - GradientForm::Linear => { - let _ = write!( - &mut attributes.0.svg_defs, - r#"{stop_string}"# - ); - } - GradientForm::Radial => { - let _ = write!( - &mut attributes.0.svg_defs, - r#"{stop_string}"# - ); - } - } - - 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()); - } - }); -} - -/// Draws one item of gradient content into the Vello scene under the viewport-replaced infinite transform. -fn render_gradient_item_to_vello(item: ItemRef<'_, Gradient>, scene: &mut Scene, parent_transform: DAffine2, render_params: &RenderParams) { - use vello::peniko; - - if let RenderMode::Outline = render_params.render_mode { - return; - } - - { - let Some(gradient) = item.element() else { return }; - let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); - let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - let blend_mode_attr: BlendMode = item.attribute_cloned_or_default(ATTR_BLEND_MODE); - let opacity_attr: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.); - let opacity_fill_attr: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); - 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 settings = gradient_settings_from_item(item); - let (samples, span) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::VelloRampTexels); - - let stops = peniko_color_stops(&samples); - - let extend = peniko_extend(settings.spread); - - // 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_form { - GradientForm::Linear => peniko::LinearGradientPosition { - start: to_point(DVec2::X * span.0), - end: to_point(DVec2::X * span.1), - } - .into(), - GradientForm::Radial => peniko::RadialGradientPosition { - start_center: to_point(DVec2::ZERO), - start_radius: 0., - end_center: to_point(DVec2::ZERO), - end_radius: span.1 as f32, - } - .into(), - }; - - let fill = peniko::Brush::Gradient(peniko::Gradient { - kind, - stops, - extend, - interpolation_alpha_space: peniko::InterpolationAlphaSpace::Unpremultiplied, - ..Default::default() - }); - let brush_transform = kurbo::Affine::new(gradient_placement(gradient_transform, gradient_form).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` 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(); - } - } -} - -/// The metadata pass over a run of gradient items: each contributes its control geometry as targets under the -/// run's `element_id`, baked relative to the first item's transform (recorded as its `local_transforms` entry). -fn collect_gradient_items_metadata<'a>(items: impl Iterator>, metadata: &mut RenderMetadata, element_id: Option) { - let Some(element_id) = element_id else { return }; - - let mut item_zero_inverse = None; - let mut outline_targets = Vec::new(); - let mut click_targets = Vec::new(); - for item in items { - let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); - let item_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - - // The first item's transform is the reference all targets bake against - let item_zero_inverse = *item_zero_inverse.get_or_insert_with(|| if transform_is_invertible(item_transform) { item_transform.inverse() } else { DAffine2::IDENTITY }); - - let mut target = ClickTarget::new_with_path(gradient_control_outline(gradient_form), 0.); - target.apply_transform(item_zero_inverse * item_transform); - let target = Arc::new(target); - - if gradient_control_interior_is_clickable(gradient_form) { - click_targets.push(target.clone()); - } - outline_targets.push(target); - } - - if outline_targets.is_empty() { - return; - } - - metadata.outlines.insert(element_id, outline_targets); - if !click_targets.is_empty() { - metadata.click_targets.insert(element_id, click_targets); - } -} - -/// Collects one gradient item's control geometry as a click target when its interior is draggable. -fn add_gradient_item_click_targets(item: ItemRef<'_, Gradient>, click_targets: &mut Vec) { - let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); - if !gradient_control_interior_is_clickable(gradient_form) { - return; - } - - let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - let mut target = ClickTarget::new_with_path(gradient_control_outline(gradient_form), 0.); - target.apply_transform(transform); - click_targets.push(target); -} - -/// Collects one gradient item's control geometry as an outline target. -fn add_gradient_item_outline_targets(item: ItemRef<'_, Gradient>, outlines: &mut Vec) { - let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); - let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - - let mut target = ClickTarget::new_with_path(gradient_control_outline(gradient_form), 0.); - target.apply_transform(transform); - outlines.push(target); } /// Builds a `kurbo::BezPath` from a glyph outline, baking in the glyph origin (`ox`, `oy`) and faux-italic shear (`tilt_tan`). @@ -3447,20 +2599,7 @@ pub fn text_list_bounding_box>(source: &S, outer } } -/// Folds one laid-out text item's corner points into the running bounds. -fn accumulate_text_item_bounds(item: ItemRef<'_, String>, outer_transform: DAffine2, bounds: &mut Option<[DVec2; 2]>) { - let Some((size, transform)) = text_item_size_and_transform(item) else { return }; - 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], - }); - } -} - -/// Like `List::thumbnail_bounding_box`, but lays out `Graphic::TextList` items, which the `BoundingBox` trait reports as `None`. +/// Like `List::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` wrapping the text) frame their content. pub fn graphic_list_bounding_box<'e, S: LaneSource>>(source: &S, transform: DAffine2) -> RenderBoundingBox { let mut combined: Option<[DVec2; 2]> = None; @@ -3471,7 +2610,7 @@ pub fn graphic_list_bounding_box<'e, S: LaneSource>>(sourc let Some(graphic) = source.element(index) else { continue }; let bounds = match graphic { Graphic::Text(text) => text_list_bounding_box(&Single(text), item_transform), - Graphic::GraphicList(sub_list) => graphic_list_bounding_box(sub_list, item_transform), + Graphic::Graphic(sub_list) => graphic_list_bounding_box(sub_list, item_transform), other => other.thumbnail_bounding_box(item_transform, true), }; match bounds { @@ -3670,7 +2809,7 @@ fn collect_text_metadata>(source: &S, metadata: } let Some((size, item_transform)) = text_item_size_and_transform(source, index) else { continue }; - let subpath = rectangle_bezpath(DVec2::ZERO, size); + 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)); @@ -3686,7 +2825,7 @@ fn collect_text_metadata>(source: &S, metadata: fn add_text_upstream_click_targets>(source: &S, click_targets: &mut Vec) { for index in 0..source.lane_count() { let Some((size, transform)) = text_item_size_and_transform(source, index) else { continue }; - let subpath = rectangle_bezpath(DVec2::ZERO, size); + 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); @@ -3787,7 +2926,7 @@ impl Render for RunView<'_, Color> { } } -impl Render for RunView<'_, Gradient> { +impl Render for RunView<'_, GradientStops> { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { render_gradient_svg(self, render, render_params) } @@ -3898,7 +3037,7 @@ mod group_walk_tests { use graphic_types::vector_types::vector::PointId; fn unit_square_at(corner: DVec2) -> Vector { - Vector::from_bezpath(rectangle_bezpath(corner, corner + DVec2::ONE)) + Vector::from_subpath(Subpath::::new_rectangle(corner, corner + DVec2::ONE)) } fn color_paint() -> List> { @@ -3944,7 +3083,7 @@ mod group_walk_tests { let params = RenderParams::default(); let native = rendered_svg(|render| Graphic::Group(group.clone()).render_svg(render, ¶ms)); - let legacy = rendered_svg(|render| Graphic::GraphicList(graphic_types::graphic::group_to_legacy_list(&group)).render_svg(render, ¶ms)); + let legacy = rendered_svg(|render| Graphic::Graphic(graphic_types::graphic::group_to_legacy_list(&group)).render_svg(render, ¶ms)); assert!(native.0.contains(r##"fill="#"##), "the lane's fill paint must reach the vector interior: {}", native.0); assert_eq!(native, legacy);