diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 259583676a..699a996570 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -820,14 +820,14 @@ impl TaggedValue { /// /// Routes legacy variant names into modern variants, in typed Rust. Each legacy name is also matched against the historical `#[serde(alias = "...")]` spellings the deleted variant accepted, so old-shape inner payloads are caught: /// -/// - `Graphic` (or alias `GraphicGroup`/`Group`) → `TaggedValue::TypeDefault(list!(Graphic))` -/// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(list!(Artboard))` +/// - `Graphic` (or alias `GraphicGroup`/`Group`) → `TaggedValue::TypeDefault(concrete!(List))` +/// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(concrete!(List))` /// - `Raster` (or alias `ImageFrame`/`RasterData`/`Image`): /// - non-empty (the legacy `image` proto's input 1, where the inner `Raster` serializes as the embedded `Image`) → `TaggedValue::ImageData(>)` -/// - empty → `TaggedValue::TypeDefault(list!(Raster))` +/// - empty → `TaggedValue::TypeDefault(concrete!(List>))` /// - `Vector` (or alias `VectorData`): /// - non-empty → `TaggedValue::VectorModification()` (the document_migration's Path pass disambiguates this between SVG-import legacy and a discardable modern baked value via the input's `exposed` flag) -/// - empty → `TaggedValue::TypeDefault(list!(Vector))` +/// - empty → `TaggedValue::TypeDefault(concrete!(List))` /// - `FillChoice` → `TaggedValue::Color` (solid), `TaggedValue::GradientRamp` (gradient), or `TaggedValue::no_paint()` (none) /// - `Gradient` (or alias `GradientTable`/`GradientPositions`/`Gradient`) → `TaggedValue::LegacyGradient` (ancient full struct) or `TaggedValue::GradientRamp` (ramp and legacy stops shapes, unwrapped from the legacy table form) /// - `TypeDefault` with the old bare-`TypeDescriptor` payload → the same variant wrapping a `Type` (name-encoded `List` normalized to structural) @@ -844,8 +844,8 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize && let Some((tag, content)) = map.iter().next() { match tag.as_str() { - "Graphic" | "GraphicGroup" | "Group" => return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Graphic)))), - "Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Artboard)))), + "Graphic" | "GraphicGroup" | "Group" => return Ok(MemoHash::new(TaggedValue::TypeDefault(concrete!(List)))), + "Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(concrete!(List)))), "Raster" | "ImageFrame" | "RasterData" | "Image" => { let first_element = content .as_object() @@ -856,7 +856,7 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize let image: Image = serde_json::from_value(image_value.clone()).map_err(serde::de::Error::custom)?; return Ok(MemoHash::new(TaggedValue::ImageData(image))); } - return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Raster)))); + return Ok(MemoHash::new(TaggedValue::TypeDefault(concrete!(List>)))); } "Vector" | "VectorData" => { let vector = graphic_types::migrations::migrate_to_optional_vector(content.clone()).map_err(serde::de::Error::custom)?; @@ -864,7 +864,7 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize let modification = Box::new(VectorModification::create_from_vector(&vector)); return Ok(MemoHash::new(TaggedValue::VectorModification(modification))); } - return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Vector)))); + return Ok(MemoHash::new(TaggedValue::TypeDefault(concrete!(List)))); } // The `TypeDefault` payload used to be a bare `TypeDescriptor`; it now carries a `Type` "TypeDefault" if content.as_object().is_some_and(|c| c.contains_key("name")) => { @@ -1038,12 +1038,12 @@ mod typedefault_dispatch { } macro_rules! check_item { ($element:ty) => { - check!(Item<$element>, item!($element)); + check!(Item<$element>, concrete!($element)); }; } macro_rules! check_list { ($element:ty) => { - check!(List<$element>, list!($element)); + check!(List<$element>, concrete!(List<$element>)); }; } macro_rules! check_bare { @@ -1068,7 +1068,7 @@ mod paint_default_parsing { fn paint_wire_parses_color_default_through_its_element() { let black = Some(TaggedValue::Color(Color::BLACK)); assert_eq!( - TaggedValue::from_primitive_string("Color::BLACK", &list!(Graphic)), + TaggedValue::from_primitive_string("Color::BLACK", &concrete!(List)), black, "a `List` paint wire should resolve its color default" ); diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index 74da9ddcb3..7e50267f4b 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -1,4 +1,4 @@ -use crate::renderer::{RenderParams, format_transform_matrix, gradient_placement, transform_is_invertible}; +use crate::renderer::{ClearGuardPlacement, RenderParams, format_transform_matrix, gradient_placement, gradient_settings_from_lane, spread_adjusted_samples, transform_is_invertible}; use crate::{Render, RenderSvgSegmentList, SvgRender}; use core_types::Color; use core_types::attribute::Transform; @@ -7,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::GradientType; -use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod}; +use graphic_types::vector_types::gradient::GradientForm; +use graphic_types::vector_types::markers::GradientForm as GradientFormAttr; use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use std::fmt::Write; -use vector_types::GradientStops; -use vector_types::gradient::GradientSpreadMethod; +use vector_types::Gradient; +use vector_types::gradient::GradientSpread; #[derive(Copy, Clone, PartialEq)] pub enum PaintTarget { @@ -83,7 +83,7 @@ impl RenderExt for List { } } -impl RenderExt for List { +impl RenderExt for List { type Output = u64; /// Adds the gradient def through mutating the first argument, returning the gradient ID. @@ -103,135 +103,130 @@ 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: GradientType = source.attr::(0); - let local_gradient_transform: DAffine2 = source.attr::(0); - let spread_method: GradientSpreadMethod = source.attr::(0); + let Some(stops) = source.element(0) else { return 0 }; + let gradient_form: GradientForm = source.attr::(0); + let local_gradient_transform: DAffine2 = source.attr::(0); + let settings = gradient_settings_from_lane(source, 0); - for (position, color, original_midpoint) in stops.interpolated_samples() { - stop.push_str("") + let (samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder); + + for (position, color, original_midpoint) in samples { + stop.push_str(" { - let _ = write!( - svg_defs, - r#"{}"#, - gradient_id, stop - ); - } - GradientType::Radial => { - let _ = write!( - svg_defs, - r#"{}"#, - gradient_id, stop - ); - } + let _ = write!(stop, r##" stop-color="#{}""##, SRGBA8::from(color).to_rgb_hex()); + if color.a() < 1. { + let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } - - gradient_id + if let Some(midpoint) = original_midpoint { + let _ = write!(stop, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.); + } + 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}""#) + }; + + // `Clear` rides pad, with the transparent guard stops from `spread_adjusted_samples` doing the clearing + 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 + ); + } + } + + gradient_id } -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 `List.render` with `PaintTarget::Stroke`. - fn render( - &self, - _svg_defs: &mut String, - _item_transform: DAffine2, - _element_transform: DAffine2, - _stroke_transform: DAffine2, - _bounds: DAffine2, - render_params: &RenderParams, - _target: PaintTarget, - ) -> Self::Output { - // Don't render a stroke at all if it would be invisible - if !self.has_renderable_stroke() { - return String::new(); - } - - let default_weight = if self.align != StrokeAlign::Center && render_params.aligned_strokes { 1. / 2. } else { 1. }; - - // Set to None if the value is the SVG default - let weight = (self.weight != default_weight).then_some(self.weight); - let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths()); - let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset); - let stroke_cap = (self.cap != StrokeCap::Butt).then_some(self.cap); - 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(); - if let Some(mut weight) = weight { - if stroke_align.is_some() && render_params.aligned_strokes { - weight *= 2.; - } - let _ = write!(&mut attributes, r#" stroke-width="{weight}""#); - } - if let Some(dash_array) = dash_array { - let _ = write!(&mut attributes, r#" stroke-dasharray="{dash_array}""#); - } - if let Some(dash_offset) = dash_offset { - let _ = write!(&mut attributes, r#" stroke-dashoffset="{dash_offset}""#); - } - if let Some(stroke_cap) = stroke_cap { - let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, stroke_cap.svg_name()); - } - if let Some(stroke_join) = stroke_join { - let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, stroke_join.svg_name()); - } - if let Some(stroke_join_miter_limit) = stroke_join_miter_limit { - let _ = write!(&mut attributes, r#" stroke-miterlimit="{stroke_join_miter_limit}""#); - } - if paint_order.is_some() { - let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#); - } - attributes +/// Provide the shape-related SVG attributes for the stroke. The paint-related attributes for the stroke are generated from `List.render` with `PaintTarget::Stroke`. +/// +/// `paint_order` arrives separately because master deleted `Stroke::paint_order`; it rides the stroke paint list instead. +pub fn render_stroke_shape(stroke: &Stroke, paint_order: PaintOrder, render_params: &RenderParams) -> String { + // Don't render a stroke at all if it would be invisible + if !stroke.has_renderable_stroke() { + return String::new(); } + + let default_weight = if stroke.align != StrokeAlign::Center && render_params.aligned_strokes { 1. / 2. } else { 1. }; + + // Set to None if the value is the SVG default + let weight = (stroke.weight != default_weight).then_some(stroke.weight); + let dash_array = (!stroke.dash_lengths.is_empty()).then_some(stroke.dash_lengths()); + let dash_offset = (stroke.dash_offset != 0.).then_some(stroke.dash_offset); + let stroke_cap = (stroke.cap != StrokeCap::Butt).then_some(stroke.cap); + let stroke_join = (stroke.join != StrokeJoin::Miter).then_some(stroke.join); + let stroke_join_miter_limit = (stroke.join_miter_limit != 4.).then_some(stroke.join_miter_limit); + let stroke_align = (stroke.align != StrokeAlign::Center).then_some(stroke.align); + let paint_order = (paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow); + + // Render the needed stroke attributes + let mut attributes = String::new(); + if let Some(mut weight) = weight { + if stroke_align.is_some() && render_params.aligned_strokes { + weight *= 2.; + } + let _ = write!(&mut attributes, r#" stroke-width="{weight}""#); + } + if let Some(dash_array) = dash_array { + let _ = write!(&mut attributes, r#" stroke-dasharray="{dash_array}""#); + } + if let Some(dash_offset) = dash_offset { + let _ = write!(&mut attributes, r#" stroke-dashoffset="{dash_offset}""#); + } + if let Some(stroke_cap) = stroke_cap { + let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, stroke_cap.svg_name()); + } + if let Some(stroke_join) = stroke_join { + let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, stroke_join.svg_name()); + } + if let Some(stroke_join_miter_limit) = stroke_join_miter_limit { + let _ = write!(&mut attributes, r#" stroke-miterlimit="{stroke_join_miter_limit}""#); + } + if paint_order.is_some() { + let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#); + } + attributes } impl RenderExt for List> { @@ -256,7 +251,9 @@ 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::Graphic(_)) | Some(Graphic::Text(_)) | Some(Graphic::Group(_)) => { + // Brush strokes have no vector outline, so they contribute no paint + Some(Graphic::Stroke(_)) | Some(Graphic::StrokeList(_)) => format!(r#" {paint_attr}="none""#), + Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::GraphicList(_)) | 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. }; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 7a04057534..e7bf95941d 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -12,7 +12,7 @@ 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::list::{ATTR_ALIGN, ATTR_CAP, ATTR_DASH_OFFSET, ATTR_DASH_PATTERN, ATTR_JOIN, ATTR_JOIN_MITER_LIMIT, ATTR_WEIGHT, Item, List}; use core_types::math::quad::Quad; use core_types::record::{Group, RunView}; use core_types::render_complexity::RenderComplexity; @@ -26,13 +26,16 @@ 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::{GradientStops, GradientType}; -use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod}; -use graphic_types::vector_types::subpath::Subpath; +use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientSettings}; +use graphic_types::vector_types::markers::{ + GradientCyclic, GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientInterpolation as GradientInterpolationAttr, GradientSpace as GradientSpaceAttr, + GradientSpread as GradientSpreadAttr, +}; +use graphic_types::vector_types::vector::algorithms::shapes::rectangle_bezpath; 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::vector_types::vector::style::{DashPattern, PaintOrder, RenderMode, Stroke as StrokeStyle, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::{ATTR_FILL, Artboard, Graphic, Vector}; -use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts}; +use kurbo::{Affine, BezPath, Cap, Join, PathEl, Shape, StrokeOpts}; use num_traits::Zero; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; @@ -44,7 +47,7 @@ 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::gradient::GradientSpread; use vector_types::markers::EditorClickTarget; use vello::*; @@ -340,11 +343,9 @@ 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 = Subpath::::new_rectangle(DVec2::ZERO, DVec2::ONE).to_bezpath(); + let mut outline_path = rectangle_bezpath(DVec2::ZERO, DVec2::ONE); outline_path.apply_affine(Affine::new(outline_transform.to_cols_array())); scene.stroke(&outline_stroke, Affine::IDENTITY, outline_color_peniko, None, &outline_path); @@ -381,14 +382,49 @@ pub(crate) fn transform_is_invertible(transform: DAffine2) -> bool { transform.matrix2.determinant().recip().is_finite() } +/// Paint order has no census name upstream, since master expresses it as `Appearance` list order instead. +const ATTR_PAINT_ORDER: &str = "paint_order"; + +/// `Vector::stroke` is gone upstream. In our paint model a lane's stroke is the `ATTR_STROKE` +/// `List`, so the stroke's GEOMETRY parameters ride that list's own attribute columns, on the +/// same names master's `Coverage` uses. Mirrors `vector_nodes::stroke_params`, which writes them. +fn stroke_params(paint: &List) -> StrokeStyle { + let defaults = StrokeStyle::default(); + StrokeStyle { + weight: paint.attribute_cloned_or(ATTR_WEIGHT, 0, defaults.weight), + dash_lengths: paint.attribute::(ATTR_DASH_PATTERN, 0).map(DashPattern::clamped_lengths).unwrap_or_default(), + dash_offset: paint.attribute_cloned_or(ATTR_DASH_OFFSET, 0, defaults.dash_offset), + cap: paint.attribute_cloned_or(ATTR_CAP, 0, defaults.cap), + join: paint.attribute_cloned_or(ATTR_JOIN, 0, defaults.join), + join_miter_limit: paint.attribute_cloned_or(ATTR_JOIN_MITER_LIMIT, 0, defaults.join_miter_limit), + align: paint.attribute_cloned_or(ATTR_ALIGN, 0, defaults.align), + transform: paint.attribute_cloned_or(ATTR_TRANSFORM, 0, defaults.transform), + } +} + +/// The stroke geometry a lane's `ATTR_STROKE` paint list carries, absent when the lane has no stroke paint. +fn lane_stroke>(source: &S, index: usize) -> Option { + paint_graphics::(source, index).map(stroke_params) +} + +/// Whether the stroke paints below the fill, off the same paint list that carries the stroke geometry. +fn stroke_paint_order(paint: Option<&List>) -> PaintOrder { + paint.map(|paint| paint.attribute_cloned_or_default(ATTR_PAINT_ORDER, 0)).unwrap_or_default() +} + +/// Whether every contour of the path is explicitly closed, which gates the stroke-alignment compositing trick. +fn all_contours_closed(vector: &Vector) -> bool { + vector.stroke_bezpath_iter().all(|path| matches!(path.elements().last(), Some(PathEl::ClosePath))) +} + /// 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 { +pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientForm) -> DAffine2 { match gradient_type { - GradientType::Radial => transform, - GradientType::Linear => { + GradientForm::Radial => transform, + GradientForm::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 }; @@ -400,32 +436,141 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientTyp } } -fn create_peniko_gradient_brush>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { - let stops = gradient_list.element(0)?; +/// The whole-ramp settings a gradient lane carries beside its element, defaulting each absent one. +pub(crate) fn gradient_settings_from_lane>(source: &S, index: usize) -> GradientSettings { + GradientSettings { + spread: source.attr::(index), + cyclic: source.attr::(index), + space: source.attr::(index), + hue_direction: source.attr::(index), + interpolation: source.attr::(index), + } +} - let gradient_type: GradientType = gradient_list.attr::(0); - let gradient_transform: DAffine2 = gradient_list.attr::(0); - let spread_method: GradientSpreadMethod = gradient_list.attr::(0); +/// Texel count of the baked gradient ramp Vello samples stops through (`N_SAMPLES`/`GRADIENT_WIDTH` in vello_encoding). +const VELLO_GRADIENT_RAMP_TEXELS: f64 = 512.; +/// Renderable gradient samples of `(position, color, original midpoint)`, as produced by [`Gradient::interpolated_samples`]. +type GradientSamples = Vec<(f64, Color, Option)>; + +/// 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 stops.interpolated_samples() { + + 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() { peniko_stops.push(peniko::ColorStop { - offset: position as f32, - color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color()), + offset: 0., + color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(Color::BLACK).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)); + 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, + } +} + +fn create_peniko_gradient_brush>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { + let stops = gradient_list.element(0)?; + + let gradient_form: GradientForm = gradient_list.attr::(0); + let gradient_transform: DAffine2 = gradient_list.attr::(0); + let settings = gradient_settings_from_lane(gradient_list, 0); + + let (samples, span) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::VelloRampTexels); + 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, + // with the span widening the geometry to hold the `Clear` guards outside the visible range + let (start, end, gradient_to_device) = (DVec2::X * span.0, DVec2::X * span.1, gradient_placement(multiplied_transform * gradient_transform, gradient_form)); let brush = peniko::Brush::Gradient(peniko::Gradient { - kind: match gradient_type { - GradientType::Linear => peniko::LinearGradientPosition { + kind: match gradient_form { + GradientForm::Linear => peniko::LinearGradientPosition { start: to_point(start), end: to_point(end), } .into(), - GradientType::Radial => peniko::RadialGradientPosition { + GradientForm::Radial => peniko::RadialGradientPosition { start_center: to_point(start), start_radius: 0., end_center: to_point(start), @@ -433,13 +578,10 @@ fn create_peniko_gradient_brush>(gradient } .into(), }, - extend: match spread_method { - GradientSpreadMethod::Pad => peniko::Extend::Pad, - GradientSpreadMethod::Reflect => peniko::Extend::Reflect, - GradientSpreadMethod::Repeat => peniko::Extend::Repeat, - }, + extend: peniko_extend(settings.spread), stops: peniko_stops, - interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied, + // Straight alpha, keeping parity with the SVG renderer's stop interpolation + interpolation_alpha_space: peniko::InterpolationAlphaSpace::Unpremultiplied, ..Default::default() }); @@ -552,26 +694,29 @@ pub trait Render: BoundingBox + RenderComplexity { 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::GraphicList(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), + // Brush strokes have no vector outline, so they are inert in rendering + Graphic::Stroke(_) | Graphic::StrokeList(_) => (), 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::GraphicList(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::Stroke(_) | Graphic::StrokeList(_) => (), Graphic::Group(group) => render_group_vello(group, PaintReach::NONE, scene, transform, context, render_params), } } @@ -590,14 +735,14 @@ impl Render for Graphic<'_> { fn contains_artboard(&self) -> bool { match self { - Graphic::Graphic(list) => list.contains_artboard(), + Graphic::GraphicList(list) => list.contains_artboard(), _ => false, } } fn new_ids_from_hash(&mut self, reference: Option) { match self { - Graphic::Graphic(list) => list.new_ids_from_hash(reference), + Graphic::GraphicList(list) => list.new_ids_from_hash(reference), Graphic::Vector(vector) => vector.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()), _ => (), } @@ -607,7 +752,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::Graphic(inner) => render_graphic_svg_with(inner, reach.nested(), render, render_params), + Graphic::GraphicList(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), } @@ -616,7 +761,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::Graphic(inner) => render_graphic_vello_with(inner, reach.nested(), scene, transform, context, render_params), + Graphic::GraphicList(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), } @@ -647,7 +792,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::Graphic(_) => {} + Graphic::GraphicList(_) => {} // A leaf's layer identity and transform ride its containing lane. Graphic::Vector(_) => { metadata.first_element_source_id.insert(element_id, lane_source); @@ -660,13 +805,14 @@ fn collect_element_metadata<'a>( } match element { - Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id), + Graphic::GraphicList(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::Stroke(_) | Graphic::StrokeList(_) => {} Graphic::Text(text) => collect_text_metadata(&Single(text), metadata, footprint, element_id), Graphic::Group(group) => collect_group_metadata(group, reach, metadata, footprint, element_id), } @@ -694,7 +840,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); @@ -703,11 +849,11 @@ 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::Graphic(list) => add_graphic_upstream_click_targets_with(list, reach.nested(), click_targets), + Graphic::GraphicList(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::Color(_) | Graphic::Gradient(_) | Graphic::Stroke(_) | Graphic::StrokeList(_) => {} Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), click_targets), Graphic::Group(group) => add_group_upstream_click_targets(group, reach, click_targets), } @@ -715,11 +861,11 @@ 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::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach.nested(), outlines), + Graphic::GraphicList(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::Color(_) | Graphic::Gradient(_) | Graphic::Stroke(_) | Graphic::StrokeList(_) => {} Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), outlines), Graphic::Group(group) => add_group_upstream_outline_targets(group, reach, outlines), } @@ -741,7 +887,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) @@ -763,7 +909,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) @@ -786,7 +932,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) } @@ -930,8 +1076,8 @@ fn collect_artboard_metadata<'a, S: LaneSource>>(source: 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()]); + let subpath = rectangle_bezpath(DVec2::ZERO, dimensions); + metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_path(subpath, 0.).into()]); metadata.upstream_footprints.insert(element_id, footprint); metadata.local_transforms.insert(element_id, DAffine2::from_translation(location)); if clip { @@ -950,8 +1096,8 @@ 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 = Subpath::new_rectangle(DVec2::ZERO, dimensions); - click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); + let subpath_rectangle = rectangle_bezpath(DVec2::ZERO, dimensions); + click_targets.push(ClickTarget::new_with_path(subpath_rectangle, 0.)); } } @@ -1280,8 +1426,15 @@ fn render_vector_svg>(source: &S, render: &mut S let opacity_attr: f64 = source.attr::(index); let opacity_fill_attr: f64 = source.attr::(index); + let fill_graphic_list = paint_graphics::(source, index); + let fill_graphic = fill_graphic_list.and_then(|l| l.element(0)); + + let stroke_graphic_list = paint_graphics::(source, index); + let stroke_graphic = stroke_graphic_list.and_then(|l| l.element(0)); + let stroke_geometry = stroke_graphic_list.map(stroke_params); + // 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 has_real_stroke = stroke_geometry.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); @@ -1289,7 +1442,9 @@ fn render_vector_svg>(source: &S, render: &mut S 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 stroke_layer_bounds = vector + .stroke_inclusive_bounding_box_with_transform(DAffine2::IDENTITY, stroke_geometry.as_ref()) + .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]); @@ -1301,26 +1456,20 @@ fn render_vector_svg>(source: &S, render: &mut S path.push_str(bezpath.to_svg().as_str()); } - let mask_type = if vector.stroke.as_ref().map(|x| x.align) == Some(StrokeAlign::Inside) { + let mask_type = if stroke_geometry.as_ref().map(|x| x.align) == Some(StrokeAlign::Inside) { MaskType::Clip } else { MaskType::Mask }; - let fill_graphic_list = paint_graphics::(source, index); - let fill_graphic = fill_graphic_list.and_then(|l| l.element(0)); - - let stroke_graphic_list = paint_graphics::(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 path_is_closed = all_contours_closed(vector); 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); + && stroke_geometry.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) + && stroke_graphic.is_some_and(|graphic| !graphic.is_guaranteed_fully_transparent()); + let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.is_guaranteed_to_cover_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 wants_stroke_below = stroke_paint_order(stroke_graphic_list) == PaintOrder::StrokeBelow; let override_paint_order = can_draw_aligned_stroke && can_use_paint_order; let use_face_fill = vector.use_face_fill(); @@ -1340,8 +1489,9 @@ fn render_vector_svg>(source: &S, render: &mut S 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 item carries only `ATTR_FILL`, so it draws no stroke; master deleted `Vector::stroke`, + // which used to have to be cleared on the clone + let cloned_vector = vector.clone(); // The mask must draw at full alpha so the SVG ``/`` fully zeroes the path interior. // The wrapping SVG group (above) handles the user-set opacity. @@ -1353,7 +1503,7 @@ fn render_vector_svg>(source: &S, render: &mut S }); if use_face_fill { - for mut face_path in vector.construct_faces().filter(|face| face.area() >= 0.) { + for mut face_path in vector.construct_faces().into_iter().filter(|face| face.area() >= 0.) { face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); let face_d = face_path.to_svg(); @@ -1381,7 +1531,7 @@ fn render_vector_svg>(source: &S, render: &mut S 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(); + let stroke = stroke_geometry.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; @@ -1408,12 +1558,11 @@ fn render_vector_svg>(source: &S, render: &mut S render_params.aligned_strokes = can_draw_aligned_stroke; render_params.override_paint_order = override_paint_order; - let stroke_shape_attribute = vector - .stroke + let stroke_shape_attribute = stroke_geometry .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) + crate::render_ext::render_stroke_shape(stroke, stroke_paint_order(stroke_graphic_list), &render_params) } else { String::new() } @@ -1421,7 +1570,7 @@ fn render_vector_svg>(source: &S, render: &mut S .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_visible = stroke_geometry.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_guaranteed_fully_transparent()); let stroke_attribute = if stroke_visible { stroke_graphic_list .map(|list| { @@ -1494,7 +1643,8 @@ fn render_vector_vello>(source: &S, scene: &mut let opacity_attr: f64 = source.attr::(index); let opacity_fill_attr: f64 = source.attr::(index); let multiplied_transform = parent_transform * item_transform; - let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); + let stroke_geometry = lane_stroke(source, index); + let has_real_stroke = stroke_geometry.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 @@ -1531,9 +1681,9 @@ fn render_vector_vello>(source: &S, scene: &mut // 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 stroke = stroke_geometry.as_ref(); + let stroke_fully_transparent = stroke_graphic_list.is_none_or(|l| l.element(0).is_none_or(|g| g.is_guaranteed_fully_transparent())); + let can_draw_aligned_stroke = !stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && all_contours_closed(element); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; if opacity < 1. || blend_mode_attr != BlendMode::default() { @@ -1555,7 +1705,7 @@ fn render_vector_vello>(source: &S, scene: &mut } let use_layer = can_draw_aligned_stroke; - let wants_stroke_below = stroke.is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); + let wants_stroke_below = stroke_paint_order(stroke_graphic_list) == 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 }; @@ -1580,7 +1730,9 @@ 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::Graphic(_) | Graphic::Text(_) | Graphic::Group(_) => { + // Brush strokes have no vector outline, so they paint nothing + Graphic::Stroke(_) | Graphic::StrokeList(_) => {} + Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::GraphicList(_) | 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(); @@ -1593,7 +1745,7 @@ fn render_vector_vello>(source: &S, scene: &mut 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.) { + for mut face_path in element.construct_faces().into_iter().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 { @@ -1661,7 +1813,9 @@ 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::Graphic(_) | Graphic::Text(_) | Graphic::Group(_) => { + // Brush strokes have no vector outline, so they paint nothing + Graphic::Stroke(_) | Graphic::StrokeList(_) => {} + Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::GraphicList(_) | 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); @@ -1681,8 +1835,9 @@ fn render_vector_vello>(source: &S, scene: &mut } _ => { if use_layer { - let mut cloned_element = element.clone(); - cloned_element.stroke = None; + // The mask item carries only `ATTR_FILL`, so it draws no stroke; master deleted `Vector::stroke`, + // which used to have to be cleared on the clone + 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. @@ -1735,7 +1890,7 @@ fn render_vector_vello>(source: &S, scene: &mut Stroke, } - let order = match stroke.is_some_and(|stroke| !stroke.paint_order.is_default()) { + let order = match !stroke_paint_order(stroke_graphic_list).is_default() { true => [Op::Stroke, Op::Fill], false => [Op::Fill, Op::Stroke], // Default }; @@ -1889,32 +2044,33 @@ impl Render for List { } } -/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work +/// Build one multi-contour `Path` (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 subpaths: Vec> = geometry.stroke_bezier_paths().collect(); - let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed()); + let mut bezpaths: Vec = geometry.stroke_bezpath_iter().filter(|bezpath| !bezpath.elements().is_empty()).collect(); + let contours_closed = bezpaths.iter().all(|bezpath| matches!(bezpath.elements().last(), Some(PathEl::ClosePath))); // 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 - } - }); + let stroke_width = lane_stroke(source, index).map_or(0., |stroke| if stroke.align.is_not_centered() && contours_closed { stroke.weight * 2. } else { stroke.weight }); if filled { - for subpath in &mut subpaths { - subpath.set_closed(true); + for bezpath in &mut bezpaths { + if !matches!(bezpath.elements().last(), Some(PathEl::ClosePath)) { + bezpath.close_path(); + } } } - if !subpaths.is_empty() { - let mut click_target = ClickTarget::new_with_compound_path(subpaths, stroke_width); + 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); click_target.apply_transform(transform); targets.push(click_target); } @@ -2085,9 +2241,9 @@ 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 = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); + let subpath = rectangle_bezpath(DVec2::ZERO, DVec2::ONE); - metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]); + metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_path(subpath, 0.).into()]); metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one item of the `List>` if source.lane_count() > 0 { @@ -2102,8 +2258,8 @@ fn collect_raster_metadata(source: &S, metadata: &mut RenderMetad } fn add_raster_upstream_click_targets(click_targets: &mut Vec) { - let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); - click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); + let subpath = rectangle_bezpath(DVec2::ZERO, DVec2::ONE); + click_targets.push(ClickTarget::new_with_path(subpath, 0.)); } impl Render for List> { @@ -2282,7 +2438,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 { @@ -2299,8 +2455,8 @@ fn render_gradient_svg>(source: &S, rende 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: GradientSpreadMethod = source.attr::(index); - let gradient_type: GradientType = source.attr::(index); + let settings = gradient_settings_from_lane(source, index); + let gradient_form: GradientForm = 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 { @@ -2317,7 +2473,8 @@ fn render_gradient_svg>(source: &S, rende } let mut stop_string = String::new(); - for (position, color, original_midpoint) in gradient.interpolated_samples() { + let (samples, _) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::SvgStopOrder); + for (position, color, original_midpoint) in samples { let _ = write!(stop_string, r##">(source: &S, rende }; let gradient_id = generate_uuid(); - let spread_method_attribute = if spread_method == GradientSpreadMethod::Pad { + // `Clear` rides pad, with the transparent guard stops from `spread_adjusted_samples` doing the clearing + let spread_method_attribute = if matches!(settings.spread, GradientSpread::Pad | GradientSpread::Clear) { String::new() } else { - format!(r#" spreadMethod="{}""#, spread_method.svg_name()) + 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_type { - GradientType::Linear => { + match gradient_form { + GradientForm::Linear => { let _ = write!( &mut attributes.0.svg_defs, r#"{stop_string}"# ); } - GradientType::Radial => { + GradientForm::Radial => { let _ = write!( &mut attributes.0.svg_defs, r#"{stop_string}"# @@ -2374,7 +2532,7 @@ fn render_gradient_svg>(source: &S, rende } } -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 { @@ -2383,8 +2541,8 @@ fn render_gradient_vello>(source: &S, sce for index in 0..source.lane_count() { let Some(gradient) = source.element(index) else { continue }; - let spread_method: GradientSpreadMethod = source.attr::(index); - let gradient_type: GradientType = source.attr::(index); + let settings = gradient_settings_from_lane(source, index); + let gradient_form: GradientForm = source.attr::(index); let transform: DAffine2 = source.attr::(index); let blend_mode_attr: BlendMode = source.attr::(index); let opacity_attr: f64 = source.attr::(index); @@ -2394,33 +2552,25 @@ fn render_gradient_vello>(source: &S, sce 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 (samples, span) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::VelloRampTexels); + let stops = peniko_color_stops(&samples); + let extend = peniko_extend(settings.spread); - 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), + // The unit gradient line is the +X unit vector in local space, before the item's transform is applied, with the + // span widening it to hold the `Clear` guards outside the visible range. + // For radial, the circle at the origin scales out to the line's length once the brush transform applies. + let (start, end) = (DVec2::X * span.0, DVec2::X * span.1); + let kind = match gradient_form { + GradientForm::Linear => peniko::LinearGradientPosition { + start: to_point(start), + end: to_point(end), } .into(), - GradientType::Radial => peniko::RadialGradientPosition { - start_center: to_point(DVec2::ZERO), + GradientForm::Radial => peniko::RadialGradientPosition { + start_center: to_point(start), start_radius: 0., - end_center: to_point(DVec2::ZERO), - end_radius: 1., + end_center: to_point(start), + end_radius: start.distance(end) as f32, } .into(), }; @@ -2432,7 +2582,7 @@ fn render_gradient_vello>(source: &S, sce interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied, ..Default::default() }); - let brush_transform = kurbo::Affine::new(gradient_placement(gradient_transform, gradient_type).to_cols_array()); + 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; @@ -2458,7 +2608,7 @@ fn render_gradient_vello>(source: &S, sce } } -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) } @@ -2610,7 +2760,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::Graphic(sub_list) => graphic_list_bounding_box(sub_list, item_transform), + Graphic::GraphicList(sub_list) => graphic_list_bounding_box(sub_list, item_transform), other => other.thumbnail_bounding_box(item_transform, true), }; match bounds { @@ -2809,8 +2959,8 @@ fn collect_text_metadata>(source: &S, metadata: } 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.); + let subpath = rectangle_bezpath(DVec2::ZERO, size); + let mut target = ClickTarget::new_with_path(subpath, 0.); target.apply_transform(item_zero_inverse * item_transform); accumulated_click_targets.entry(element_id).or_default().push(Arc::new(target)); } @@ -2825,8 +2975,8 @@ 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 = Subpath::new_rectangle(DVec2::ZERO, size); - let mut target = ClickTarget::new_with_subpath(subpath, 0.); + let subpath = rectangle_bezpath(DVec2::ZERO, size); + let mut target = ClickTarget::new_with_path(subpath, 0.); target.apply_transform(transform); click_targets.push(target); } @@ -2926,7 +3076,7 @@ impl Render for RunView<'_, Color> { } } -impl Render for RunView<'_, GradientStops> { +impl Render for RunView<'_, Gradient> { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { render_gradient_svg(self, render, render_params) } @@ -3034,10 +3184,9 @@ 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::::new_rectangle(corner, corner + DVec2::ONE)) + Vector::from_bezpath(rectangle_bezpath(corner, corner + DVec2::ONE)) } fn color_paint() -> List> { @@ -3083,7 +3232,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::Graphic(graphic_types::graphic::group_to_legacy_list(&group)).render_svg(render, ¶ms)); + let legacy = rendered_svg(|render| Graphic::GraphicList(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); diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 82a375d42f..33616aaff5 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -67,10 +67,7 @@ impl Preprocessor { let resource_id = *hash_to_node_id.entry(hash).or_insert_with(|| { let id = NodeId::new(); let resource_node = DocumentNode { - inputs: vec![ - NodeInput::scope(platform_application_io::editor_api::IDENTIFIER), - NodeInput::value(TaggedValue::ResourceHash(hash), false), - ], + inputs: vec![NodeInput::value(TaggedValue::ResourceHash(hash), false), NodeInput::scope("editor-api")], implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::resource::IDENTIFIER), ..Default::default() }; @@ -156,82 +153,38 @@ impl Preprocessor { .take(wrapper_input_count) .enumerate() .map(|(i, inputs)| { - // A field registering the Item/List wire pair gets a input adapter instead of a typed conversion - if inputs.len() != 1 - && let Some(list_input) = collapse_item_list_pair(inputs) - { - let element_name = match list_input.nested_type() { - Type::List(element) => element.identifier_name(), - nested => nested.identifier_name(), - }; - let input_adapter_identifier = ProtoNodeIdentifier::with_owned_string(format!("input_adapter<{element_name}>")); - - let document_node = if into_node_registry.keys().any(|ident| ident.as_str() == input_adapter_identifier.as_str()) { - generated_nodes += 1; - let mut original_location = OriginalLocation::default(); - original_location.auto_convert_index = Some(i); - DocumentNode { - inputs: vec![NodeInput::import(generic!(X), i)], - implementation: DocumentNodeImplementation::ProtoNode(input_adapter_identifier), - visible: true, - original_location, - ..Default::default() - } - } else { - DocumentNode { - inputs: vec![NodeInput::import(generic!(X), i)], - implementation: DocumentNodeImplementation::ProtoNode(passthrough_node.clone()), - visible: false, - ..Default::default() - } - }; - return (NodeId(i as u64), document_node); - } - - let single_wire_type = match inputs.len() { - 1 => inputs.iter().next(), - _ => None, - }; ( NodeId(i as u64), - match single_wire_type { - Some(input) => { + match inputs.len() { + 1 => { + let input = inputs.iter().next().unwrap(); let input_ty = input.nested_type(); + let mut inputs = vec![NodeInput::import(input.clone(), i)]; - // A single-registered ranked field gets the input adapter, so ranked wires pass through and convertible elements cast - let element_name = match input_ty { - Type::Item(element) => Some(element.identifier_name()), - Type::List(element) => Some(element.identifier_name()), - _ => (input_ty.identifier_name() == "ListDyn").then(|| "ListDyn".to_string()), + let into_node_identifier = ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::IntoNode<{}>", input_ty.identifier_name())); + let convert_node_identifier = ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::ConvertNode<{}>", input_ty.identifier_name())); + + let proto_node = if into_node_registry.keys().any(|ident: &ProtoNodeIdentifier| ident.as_str() == into_node_identifier.as_str()) { + generated_nodes += 1; + into_node_identifier + } else if into_node_registry.keys().any(|ident| ident.as_str() == convert_node_identifier.as_str()) { + generated_nodes += 1; + inputs.push(NodeInput::value(TaggedValue::None, false)); + convert_node_identifier + } else { + passthrough_node.clone() }; - if let Some(element_name) = element_name { - let input_adapter_identifier = ProtoNodeIdentifier::with_owned_string(format!("input_adapter<{element_name}>")); - if into_node_registry.keys().any(|ident| ident.as_str() == input_adapter_identifier.as_str()) { - generated_nodes += 1; - let mut original_location = OriginalLocation::default(); - original_location.auto_convert_index = Some(i); - let document_node = DocumentNode { - inputs: vec![NodeInput::import(generic!(X), i)], - implementation: DocumentNodeImplementation::ProtoNode(input_adapter_identifier), - visible: true, - original_location, - ..Default::default() - }; - return (NodeId(i as u64), document_node); - } - } - let mut original_location = OriginalLocation::default(); original_location.auto_convert_index = Some(i); DocumentNode { - inputs: vec![NodeInput::import(input.clone(), i)], - implementation: DocumentNodeImplementation::ProtoNode(passthrough_node.clone()), + inputs, + implementation: DocumentNodeImplementation::ProtoNode(proto_node), visible: true, original_location, ..Default::default() } } - None => DocumentNode { + _ => DocumentNode { inputs: vec![NodeInput::import(generic!(X), i)], implementation: DocumentNodeImplementation::ProtoNode(passthrough_node.clone()), visible: false, @@ -322,13 +275,12 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp let Some(ty) = field.default_type.as_ref().or_else(|| first_node_io.inputs.get(index)) else { return NodeInput::value(TaggedValue::None, true); }; - let ty = ty.clone().normalize_rank(); - let exposed = if index == 0 { ty != fn_type_fut!(Context, ()) } else { field.exposed }; + let exposed = if index == 0 { *ty != fn_type_fut!(Context, ()) } else { field.exposed }; match &field.value_source { RegistryValueSource::None => {} RegistryValueSource::Default(data) => { - if let Some(custom_default) = TaggedValue::from_primitive_string(data, &ty) { + if let Some(custom_default) = TaggedValue::from_primitive_string(data, ty) { return NodeInput::value(custom_default, exposed); } else { // It is incredibly useful to get a warning when the default type cannot be parsed rather than defaulting to `()`. @@ -339,17 +291,9 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp RegistryValueSource::SourceId => return NodeInput::Reflection(DocumentNodeMetadata::SourceId), }; - // A ranked `Item` type prefers a bare `T` value (promoted at resolution), since bare values drive the Properties panel widgets - if let Type::Item(element) = &ty - && let Some(type_default) = TaggedValue::from_type(element) - { + if let Some(type_default) = TaggedValue::from_type(ty) { return NodeInput::value(type_default, exposed); } - - if let Some(type_default) = TaggedValue::from_type(&ty) { - return NodeInput::value(type_default, exposed); - } - NodeInput::value(TaggedValue::None, true) }) .collect() @@ -367,64 +311,3 @@ impl std::fmt::Display for PreprocessorError { } } } - -/// Collapses an element-wise node's dual wire registration for one field, `{Item, List}`, to its `List` document wire form. -fn collapse_item_list_pair(types: &HashSet) -> Option<&Type> { - let mut types_iterator = types.iter(); - let (first, second) = (types_iterator.next()?, types_iterator.next()?); - if types_iterator.next().is_some() { - return None; - } - - for (item, list) in [(first, second), (second, first)] { - if let Type::List(list_element) = list.nested_type() - && let Type::Item(item_element) = item.nested_type() - && list_element == item_element - { - return Some(list); - } - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn item_list_wire_pair_collapses_to_list() { - let registry = core_types::registry::NODE_REGISTRY.lock().unwrap(); - let identifier = ProtoNodeIdentifier::new("core_types::vector::DimensionsNode"); - let implementations = registry.get(&identifier).expect("Dimensions should be registered"); - - let primary_types: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.inputs[0].clone()).collect(); - assert_eq!(primary_types.len(), 2, "An element-wise node should register Item and List wire variants for its primary input"); - - let collapsed = collapse_item_list_pair(&primary_types).expect("The Item/List wire pair should collapse"); - assert!( - matches!(collapsed.nested_type(), Type::List(_)), - "The collapse should pick the structural List form, but got {}", - collapsed.nested_type() - ); - } - - #[test] - fn fill_paint_color_default_parses_against_its_graphic_wire() { - let node_registry = core_types::registry::NODE_REGISTRY.lock().unwrap(); - let metadata_registry = core_types::registry::NODE_METADATA.lock().unwrap(); - - let identifier = graphene_std::vector::fill::IDENTIFIER; - let implementations = node_registry.get(&identifier).expect("Fill should be registered"); - let first_node_io = implementations.first().map(|(_, node_io)| node_io).expect("Fill should have at least one implementation"); - let metadata = metadata_registry.get(&identifier).expect("Fill should have registered metadata"); - - let inputs = node_inputs(&metadata.fields, first_node_io); - let paint = inputs[1].as_value().expect("The paint input should hold a value"); - assert_eq!( - *paint, - TaggedValue::Color(Color::BLACK), - "The paint input's `Color::BLACK` default should parse against its `Item` wire type" - ); - } -}