diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 5d8cb2b7ad..f9bcf4e998 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1,8 +1,8 @@ use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill, Stroke}; -use core_types::attribute::Attribute; +use core_types::attribute::{Attribute, Opacity}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::graphene_hash::CacheHash; -use core_types::lane::LaneSource; +use core_types::lane::{LaneColumn, LaneSource}; use core_types::list::{AttributeValueDyn, Item, ItemAttributeValues, List}; use core_types::ops::{FromAnchorPosition, ListConvert}; use core_types::render_complexity::RenderComplexity; @@ -11,7 +11,6 @@ use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use raster_types::{CPU, GPU, Raster}; -use std::borrow::Cow; use vector_types::GradientStops; pub use vector_types::Vector; @@ -201,14 +200,13 @@ pub fn is_paint_present(graphic_list: &List) -> bool { } /// Look up the paint graphics stored under the marker `A`, in the canonical `List` form. -pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option>> +pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option<&'a List> where S: LaneSource, A: Attribute = Option<&'a List>>, { source .attr::(index) - .map(Cow::Borrowed) // Treat a blank paint attribute as absent so an empty attribute doesn't count as painted .filter(|graphic_list| is_paint_present(graphic_list)) } @@ -223,6 +221,110 @@ where paint_graphics::(source, index).is_some() } +/// Whether every lane of a vector source draws as a plain clip path: fully +/// opaque, fill absent or opaque, stroke invisible or fully transparent. +pub fn vector_can_reduce_to_clip_path>(source: &S) -> bool { + (0..source.lane_count()).all(|index| { + let Some(element) = source.element(index) else { return false }; + let opacity: f64 = source.attr::(index); + + let fill_opaque_or_absent = paint_graphics::(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque())); + + let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) + || paint_graphics::(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); + + opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent + }) +} + +/// The paint a lane carries for its interiors, in the reference form +/// [`PaintOverlay`] threads down. +#[derive(Clone, Copy, Default)] +pub struct LanePaint<'a> { + pub fill: Option<&'a List>, + pub stroke: Option<&'a List>, +} + +impl<'a> LanePaint<'a> { + pub const NONE: Self = Self { fill: None, stroke: None }; + + /// The lane's present, non-blank paint. + pub fn read(source: &'a S, index: usize) -> Self { + Self { + fill: paint_graphics::(source, index), + stroke: paint_graphics::(source, index), + } + } + + pub fn is_present(&self) -> bool { + self.fill.is_some() || self.stroke.is_some() + } +} + +/// A source with a lane's paint forced over its fill and stroke columns, +/// reaching the interiors the legacy conversion's paint push reached. +pub struct PaintOverlay<'a, S> { + inner: &'a S, + paint: LanePaint<'a>, +} + +impl<'a, S> PaintOverlay<'a, S> { + pub fn new(inner: &'a S, paint: LanePaint<'a>) -> Self { + Self { inner, paint } + } +} + +pub struct PaintOverlayColumn<'a, S: LaneSource + 'a, A: Attribute> { + inner: S::Column<'a, A>, + forced: Option>, +} + +impl<'a, S: LaneSource, A: Attribute> LaneColumn<'a, A> for PaintOverlayColumn<'a, S, A> { + fn try_get(&self, lane: usize) -> Option> { + match self.forced { + Some(forced) => Some(forced), + None => self.inner.try_get(lane), + } + } +} + +/// The forced value for the marker `A`: the lane paint where `A` is this +/// crate's fill or stroke marker, absent otherwise. +fn forced_paint<'a, A: Attribute>(paint: LanePaint<'a>) -> Option> { + let slot = match A::NAME { + name if name == Fill::NAME => paint.fill, + name if name == Stroke::NAME => paint.stroke, + _ => None, + }?; + // SAFETY: the census admits one value type per attribute name and panics on + // a conflict at registration, so a marker named `fill` or `stroke` carries + // this crate's `Option<&List>` value form. + Some(unsafe { std::mem::transmute_copy::>, A::Value<'a>>(&Some(slot)) }) +} + +impl<'a, S: LaneSource> LaneSource for PaintOverlay<'a, S> { + type Element = S::Element; + type Column<'b, A: Attribute> + = PaintOverlayColumn<'b, S, A> + where + Self: 'b; + + fn lane_count(&self) -> usize { + self.inner.lane_count() + } + + fn element(&self, lane: usize) -> Option<&S::Element> { + self.inner.element(lane) + } + + fn column(&self) -> PaintOverlayColumn<'_, S, A> { + PaintOverlayColumn { + inner: self.inner.column::(), + forced: forced_paint::(self.paint), + } + } +} + /// Stores a paint attribute in the paint marker's owned form, the only representation paint readers accept. pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) { attributes.insert(key, Some(paint.into_graphic_list())); @@ -449,17 +551,7 @@ impl Graphic { pub fn can_reduce_to_clip_path(&self) -> bool { match self { - Graphic::Vector(vector) => (0..vector.len()).all(|index| { - let Some(element) = vector.element(index) else { return false }; - let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.); - - let fill_opaque_or_absent = paint_graphics::(vector, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque())); - - let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) - || paint_graphics::(vector, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); - - opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent - }), + Graphic::Vector(vector) => vector_can_reduce_to_clip_path(vector), _ => false, } } @@ -1051,6 +1143,79 @@ mod tests { } } +#[cfg(test)] +mod run_tests { + use super::*; + use core_types::attribute::Attribute; + use core_types::bounds::BoundingBox; + use core_types::lane::LaneSource; + use core_types::node::RecordBatch; + use core_types::record::{FieldWrite, GroupItem, Layout, RunView, element_write_hashed}; + use glam::{DAffine2, DVec2}; + use vector_types::subpath::Subpath; + use vector_types::vector::PointId; + + fn unit_square_at(corner: DVec2) -> Vector { + Vector::from_subpath(Subpath::::new_rectangle(corner, corner + DVec2::ONE)) + } + + #[test] + fn a_run_serves_the_parked_paint_reference() { + let paint = List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK))); + let vector = unit_square_at(DVec2::ZERO); + + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + let mut bytes = vec![0u8; layout.lane_stride()]; + // SAFETY: `bytes` is one lane of `layout`; a parked element stores its + // reference, and the fill field stores the marker's value form. + unsafe { + let base = bytes.as_mut_ptr(); + base.cast::<&Vector>().write(&vector); + base.add(layout.offset_of(Fill::NAME, 0).unwrap()).cast::>>().write(Some(&paint)); + } + // SAFETY: `bytes` holds one lane of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 1, &layout)) }; + let run = RunView::::new(&item).expect("the run holds vector elements"); + + assert_eq!(run.attr::(0), Some(&paint)); + assert_eq!(paint_graphics::(&run, 0), Some(&paint)); + assert_eq!(paint_graphics::(&run, 0), None); + + let legacy = run_to_legacy_list::(&item).expect("the run lowers to a legacy vector list"); + assert_eq!(paint_graphics::(&legacy, 0), paint_graphics::(&run, 0)); + } + + #[test] + fn a_run_and_its_legacy_list_agree_on_bounding_boxes() { + let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(4., 4.))]; + let transforms = [DAffine2::from_translation(DVec2::new(1., 2.)), DAffine2::from_scale(DVec2::splat(3.))]; + + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + let stride = layout.lane_stride(); + let mut bytes = vec![0u8; stride * 2]; + // SAFETY: `bytes` is `stride` per lane, and the offsets come from `layout`. + unsafe { + for lane in 0..2 { + let base = bytes.as_mut_ptr().add(lane * stride); + base.cast::<&Vector>().write(&vectors[lane]); + base.add(layout.offset_of(core_types::ATTR_TRANSFORM, 0).unwrap()).cast::().write(transforms[lane]); + } + } + // SAFETY: `bytes` holds two lanes of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 2, &layout)) }; + let run = RunView::::new(&item).expect("the run holds vector elements"); + let legacy = run_to_legacy_list::(&item).expect("the run lowers to a legacy vector list"); + + let outer = DAffine2::from_angle(0.3); + for include_stroke in [false, true] { + let bounds = run.bounding_box(outer, include_stroke); + assert_eq!(bounds, legacy.bounding_box(outer, include_stroke)); + assert!(matches!(bounds, RenderBoundingBox::Rectangle(_))); + assert_eq!(run.thumbnail_bounding_box(outer, include_stroke), legacy.thumbnail_bounding_box(outer, include_stroke)); + } + } +} + #[cfg(test)] mod graphic_is_opaque_tests { use vector_types::{ATTR_SPREAD_METHOD, GradientSpreadMethod, GradientStop}; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index f00a424914..175c9a6435 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -12,6 +12,7 @@ use core_types::color::Color; use core_types::color::SRGBA8; use core_types::lane::LaneSource; use core_types::list::{Item, List}; +use core_types::record::{Group, GroupContent, RunView}; use core_types::math::quad::Quad; use core_types::render_complexity::RenderComplexity; use core_types::transform::Footprint; @@ -21,7 +22,7 @@ use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; use graphene_hash::CacheHashWrapper; use graphene_resource::Resource; -use graphic_types::graphic::{has_paint, is_paint_present, paint_graphics, set_paint_attribute}; +use graphic_types::graphic::{LanePaint, PaintOverlay, group_to_legacy_list, 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}; @@ -557,7 +558,7 @@ impl Render for Graphic { Graphic::Color(list) => list.render_svg(render, render_params), Graphic::Gradient(list) => list.render_svg(render, render_params), Graphic::Text(list) => list.render_svg(render, render_params), - Graphic::Group(group) => graphic_types::graphic::group_to_legacy_list(group).render_svg(render, render_params), + Graphic::Group(group) => render_group_svg(group, PaintReach::NONE, render, render_params), } } @@ -570,110 +571,20 @@ impl Render for Graphic { Graphic::Color(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Gradient(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Text(list) => list.render_to_vello(scene, transform, context, render_params), - Graphic::Group(group) => graphic_types::graphic::group_to_legacy_list(group).render_to_vello(scene, transform, context, render_params), + Graphic::Group(group) => render_group_vello(group, PaintReach::NONE, scene, transform, context, render_params), } } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { - if let Some(element_id) = element_id { - match self { - Graphic::Group(_) => { - metadata.upstream_footprints.insert(element_id, footprint); - } - Graphic::Graphic(_) => { - metadata.upstream_footprints.insert(element_id, footprint); - } - Graphic::Vector(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - let layer_path: &[NodeId] = list.attr::(0); - let layer = layer_path.last().copied(); - let transform: DAffine2 = list.attr::(0); - - metadata.first_element_source_id.insert(element_id, layer); - metadata.local_transforms.insert(element_id, transform); - } - } - Graphic::RasterCPU(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - Graphic::RasterGPU(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - Graphic::Color(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - Graphic::Gradient(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - Graphic::Text(list) => { - metadata.upstream_footprints.insert(element_id, footprint); - - // TODO: Find a way to handle more than the first item - if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attr::(0)); - } - } - } - } - - match self { - Graphic::Graphic(list) => list.collect_metadata(metadata, footprint, element_id), - Graphic::Vector(list) => list.collect_metadata(metadata, footprint, element_id), - Graphic::RasterCPU(list) => list.collect_metadata(metadata, footprint, element_id), - Graphic::RasterGPU(list) => list.collect_metadata(metadata, footprint, element_id), - Graphic::Color(list) => list.collect_metadata(metadata, footprint, element_id), - Graphic::Gradient(list) => list.collect_metadata(metadata, footprint, element_id), - Graphic::Text(list) => list.collect_metadata(metadata, footprint, element_id), - Graphic::Group(group) => graphic_types::graphic::group_to_legacy_list(group).collect_metadata(metadata, footprint, element_id), - } + collect_element_metadata(self, PaintReach::NONE, metadata, footprint, element_id) } fn add_upstream_click_targets(&self, click_targets: &mut Vec) { - match self { - Graphic::Graphic(list) => list.add_upstream_click_targets(click_targets), - Graphic::Vector(list) => list.add_upstream_click_targets(click_targets), - Graphic::RasterCPU(list) => list.add_upstream_click_targets(click_targets), - Graphic::RasterGPU(list) => list.add_upstream_click_targets(click_targets), - Graphic::Color(list) => list.add_upstream_click_targets(click_targets), - Graphic::Gradient(list) => list.add_upstream_click_targets(click_targets), - Graphic::Text(list) => list.add_upstream_click_targets(click_targets), - Graphic::Group(group) => graphic_types::graphic::group_to_legacy_list(group).add_upstream_click_targets(click_targets), - } + add_element_upstream_click_targets(self, PaintReach::NONE, click_targets) } fn add_upstream_outline_targets(&self, outlines: &mut Vec) { - match self { - Graphic::Graphic(list) => list.add_upstream_outline_targets(outlines), - Graphic::Vector(list) => list.add_upstream_outline_targets(outlines), - Graphic::RasterCPU(list) => list.add_upstream_outline_targets(outlines), - Graphic::RasterGPU(list) => list.add_upstream_outline_targets(outlines), - Graphic::Color(list) => list.add_upstream_outline_targets(outlines), - Graphic::Gradient(list) => list.add_upstream_outline_targets(outlines), - Graphic::Text(list) => list.add_upstream_outline_targets(outlines), - Graphic::Group(group) => graphic_types::graphic::group_to_legacy_list(group).add_upstream_outline_targets(outlines), - } + add_element_upstream_outline_targets(self, PaintReach::NONE, outlines) } fn contains_artboard(&self) -> bool { @@ -703,6 +614,347 @@ impl Render for Graphic { } } +/// How far a lane's paint reaches into the element beneath it, mirroring the +/// legacy conversion's paint push: vector interiors directly and vector +/// children of a nested graphic list, one level deep. +#[derive(Clone, Copy)] +struct PaintReach<'a> { + paint: LanePaint<'a>, + hops: u8, +} + +impl<'a> PaintReach<'a> { + const NONE: Self = Self { paint: LanePaint::NONE, hops: 0 }; + + fn read(source: &'a S, index: usize) -> Self { + Self { + paint: LanePaint::read(source, index), + hops: 2, + } + } + + /// The lane's effective reach: an inherited paint stays authoritative + /// (lane paint below a push's origin is inert in the legacy model), an + /// absent one reads the lane's own paint. + fn for_lane(self, source: &'a S, index: usize) -> Self { + match self.paint.is_present() { + true => self, + false => Self::read(source, index), + } + } + + fn applies(&self) -> bool { + self.hops > 0 && self.paint.is_present() + } + + /// The reach one graphic nesting level further down. + fn nested(self) -> Self { + Self { + paint: self.paint, + hops: self.hops.saturating_sub(1), + } + } + + /// The reach entering a group's own graphic run: a spent or absent reach + /// resets so the group's own lane paint applies at its own boundary. + fn into_group_graphics(self) -> Self { + match self.applies() { + true => self.nested(), + false => Self::NONE, + } + } +} + +fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) { + match element { + Graphic::Vector(inner) if reach.applies() => render_vector_svg(&PaintOverlay::new(inner, reach.paint), render, render_params), + Graphic::Graphic(inner) => render_graphic_svg_with(inner, reach.nested(), render, render_params), + Graphic::Group(group) => render_group_svg(group, reach, render, render_params), + _ => element.render_svg(render, render_params), + } +} + +fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { + match element { + Graphic::Vector(inner) if reach.applies() => render_vector_vello(&PaintOverlay::new(inner, reach.paint), scene, transform, context, render_params), + Graphic::Graphic(inner) => render_graphic_vello_with(inner, reach.nested(), scene, transform, context, render_params), + Graphic::Group(group) => render_group_vello(group, reach, scene, transform, context, render_params), + _ => element.render_to_vello(scene, transform, context, render_params), + } +} + +fn element_can_reduce_to_clip_path<'a>(element: &'a Graphic, reach: PaintReach<'a>) -> bool { + match element { + Graphic::Vector(inner) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(inner, reach.paint)), + Graphic::Group(group) => match &group.content { + GroupContent::Run(item) => match RunView::::new(item) { + Some(run) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&run, reach.paint)), + Some(run) => vector_can_reduce_to_clip_path(&run), + None => false, + }, + GroupContent::Stack(_) => false, + }, + _ => element.can_reduce_to_clip_path(), + } +} + +fn collect_element_metadata<'a>(element: &'a Graphic, reach: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { + if let Some(element_id) = element_id { + match element { + Graphic::Group(_) => { + metadata.upstream_footprints.insert(element_id, footprint); + } + Graphic::Graphic(_) => { + metadata.upstream_footprints.insert(element_id, footprint); + } + Graphic::Vector(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + let layer_path: &[NodeId] = list.attr::(0); + let layer = layer_path.last().copied(); + let transform: DAffine2 = list.attr::(0); + + metadata.first_element_source_id.insert(element_id, layer); + metadata.local_transforms.insert(element_id, transform); + } + } + Graphic::RasterCPU(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attr::(0)); + } + } + Graphic::RasterGPU(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attr::(0)); + } + } + Graphic::Color(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attr::(0)); + } + } + Graphic::Gradient(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attr::(0)); + } + } + Graphic::Text(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attr::(0)); + } + } + } + } + + match element { + Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id), + Graphic::Vector(list) if reach.applies() => collect_vector_metadata(&PaintOverlay::new(list, reach.paint), metadata, footprint, element_id), + Graphic::Vector(list) => collect_vector_metadata(list, metadata, footprint, element_id), + Graphic::RasterCPU(list) => collect_raster_metadata(list, metadata, footprint, element_id), + Graphic::RasterGPU(list) => collect_raster_metadata(list, metadata, footprint, element_id), + Graphic::Color(_) => {} + Graphic::Gradient(_) => {} + Graphic::Text(list) => collect_text_metadata(list, metadata, footprint, element_id), + Graphic::Group(group) => collect_group_metadata(group, reach, metadata, footprint, element_id), + } +} + +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::Vector(list) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(list, reach.paint), click_targets), + Graphic::Vector(list) => add_vector_upstream_click_targets(list, click_targets), + Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets), + Graphic::Color(_) | Graphic::Gradient(_) => {} + Graphic::Text(list) => add_text_upstream_click_targets(list, click_targets), + Graphic::Group(group) => add_group_upstream_click_targets(group, reach, click_targets), + } +} + +fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, outlines: &mut Vec) { + match element { + Graphic::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach.nested(), outlines), + Graphic::Vector(list) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(list, reach.paint), outlines), + Graphic::Vector(list) => add_vector_upstream_outline_targets(list, outlines), + Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines), + Graphic::Color(_) | Graphic::Gradient(_) => {} + Graphic::Text(list) => add_text_upstream_click_targets(list, outlines), + Graphic::Group(group) => add_group_upstream_outline_targets(group, reach, outlines), + } +} + +/// The native group render: a run dispatches on its element type into the +/// generic bodies. `Stack` has consumers but no constructor yet, so it and +/// unknown element types keep the legacy conversion. +fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) { + let GroupContent::Run(item) = &group.content else { + return group_to_legacy_list(group).render_svg(render, render_params); + }; + if let Some(run) = RunView::::new(item) { + render_graphic_svg_with(&run, reach.into_group_graphics(), render, render_params) + } else if let Some(run) = RunView::::new(item) { + match reach.applies() { + true => render_vector_svg(&PaintOverlay::new(&run, reach.paint), render, render_params), + false => render_vector_svg(&run, render, render_params), + } + } else if let Some(run) = RunView::>::new(item) { + render_raster_cpu_svg(&run, render, render_params) + } 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) { + render_gradient_svg(&run, render, render_params) + } else if let Some(run) = RunView::::new(item) { + render_text_svg(&run, render, render_params) + } else { + group_to_legacy_list(group).render_svg(render, render_params) + } +} + +fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { + let GroupContent::Run(item) = &group.content else { + return group_to_legacy_list(group).render_to_vello(scene, transform, context, render_params); + }; + if let Some(run) = RunView::::new(item) { + render_graphic_vello_with(&run, reach.into_group_graphics(), scene, transform, context, render_params) + } else if let Some(run) = RunView::::new(item) { + match reach.applies() { + true => render_vector_vello(&PaintOverlay::new(&run, reach.paint), scene, transform, context, render_params), + false => render_vector_vello(&run, scene, transform, context, render_params), + } + } else if let Some(run) = RunView::>::new(item) { + render_raster_cpu_vello(&run, scene, transform, render_params) + } else if let Some(run) = RunView::>::new(item) { + 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) { + render_gradient_vello(&run, scene, transform, render_params) + } else if let Some(run) = RunView::::new(item) { + render_text_vello(&run, scene, transform, render_params) + } else { + group_to_legacy_list(group).render_to_vello(scene, transform, context, render_params) + } +} + +/// Reproduces the legacy wrapper's metadata effects for a typed run: the +/// lanes collect with their own recovered ids, and the caller's element id +/// aggregates the run's upstream click targets and outlines. +fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { + fn typed_run( + source: &S, + metadata: &mut RenderMetadata, + footprint: Footprint, + element_id: Option, + collect: impl Fn(&S, &mut RenderMetadata, Footprint, Option), + click: impl Fn(&S, &mut Vec), + outline: impl Fn(&S, &mut Vec), + ) { + collect(source, metadata, footprint, None); + let Some(element_id) = element_id else { return }; + let mut click_targets = Vec::new(); + click(source, &mut click_targets); + metadata.click_targets.insert(element_id, click_targets.into_iter().map(Into::into).collect()); + let mut outlines = Vec::new(); + outline(source, &mut outlines); + metadata.outlines.insert(element_id, outlines.into_iter().map(Into::into).collect()); + } + + let GroupContent::Run(item) = &group.content else { + return group_to_legacy_list(group).collect_metadata(metadata, footprint, element_id); + }; + if let Some(run) = RunView::::new(item) { + collect_graphic_metadata_with(&run, reach.into_group_graphics(), metadata, footprint, element_id) + } else if let Some(run) = RunView::::new(item) { + match reach.applies() { + true => typed_run( + &PaintOverlay::new(&run, reach.paint), + metadata, + footprint, + element_id, + collect_vector_metadata, + add_vector_upstream_click_targets, + add_vector_upstream_outline_targets, + ), + false => typed_run(&run, metadata, footprint, element_id, collect_vector_metadata, add_vector_upstream_click_targets, add_vector_upstream_outline_targets), + } + } else if let Some(run) = RunView::>::new(item) { + typed_run(&run, metadata, footprint, element_id, collect_raster_metadata, |_, out| add_raster_upstream_click_targets(out), |_, out| { + add_raster_upstream_click_targets(out) + }) + } else if let Some(run) = RunView::>::new(item) { + typed_run(&run, metadata, footprint, element_id, collect_raster_metadata, |_, out| add_raster_upstream_click_targets(out), |_, out| { + add_raster_upstream_click_targets(out) + }) + } else if let Some(run) = RunView::::new(item) { + typed_run(&run, metadata, footprint, element_id, |_, _, _, _| (), |_, _| (), |_, _| ()) + } else if let Some(run) = RunView::::new(item) { + typed_run(&run, metadata, footprint, element_id, |_, _, _, _| (), |_, _| (), |_, _| ()) + } else if let Some(run) = RunView::::new(item) { + typed_run(&run, metadata, footprint, element_id, collect_text_metadata, add_text_upstream_click_targets, add_text_upstream_click_targets) + } else { + group_to_legacy_list(group).collect_metadata(metadata, footprint, element_id) + } +} + +fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>, click_targets: &mut Vec) { + let GroupContent::Run(item) = &group.content else { + return group_to_legacy_list(group).add_upstream_click_targets(click_targets); + }; + if let Some(run) = RunView::::new(item) { + add_graphic_upstream_click_targets_with(&run, reach.into_group_graphics(), click_targets) + } else if let Some(run) = RunView::::new(item) { + match reach.applies() { + true => add_vector_upstream_click_targets(&PaintOverlay::new(&run, reach.paint), click_targets), + false => add_vector_upstream_click_targets(&run, click_targets), + } + } else if item.typed_lanes::>().is_some() || item.typed_lanes::>().is_some() { + add_raster_upstream_click_targets(click_targets) + } else if let Some(run) = RunView::::new(item) { + add_text_upstream_click_targets(&run, click_targets) + } else if item.typed_lanes::().is_some() || item.typed_lanes::().is_some() { + } else { + group_to_legacy_list(group).add_upstream_click_targets(click_targets) + } +} + +fn add_group_upstream_outline_targets<'a>(group: &'a Group, reach: PaintReach<'a>, outlines: &mut Vec) { + let GroupContent::Run(item) = &group.content else { + return group_to_legacy_list(group).add_upstream_outline_targets(outlines); + }; + if let Some(run) = RunView::::new(item) { + add_graphic_upstream_outline_targets_with(&run, reach.into_group_graphics(), outlines) + } else if let Some(run) = RunView::::new(item) { + match reach.applies() { + true => add_vector_upstream_outline_targets(&PaintOverlay::new(&run, reach.paint), outlines), + false => add_vector_upstream_outline_targets(&run, outlines), + } + } else if item.typed_lanes::>().is_some() || item.typed_lanes::>().is_some() { + add_raster_upstream_click_targets(outlines) + } else if let Some(run) = RunView::::new(item) { + add_text_upstream_click_targets(&run, outlines) + } else if item.typed_lanes::().is_some() || item.typed_lanes::().is_some() { + } else { + group_to_legacy_list(group).add_upstream_outline_targets(outlines) + } +} + /// Reads the artboard metadata for the item at `index`. fn read_artboard_attributes(source: &S, index: usize) -> (DVec2, DVec2, Color, bool) { let location: DVec2 = source.attr::(index); @@ -857,6 +1109,10 @@ impl Render for List { } fn render_graphic_svg>(source: &S, render: &mut SvgRender, render_params: &RenderParams) { + render_graphic_svg_with(source, PaintReach::NONE, render, render_params) +} + +fn render_graphic_svg_with<'a, S: LaneSource>(source: &'a S, inherited: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) { let mut mask_state = None; for index in 0..source.lane_count() { @@ -865,6 +1121,7 @@ fn render_graphic_svg>(source: &S, render: &mut let opacity_attr: f64 = source.attr::(index); let opacity_fill_attr: f64 = source.attr::(index); let element = source.element(index).unwrap(); + let reach = inherited.for_lane(source, index); render.parent_tag( "g", @@ -887,10 +1144,10 @@ fn render_graphic_svg>(source: &S, render: &mut if next_clips && mask_state.is_none() { let uuid = generate_uuid(); - let mask_type = if element.can_reduce_to_clip_path() { MaskType::Clip } else { MaskType::Mask }; + let mask_type = if element_can_reduce_to_clip_path(element, reach) { MaskType::Clip } else { MaskType::Mask }; mask_state = Some((uuid, mask_type)); let mut svg = SvgRender::new(); - element.render_svg(&mut svg, &render_params.for_clipper()); + render_element_svg(element, reach, &mut svg, &render_params.for_clipper()); write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap(); mask_type.write_to_defs(&mut attributes.0.svg_defs, uuid, svg.svg.to_svg_string()); @@ -906,13 +1163,17 @@ fn render_graphic_svg>(source: &S, render: &mut } }, |render| { - element.render_svg(render, render_params); + render_element_svg(element, reach, render, render_params); }, ); } } fn render_graphic_vello>(source: &S, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { + render_graphic_vello_with(source, PaintReach::NONE, scene, transform, context, render_params) +} + +fn render_graphic_vello_with<'a, S: LaneSource>(source: &'a S, inherited: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { let mut mask_element_and_transform = None; for index in 0..source.lane_count() { @@ -922,6 +1183,7 @@ fn render_graphic_vello>(source: &S, scene: &mu let opacity_attr: f64 = source.attr::(index); let opacity_fill_attr: f64 = source.attr::(index); let element = source.element(index).unwrap(); + let reach = inherited.for_lane(source, index); let mut layer = false; @@ -949,10 +1211,10 @@ fn render_graphic_vello>(source: &S, scene: &mu let next_clips = index + 1 < source.lane_count() && source.element(index + 1).unwrap().had_clip_enabled(); if next_clips && mask_element_and_transform.is_none() { - mask_element_and_transform = Some((element, transform)); + mask_element_and_transform = Some((element, transform, reach)); - element.render_to_vello(scene, transform, context, render_params); - } else if let Some((mask_element, transform_mask)) = mask_element_and_transform { + render_element_vello(element, reach, scene, transform, context, render_params); + } else if let Some((mask_element, transform_mask, mask_reach)) = mask_element_and_transform { if !next_clips { mask_element_and_transform = None; } @@ -964,7 +1226,7 @@ fn render_graphic_vello>(source: &S, scene: &mu let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); - mask_element.render_to_vello(scene, transform_mask, context, &render_params.for_clipper()); + render_element_vello(mask_element, mask_reach, scene, transform_mask, context, &render_params.for_clipper()); scene.push_layer( peniko::Fill::NonZero, peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcIn), @@ -974,14 +1236,14 @@ fn render_graphic_vello>(source: &S, scene: &mu ); } - element.render_to_vello(scene, transform, context, render_params); + render_element_vello(element, reach, scene, transform, context, render_params); if matches!(bounds, RenderBoundingBox::Rectangle(_)) { scene.pop_layer(); scene.pop_layer(); } } else { - element.render_to_vello(scene, transform, context, render_params); + render_element_vello(element, reach, scene, transform, context, render_params); } if layer { @@ -991,20 +1253,25 @@ fn render_graphic_vello>(source: &S, scene: &mu } fn collect_graphic_metadata>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { + collect_graphic_metadata_with(source, PaintReach::NONE, metadata, footprint, element_id) +} + +fn collect_graphic_metadata_with<'a, S: LaneSource>(source: &'a S, inherited: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for index in 0..source.lane_count() { let item_transform: DAffine2 = source.attr::(index); let layer_path: &[NodeId] = source.attr::(index); let layer = layer_path.last().copied(); let element = source.element(index).unwrap(); + let reach = inherited.for_lane(source, index); let mut footprint = footprint; footprint.transform *= item_transform; if let Some(element_id) = layer { - element.collect_metadata(metadata, footprint, Some(element_id)); + collect_element_metadata(element, reach, metadata, footprint, Some(element_id)); } else { // Recurse through anonymous wrapper items to reach nested content with editor:layer_path tags - element.collect_metadata(metadata, footprint, None); + collect_element_metadata(element, reach, metadata, footprint, None); } } @@ -1015,9 +1282,10 @@ fn collect_graphic_metadata>(source: &S, metada for index in 0..source.lane_count() { let item_transform: DAffine2 = source.attr::(index); let element = source.element(index).unwrap(); + let reach = inherited.for_lane(source, index); let mut new_click_targets = Vec::new(); - element.add_upstream_click_targets(&mut new_click_targets); + add_element_upstream_click_targets(element, reach, &mut new_click_targets); for click_target in new_click_targets.iter_mut() { click_target.apply_transform(item_transform) @@ -1026,7 +1294,7 @@ fn collect_graphic_metadata>(source: &S, metada all_upstream_click_targets.extend(new_click_targets); let mut new_outlines = Vec::new(); - element.add_upstream_outline_targets(&mut new_outlines); + add_element_upstream_outline_targets(element, reach, &mut new_outlines); for outline in new_outlines.iter_mut() { outline.apply_transform(item_transform) } @@ -1039,12 +1307,17 @@ fn collect_graphic_metadata>(source: &S, metada } fn add_graphic_upstream_click_targets>(source: &S, click_targets: &mut Vec) { + add_graphic_upstream_click_targets_with(source, PaintReach::NONE, click_targets) +} + +fn add_graphic_upstream_click_targets_with<'a, S: LaneSource>(source: &'a S, inherited: PaintReach<'a>, click_targets: &mut Vec) { for index in 0..source.lane_count() { let item_transform: DAffine2 = source.attr::(index); let element = source.element(index).unwrap(); + let reach = inherited.for_lane(source, index); let mut new_click_targets = Vec::new(); - element.add_upstream_click_targets(&mut new_click_targets); + add_element_upstream_click_targets(element, reach, &mut new_click_targets); for click_target in new_click_targets.iter_mut() { click_target.apply_transform(item_transform) @@ -1055,12 +1328,17 @@ fn add_graphic_upstream_click_targets>(source: } fn add_graphic_upstream_outline_targets>(source: &S, outlines: &mut Vec) { + add_graphic_upstream_outline_targets_with(source, PaintReach::NONE, outlines) +} + +fn add_graphic_upstream_outline_targets_with<'a, S: LaneSource>(source: &'a S, inherited: PaintReach<'a>, outlines: &mut Vec) { for index in 0..source.lane_count() { let item_transform: DAffine2 = source.attr::(index); let element = source.element(index).unwrap(); + let reach = inherited.for_lane(source, index); let mut new_outlines = Vec::new(); - element.add_upstream_outline_targets(&mut new_outlines); + add_element_upstream_outline_targets(element, reach, &mut new_outlines); for outline in new_outlines.iter_mut() { outline.apply_transform(item_transform) @@ -1143,10 +1421,10 @@ fn render_vector_svg>(source: &S, render: &mut S }; let fill_graphic_list = paint_graphics::(source, index); - let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0)); + 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.as_ref().and_then(|l| l.element(0)); + let stroke_graphic = stroke_graphic_list.and_then(|l| l.element(0)); let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let can_draw_aligned_stroke = path_is_closed @@ -1163,7 +1441,7 @@ fn render_vector_svg>(source: &S, render: &mut S emit_svg_fill_path( render, path.clone(), - fill_graphic_list.as_deref(), + fill_graphic_list, item_transform, element_transform, applied_stroke_transform, @@ -1195,7 +1473,7 @@ fn render_vector_svg>(source: &S, render: &mut S emit_svg_fill_path( render, face_d, - fill_graphic_list.as_deref(), + fill_graphic_list, item_transform, element_transform, applied_stroke_transform, @@ -1247,7 +1525,7 @@ fn render_vector_svg>(source: &S, render: &mut S .stroke .as_ref() .map(|stroke| { - if stroke_graphic_list.as_deref().is_some_and(is_paint_present) { + if stroke_graphic_list.is_some_and(is_paint_present) { stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke) } else { String::new() @@ -1259,7 +1537,6 @@ fn render_vector_svg>(source: &S, render: &mut S let stroke_visible = vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent()); let stroke_attribute = if stroke_visible { stroke_graphic_list - .as_deref() .map(|list| { // Gradient should align with the fill path bbox so that a shared gradient lines up across fill and stroke. // Only clipping-based paints need the stroke-inclusive bbox. @@ -1278,7 +1555,6 @@ fn render_vector_svg>(source: &S, render: &mut S r#" fill="none""#.to_string() } else { fill_graphic_list - .as_deref() .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()) }; @@ -1310,7 +1586,7 @@ fn render_vector_svg>(source: &S, render: &mut S emit_svg_fill_path( render, path.clone(), - fill_graphic_list.as_deref(), + fill_graphic_list, item_transform, element_transform, applied_stroke_transform, @@ -1369,7 +1645,7 @@ fn render_vector_vello>(source: &S, scene: &mut // 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.as_ref().is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent())); + 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()); @@ -1396,7 +1672,7 @@ fn render_vector_vello>(source: &S, scene: &mut let wants_stroke_below = stroke.is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| { - let Some(fill_graphic) = fill_graphic_list.as_deref() else { return }; + let Some(fill_graphic) = fill_graphic_list else { return }; for paint_index in 0..fill_graphic.len() { let Some(paint) = fill_graphic.element(paint_index) else { continue }; @@ -1449,7 +1725,7 @@ fn render_vector_vello>(source: &S, scene: &mut }; let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| { - let Some(stroke_graphic_list) = stroke_graphic_list.as_deref() else { return }; + let Some(stroke_graphic_list) = stroke_graphic_list else { return }; let Some(stroke) = stroke else { return }; for paint_index in 0..stroke_graphic_list.len() { @@ -1648,10 +1924,10 @@ fn collect_vector_metadata>(source: &S, metadata e.insert(Arc::new(element.clone())); if let Some(fill_graphic) = paint_graphics::(source, index) { - metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.into_owned())); + metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.clone())); } if let Some(stroke_graphic) = paint_graphics::(source, index) { - metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.into_owned())); + metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.clone())); } } @@ -2748,3 +3024,132 @@ impl SvgRenderAttrs<'_> { self.0.svg.push(value.into()); } } + +#[cfg(test)] +mod group_walk_tests { + use super::*; + use core_types::attribute::Attribute; + use core_types::node::RecordBatch; + use core_types::record::{FieldWrite, GroupItem, Layout, element_write_hashed}; + use graphic_types::graphic::group_to_legacy_graphic; + 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)) + } + + fn color_paint() -> List { + List::new_from_element(Graphic::Color(List::new_from_element(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap()))) + } + + fn rendered_svg(render: impl FnOnce(&mut SvgRender)) -> (String, String) { + let mut svg_render = SvgRender::new(); + render(&mut svg_render); + let output: SvgRenderOutput = svg_render.into(); + (output.svg, output.svg_defs) + } + + /// One run lane per element of `elements`, each with a fill field, under the given layout writes. + unsafe fn write_lanes(layout: &Layout, elements: &[&T], fill: &[Option<&List>]) -> Vec { + let stride = layout.lane_stride(); + let mut bytes = vec![0u8; stride * elements.len()]; + for lane in 0..elements.len() { + // SAFETY: `bytes` is `stride` per lane; a parked element stores its + // reference, and the fill field stores the marker's value form. + unsafe { + let base = bytes.as_mut_ptr().add(lane * stride); + base.cast::<&T>().write(elements[lane]); + base.add(layout.offset_of(Fill::NAME, 0).unwrap()).cast::>>().write(fill[lane]); + } + } + bytes + } + + #[test] + fn a_vector_run_group_renders_like_its_legacy_form() { + let paint = color_paint(); + let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(3., 1.))]; + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + // SAFETY: the layout carries a parked vector element and the fill field. + let bytes = unsafe { write_lanes::(&layout, &[&vectors[0], &vectors[1]], &[Some(&paint), None]) }; + // SAFETY: `bytes` holds two lanes of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 2, &layout)) }; + let group = Group { row: None, content: GroupContent::Run(item) }; + + let params = RenderParams::default(); + let native = rendered_svg(|render| Graphic::Group(group.clone()).render_svg(render, ¶ms)); + let legacy = rendered_svg(|render| group_to_legacy_graphic(&group).render_svg(render, ¶ms)); + + assert!(native.0.contains(r##"fill="#"##), "the run's fill paint must render: {}", native.0); + assert_eq!(native, legacy); + } + + #[test] + fn lane_paint_on_a_graphic_run_reaches_vector_interiors() { + let paint = color_paint(); + let inner = Graphic::Vector(List::new_from_element(unit_square_at(DVec2::ZERO))); + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + // SAFETY: the layout carries a parked graphic element and the fill field. + let bytes = unsafe { write_lanes::(&layout, &[&inner], &[Some(&paint)]) }; + // SAFETY: `bytes` holds one lane of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 1, &layout)) }; + let group = Group { row: None, content: GroupContent::Run(item) }; + + 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)); + + assert!(native.0.contains(r##"fill="#"##), "the lane's fill paint must reach the vector interior: {}", native.0); + assert_eq!(native, legacy); + } + + #[test] + fn a_group_collects_the_wrapper_metadata() { + let paint = color_paint(); + let vectors = [unit_square_at(DVec2::ZERO)]; + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + // SAFETY: the layout carries a parked vector element and the fill field. + let bytes = unsafe { write_lanes::(&layout, &[&vectors[0]], &[Some(&paint)]) }; + // SAFETY: `bytes` holds one lane of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 1, &layout)) }; + let group = Group { row: None, content: GroupContent::Run(item) }; + + let footprint = Footprint::default(); + let caller = NodeId(9); + + let mut native = RenderMetadata::default(); + Graphic::Group(group.clone()).collect_metadata(&mut native, footprint, Some(caller)); + + let mut legacy = RenderMetadata::default(); + legacy.upstream_footprints.insert(caller, footprint); + graphic_types::graphic::group_to_legacy_list(&group).collect_metadata(&mut legacy, footprint, Some(caller)); + + assert!(native.click_targets.get(&caller).is_some_and(|targets| !targets.is_empty())); + assert_eq!(native, legacy); + } + + #[test] + fn a_group_serves_the_wrapper_click_targets() { + let paint = color_paint(); + let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(2., 2.))]; + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + // SAFETY: the layout carries a parked vector element and the fill field. + let bytes = unsafe { write_lanes::(&layout, &[&vectors[0], &vectors[1]], &[Some(&paint), None]) }; + // SAFETY: `bytes` holds two lanes of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 2, &layout)) }; + let group = Group { row: None, content: GroupContent::Run(item) }; + + let mut native = Vec::new(); + Graphic::Group(group.clone()).add_upstream_click_targets(&mut native); + let mut legacy = Vec::new(); + graphic_types::graphic::group_to_legacy_list(&group).add_upstream_click_targets(&mut legacy); + + assert!(!native.is_empty()); + assert_eq!(native, legacy); + + let mut native_outlines = Vec::new(); + Graphic::Group(group).add_upstream_outline_targets(&mut native_outlines); + assert_eq!(native_outlines, legacy); + } +} diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 6db3990749..fefeaf4ebb 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -73,8 +73,8 @@ fn boolean_core<'e>( }; let element = result_vector_list.element(0).cloned().unwrap_or_default(); - let fill = park_paint(graphic_types::graphic::paint_graphics::(&result_vector_list, 0).map(|paint| paint.into_owned()))?; - let stroke = park_paint(graphic_types::graphic::paint_graphics::(&result_vector_list, 0).map(|paint| paint.into_owned()))?; + let fill = park_paint(graphic_types::graphic::paint_graphics::(&result_vector_list, 0).cloned())?; + let stroke = park_paint(graphic_types::graphic::paint_graphics::(&result_vector_list, 0).cloned())?; let layer_path: Vec = result_vector_list.attribute::>(ATTR_EDITOR_LAYER_PATH, 0).map(|path| path.clone()).unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; // Snapshot the input layers so the renderer can recurse into them for diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 35f04f969f..579412aa7b 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -3040,12 +3040,12 @@ fn morph_core(content: List, progression: f64, reverse: bool, distribut let fill_paint = { let source = paint_graphics::(&content, source_index); let target = paint_graphics::(&content, target_index); - lerp_graphic(source.as_deref(), target.as_deref(), time) + lerp_graphic(source, target, time) }; let stroke_paint = { let source = paint_graphics::(&content, source_index); let target = paint_graphics::(&content, target_index); - lerp_graphic(source.as_deref(), target.as_deref(), time) + lerp_graphic(source, target, time) }; // Work directly with manipulator groups, bypassing the BezPath intermediate representation.