mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Walk groups natively with lane paint threaded down
This commit is contained in:
@@ -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<Graphic>) -> bool {
|
||||
}
|
||||
|
||||
/// Look up the paint graphics stored under the marker `A`, in the canonical `List<Graphic>` form.
|
||||
pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option<Cow<'a, List<Graphic>>>
|
||||
pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option<&'a List<Graphic>>
|
||||
where
|
||||
S: LaneSource,
|
||||
A: Attribute<Value<'a> = Option<&'a List<Graphic>>>,
|
||||
{
|
||||
source
|
||||
.attr::<A>(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::<A, S>(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<S: LaneSource<Element = Vector>>(source: &S) -> bool {
|
||||
(0..source.lane_count()).all(|index| {
|
||||
let Some(element) = source.element(index) else { return false };
|
||||
let opacity: f64 = source.attr::<Opacity>(index);
|
||||
|
||||
let fill_opaque_or_absent = paint_graphics::<Fill, _>(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::<Stroke, _>(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<Graphic>>,
|
||||
pub stroke: Option<&'a List<Graphic>>,
|
||||
}
|
||||
|
||||
impl<'a> LanePaint<'a> {
|
||||
pub const NONE: Self = Self { fill: None, stroke: None };
|
||||
|
||||
/// The lane's present, non-blank paint.
|
||||
pub fn read<S: LaneSource>(source: &'a S, index: usize) -> Self {
|
||||
Self {
|
||||
fill: paint_graphics::<Fill, _>(source, index),
|
||||
stroke: paint_graphics::<Stroke, _>(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<A::Value<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, S: LaneSource, A: Attribute> LaneColumn<'a, A> for PaintOverlayColumn<'a, S, A> {
|
||||
fn try_get(&self, lane: usize) -> Option<A::Value<'a>> {
|
||||
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<A::Value<'a>> {
|
||||
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<Graphic>>` value form.
|
||||
Some(unsafe { std::mem::transmute_copy::<Option<&'a List<Graphic>>, 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<A: Attribute>(&self) -> PaintOverlayColumn<'_, S, A> {
|
||||
PaintOverlayColumn {
|
||||
inner: self.inner.column::<A>(),
|
||||
forced: forced_paint::<A>(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::<Fill, _>(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::<Stroke, _>(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::<PointId>::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::<Vector>(), &[FieldWrite::of::<Fill>(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::<Option<&List<Graphic>>>().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::<Vector>::new(&item).expect("the run holds vector elements");
|
||||
|
||||
assert_eq!(run.attr::<Fill>(0), Some(&paint));
|
||||
assert_eq!(paint_graphics::<Fill, _>(&run, 0), Some(&paint));
|
||||
assert_eq!(paint_graphics::<Stroke, _>(&run, 0), None);
|
||||
|
||||
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
|
||||
assert_eq!(paint_graphics::<Fill, _>(&legacy, 0), paint_graphics::<Fill, _>(&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::<Vector>(), &[FieldWrite::of::<core_types::attribute::Transform>(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::<DAffine2>().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::<Vector>::new(&item).expect("the run holds vector elements");
|
||||
let legacy = run_to_legacy_list::<Vector>(&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};
|
||||
|
||||
@@ -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<NodeId>) {
|
||||
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::<EditorLayerPath>(0);
|
||||
let layer = layer_path.last().copied();
|
||||
let transform: DAffine2 = list.attr::<Transform>(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::<Transform>(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::<Transform>(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::<Transform>(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::<Transform>(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::<Transform>(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<ClickTarget>) {
|
||||
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<ClickTarget>) {
|
||||
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<S: LaneSource>(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<S: LaneSource>(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::<Vector>::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<NodeId>) {
|
||||
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::<EditorLayerPath>(0);
|
||||
let layer = layer_path.last().copied();
|
||||
let transform: DAffine2 = list.attr::<Transform>(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::<Transform>(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::<Transform>(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::<Transform>(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::<Transform>(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::<Transform>(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<ClickTarget>) {
|
||||
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<ClickTarget>) {
|
||||
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::<Graphic>::new(item) {
|
||||
render_graphic_svg_with(&run, reach.into_group_graphics(), render, render_params)
|
||||
} else if let Some(run) = RunView::<Vector>::new(item) {
|
||||
match reach.applies() {
|
||||
true => render_vector_svg(&PaintOverlay::new(&run, reach.paint), render, render_params),
|
||||
false => render_vector_svg(&run, render, render_params),
|
||||
}
|
||||
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
|
||||
render_raster_cpu_svg(&run, render, render_params)
|
||||
} else if item.typed_lanes::<Raster<GPU>>().is_some() {
|
||||
} else if let Some(run) = RunView::<Color>::new(item) {
|
||||
render_color_svg(&run, render, render_params)
|
||||
} else if let Some(run) = RunView::<GradientStops>::new(item) {
|
||||
render_gradient_svg(&run, render, render_params)
|
||||
} else if let Some(run) = RunView::<String>::new(item) {
|
||||
render_text_svg(&run, render, render_params)
|
||||
} 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::<Graphic>::new(item) {
|
||||
render_graphic_vello_with(&run, reach.into_group_graphics(), scene, transform, context, render_params)
|
||||
} else if let Some(run) = RunView::<Vector>::new(item) {
|
||||
match reach.applies() {
|
||||
true => render_vector_vello(&PaintOverlay::new(&run, reach.paint), scene, transform, context, render_params),
|
||||
false => render_vector_vello(&run, scene, transform, context, render_params),
|
||||
}
|
||||
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
|
||||
render_raster_cpu_vello(&run, scene, transform, render_params)
|
||||
} else if let Some(run) = RunView::<Raster<GPU>>::new(item) {
|
||||
render_raster_gpu_vello(&run, scene, transform, context, render_params)
|
||||
} else if let Some(run) = RunView::<Color>::new(item) {
|
||||
render_color_vello(&run, scene, render_params)
|
||||
} else if let Some(run) = RunView::<GradientStops>::new(item) {
|
||||
render_gradient_vello(&run, scene, transform, render_params)
|
||||
} else if let Some(run) = RunView::<String>::new(item) {
|
||||
render_text_vello(&run, scene, transform, render_params)
|
||||
} 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<NodeId>) {
|
||||
fn typed_run<S: LaneSource>(
|
||||
source: &S,
|
||||
metadata: &mut RenderMetadata,
|
||||
footprint: Footprint,
|
||||
element_id: Option<NodeId>,
|
||||
collect: impl Fn(&S, &mut RenderMetadata, Footprint, Option<NodeId>),
|
||||
click: impl Fn(&S, &mut Vec<ClickTarget>),
|
||||
outline: impl Fn(&S, &mut Vec<ClickTarget>),
|
||||
) {
|
||||
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::<Graphic>::new(item) {
|
||||
collect_graphic_metadata_with(&run, reach.into_group_graphics(), metadata, footprint, element_id)
|
||||
} else if let Some(run) = RunView::<Vector>::new(item) {
|
||||
match reach.applies() {
|
||||
true => 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::<Raster<CPU>>::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::<Raster<GPU>>::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::<Color>::new(item) {
|
||||
typed_run(&run, metadata, footprint, element_id, |_, _, _, _| (), |_, _| (), |_, _| ())
|
||||
} else if let Some(run) = RunView::<GradientStops>::new(item) {
|
||||
typed_run(&run, metadata, footprint, element_id, |_, _, _, _| (), |_, _| (), |_, _| ())
|
||||
} else if let Some(run) = RunView::<String>::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<ClickTarget>) {
|
||||
let GroupContent::Run(item) = &group.content else {
|
||||
return group_to_legacy_list(group).add_upstream_click_targets(click_targets);
|
||||
};
|
||||
if let Some(run) = RunView::<Graphic>::new(item) {
|
||||
add_graphic_upstream_click_targets_with(&run, reach.into_group_graphics(), click_targets)
|
||||
} else if let Some(run) = RunView::<Vector>::new(item) {
|
||||
match reach.applies() {
|
||||
true => add_vector_upstream_click_targets(&PaintOverlay::new(&run, reach.paint), click_targets),
|
||||
false => add_vector_upstream_click_targets(&run, click_targets),
|
||||
}
|
||||
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
|
||||
add_raster_upstream_click_targets(click_targets)
|
||||
} else if let Some(run) = RunView::<String>::new(item) {
|
||||
add_text_upstream_click_targets(&run, click_targets)
|
||||
} else if item.typed_lanes::<Color>().is_some() || item.typed_lanes::<GradientStops>().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<ClickTarget>) {
|
||||
let GroupContent::Run(item) = &group.content else {
|
||||
return group_to_legacy_list(group).add_upstream_outline_targets(outlines);
|
||||
};
|
||||
if let Some(run) = RunView::<Graphic>::new(item) {
|
||||
add_graphic_upstream_outline_targets_with(&run, reach.into_group_graphics(), outlines)
|
||||
} else if let Some(run) = RunView::<Vector>::new(item) {
|
||||
match reach.applies() {
|
||||
true => add_vector_upstream_outline_targets(&PaintOverlay::new(&run, reach.paint), outlines),
|
||||
false => add_vector_upstream_outline_targets(&run, outlines),
|
||||
}
|
||||
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
|
||||
add_raster_upstream_click_targets(outlines)
|
||||
} else if let Some(run) = RunView::<String>::new(item) {
|
||||
add_text_upstream_click_targets(&run, outlines)
|
||||
} else if item.typed_lanes::<Color>().is_some() || item.typed_lanes::<GradientStops>().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<S: LaneSource>(source: &S, index: usize) -> (DVec2, DVec2, Color, bool) {
|
||||
let location: DVec2 = source.attr::<Location>(index);
|
||||
@@ -857,6 +1109,10 @@ impl Render for List<Artboard> {
|
||||
}
|
||||
|
||||
fn render_graphic_svg<S: LaneSource<Element = Graphic>>(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<Element = Graphic>>(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<S: LaneSource<Element = Graphic>>(source: &S, render: &mut
|
||||
let opacity_attr: f64 = source.attr::<Opacity>(index);
|
||||
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
|
||||
let element = source.element(index).unwrap();
|
||||
let reach = inherited.for_lane(source, index);
|
||||
|
||||
render.parent_tag(
|
||||
"g",
|
||||
@@ -887,10 +1144,10 @@ fn render_graphic_svg<S: LaneSource<Element = Graphic>>(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<S: LaneSource<Element = Graphic>>(source: &S, render: &mut
|
||||
}
|
||||
},
|
||||
|render| {
|
||||
element.render_svg(render, render_params);
|
||||
render_element_svg(element, reach, render, render_params);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_graphic_vello<S: LaneSource<Element = Graphic>>(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<Element = Graphic>>(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<S: LaneSource<Element = Graphic>>(source: &S, scene: &mu
|
||||
let opacity_attr: f64 = source.attr::<Opacity>(index);
|
||||
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
|
||||
let element = source.element(index).unwrap();
|
||||
let reach = inherited.for_lane(source, index);
|
||||
|
||||
let mut layer = false;
|
||||
|
||||
@@ -949,10 +1211,10 @@ fn render_graphic_vello<S: LaneSource<Element = Graphic>>(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<S: LaneSource<Element = Graphic>>(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<S: LaneSource<Element = Graphic>>(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<S: LaneSource<Element = Graphic>>(source: &S, scene: &mu
|
||||
}
|
||||
|
||||
fn collect_graphic_metadata<S: LaneSource<Element = Graphic>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
||||
collect_graphic_metadata_with(source, PaintReach::NONE, metadata, footprint, element_id)
|
||||
}
|
||||
|
||||
fn collect_graphic_metadata_with<'a, S: LaneSource<Element = Graphic>>(source: &'a S, inherited: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
||||
for index in 0..source.lane_count() {
|
||||
let item_transform: DAffine2 = source.attr::<Transform>(index);
|
||||
let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
|
||||
let layer = layer_path.last().copied();
|
||||
let element = source.element(index).unwrap();
|
||||
let reach = inherited.for_lane(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<S: LaneSource<Element = Graphic>>(source: &S, metada
|
||||
for index in 0..source.lane_count() {
|
||||
let item_transform: DAffine2 = source.attr::<Transform>(index);
|
||||
let element = source.element(index).unwrap();
|
||||
let reach = inherited.for_lane(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<S: LaneSource<Element = Graphic>>(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<S: LaneSource<Element = Graphic>>(source: &S, metada
|
||||
}
|
||||
|
||||
fn add_graphic_upstream_click_targets<S: LaneSource<Element = Graphic>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
|
||||
add_graphic_upstream_click_targets_with(source, PaintReach::NONE, click_targets)
|
||||
}
|
||||
|
||||
fn add_graphic_upstream_click_targets_with<'a, S: LaneSource<Element = Graphic>>(source: &'a S, inherited: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) {
|
||||
for index in 0..source.lane_count() {
|
||||
let item_transform: DAffine2 = source.attr::<Transform>(index);
|
||||
let element = source.element(index).unwrap();
|
||||
let reach = inherited.for_lane(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<S: LaneSource<Element = Graphic>>(source:
|
||||
}
|
||||
|
||||
fn add_graphic_upstream_outline_targets<S: LaneSource<Element = Graphic>>(source: &S, outlines: &mut Vec<ClickTarget>) {
|
||||
add_graphic_upstream_outline_targets_with(source, PaintReach::NONE, outlines)
|
||||
}
|
||||
|
||||
fn add_graphic_upstream_outline_targets_with<'a, S: LaneSource<Element = Graphic>>(source: &'a S, inherited: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) {
|
||||
for index in 0..source.lane_count() {
|
||||
let item_transform: DAffine2 = source.attr::<Transform>(index);
|
||||
let element = source.element(index).unwrap();
|
||||
let reach = inherited.for_lane(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<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
|
||||
};
|
||||
|
||||
let fill_graphic_list = paint_graphics::<Fill, _>(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::<Stroke, _>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(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<S: LaneSource<Element = Vector>>(source: &S, metadata
|
||||
e.insert(Arc::new(element.clone()));
|
||||
|
||||
if let Some(fill_graphic) = paint_graphics::<Fill, _>(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::<Stroke, _>(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::<PointId>::new_rectangle(corner, corner + DVec2::ONE))
|
||||
}
|
||||
|
||||
fn color_paint() -> List<Graphic> {
|
||||
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<T>(layout: &Layout, elements: &[&T], fill: &[Option<&List<Graphic>>]) -> Vec<u8> {
|
||||
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::<Option<&List<Graphic>>>().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::<Vector>(), &[FieldWrite::of::<Fill>(0)]);
|
||||
// SAFETY: the layout carries a parked vector element and the fill field.
|
||||
let bytes = unsafe { write_lanes::<Vector>(&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::<Graphic>(), &[FieldWrite::of::<Fill>(0)]);
|
||||
// SAFETY: the layout carries a parked graphic element and the fill field.
|
||||
let bytes = unsafe { write_lanes::<Graphic>(&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::<Vector>(), &[FieldWrite::of::<Fill>(0)]);
|
||||
// SAFETY: the layout carries a parked vector element and the fill field.
|
||||
let bytes = unsafe { write_lanes::<Vector>(&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::<Vector>(), &[FieldWrite::of::<Fill>(0)]);
|
||||
// SAFETY: the layout carries a parked vector element and the fill field.
|
||||
let bytes = unsafe { write_lanes::<Vector>(&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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user