Read render attributes through census markers instead of string keys

This commit is contained in:
Dennis Kobert
2026-08-26 14:55:27 +00:00
parent eb11eec5bb
commit e5b1fad675
7 changed files with 283 additions and 137 deletions

View File

@@ -35,6 +35,11 @@ pub trait Attribute: 'static {
Default::default()
}
/// Borrows the value out of legacy list storage, whose stored form is the
/// owned clone [`Self::read_erased`] produces. `None` where the column is
/// absent or holds another type.
fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option<Self::Value<'a>>;
/// # Safety
/// `ptr` must point at a live field of this marker's value type.
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue>;
@@ -178,6 +183,12 @@ macro_rules! attribute {
const NAME: &'static str = $name;
type Value<'e> = ::core::option::Option<&'e $value>;
fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option<Self::Value<'a>> {
stored
.downcast_ref::<::core::option::Option<<$value as ::std::borrow::ToOwned>::Owned>>()
.map(|owned| owned.as_ref().map(::std::borrow::Borrow::borrow))
}
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::std::boxed::Box::new(unsafe { ptr.cast::<::core::option::Option<&$value>>().read() }.map(|value| <$value as ::std::borrow::ToOwned>::to_owned(value)))
}
@@ -216,6 +227,10 @@ macro_rules! attribute {
}
)?
fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option<Self::Value<'a>> {
stored.downcast_ref::<<$value as ::std::borrow::ToOwned>::Owned>().map(::std::borrow::Borrow::borrow)
}
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::std::boxed::Box::new(unsafe { ptr.cast::<&$value>().read() }.to_owned())
}
@@ -247,6 +262,10 @@ macro_rules! attribute {
}
)?
fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option<Self::Value<'a>> {
stored.downcast_ref::<$value>().copied()
}
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
::std::boxed::Box::new(unsafe { ptr.cast::<$value>().read() })
}
@@ -373,6 +392,10 @@ mod tests {
const NAME: &'static str = "opacity";
type Value<'e> = bool;
fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option<Self::Value<'a>> {
stored.downcast_ref::<bool>().copied()
}
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue> {
Box::new(unsafe { ptr.cast::<bool>().read() })
}

View File

@@ -0,0 +1,75 @@
//! The read surface over a source of lanes, so readers name census markers
//! instead of a storage shape.
use crate::attribute::Attribute;
/// One marker's column on a source, resolved once so lane reads skip the key
/// lookup.
pub trait LaneColumn<'a, A: Attribute> {
/// The lane's value, or the marker's census default where the column is
/// absent.
fn get(&self, lane: usize) -> A::Value<'a>;
}
/// A source of lanes carrying an element and census attributes.
pub trait LaneSource {
type Element;
type Column<'a, A: Attribute>: LaneColumn<'a, A>
where
Self: 'a;
fn lane_count(&self) -> usize;
fn element(&self, lane: usize) -> Option<&Self::Element>;
fn column<A: Attribute>(&self) -> Self::Column<'_, A>;
fn attr<A: Attribute>(&self, lane: usize) -> A::Value<'_> {
self.column::<A>().get(lane)
}
}
#[cfg(test)]
mod tests {
use super::LaneSource;
use crate::attribute::{EditorLayerPath, Opacity, Transform};
use crate::list::List;
use crate::uuid::NodeId;
use glam::DAffine2;
#[test]
fn a_plain_marker_reads_what_the_legacy_column_stores() {
let mut list = List::new_from_element(1u32);
let transform = DAffine2::from_translation((3., 4.).into());
list.set_attribute(crate::ATTR_TRANSFORM, 0, transform);
assert_eq!(list.attr::<Transform>(0), transform);
}
#[test]
fn an_absent_marker_reads_its_census_default_not_the_value_default() {
let list = List::new_from_element(1u32);
// `Opacity` declares `= 1.`, so the census default must win over `f64::default()`.
assert_eq!(list.attr::<Opacity>(0), 1.);
assert_eq!(list.attr::<Transform>(0), DAffine2::IDENTITY);
}
#[test]
fn a_reference_marker_borrows_the_stored_owned_form() {
let mut list = List::new_from_element(1u32);
let path = vec![NodeId(7), NodeId(9)];
list.set_attribute(crate::ATTR_EDITOR_LAYER_PATH, 0, path.clone());
assert_eq!(list.attr::<EditorLayerPath>(0), path.as_slice());
assert!(List::new_from_element(1u32).attr::<EditorLayerPath>(0).is_empty());
}
#[test]
fn a_column_of_the_wrong_stored_type_reads_as_absent() {
let mut list = List::new_from_element(1u32);
list.set_attribute(crate::ATTR_OPACITY, 0, "not an f64".to_string());
assert_eq!(list.attr::<Opacity>(0), 1.);
}
}

View File

@@ -8,6 +8,7 @@ pub mod context;
pub mod extent;
pub mod frame_table;
pub mod gpoll;
pub mod lane;
pub mod list;
pub mod math;
pub mod memo;

View File

@@ -738,6 +738,11 @@ impl Attributes {
.find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::<T>() } else { None })
}
/// The whole column for the given key, resolved once for repeated lane reads.
fn column(&self, key: &str) -> Option<&dyn AnyAttribute> {
self.attributes.iter().find_map(|(k, attribute)| (k == key).then_some(&**attribute))
}
/// Removes the entire attribute for the given key, if present.
fn remove_attribute(&mut self, key: &str) {
if let Some(position) = self.attributes.iter().position(|(k, _)| k == key) {
@@ -1168,6 +1173,41 @@ impl<T> Default for List<T> {
}
}
/// A marker's column on a [`List`], absent when the list carries no such key.
pub struct ListColumn<'a, A: crate::attribute::Attribute> {
stored: Option<&'a dyn AnyAttribute>,
marker: std::marker::PhantomData<A>,
}
impl<'a, A: crate::attribute::Attribute> crate::lane::LaneColumn<'a, A> for ListColumn<'a, A> {
fn get(&self, lane: usize) -> A::Value<'a> {
self.stored.and_then(|column| column.get_any(lane)).and_then(A::from_stored).unwrap_or_else(A::default)
}
}
impl<T> crate::lane::LaneSource for List<T> {
type Element = T;
type Column<'a, A: crate::attribute::Attribute>
= ListColumn<'a, A>
where
Self: 'a;
fn lane_count(&self) -> usize {
List::len(self)
}
fn element(&self, lane: usize) -> Option<&T> {
List::element(self, lane)
}
fn column<A: crate::attribute::Attribute>(&self) -> ListColumn<'_, A> {
ListColumn {
stored: self.attributes.column(A::NAME),
marker: std::marker::PhantomData,
}
}
}
impl<T: CacheHash> CacheHash for List<T> {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.element.cache_hash(state);

View File

@@ -42,4 +42,16 @@ mod tests {
fn an_absent_paint_defaults_to_none() {
assert_eq!(<Fill as Attribute>::default(), None);
}
#[test]
fn a_paint_marker_reads_back_what_the_paint_writer_stored() {
use core_types::lane::LaneSource;
let paint = List::new_from_element(Graphic::default());
let mut list = List::new_from_element(Graphic::default());
crate::graphic::set_paint_attribute_at(&mut list, 0, ATTR_FILL, paint.clone());
assert_eq!(list.attr::<Fill>(0), Some(&paint));
assert_eq!(list.attr::<Stroke>(0), None);
}
}

View File

@@ -1,16 +1,19 @@
use crate::renderer::{RenderParams, format_transform_matrix, gradient_placement, transform_is_invertible};
use crate::{Render, RenderSvgSegmentList, SvgRender};
use core_types::Color;
use core_types::attribute::Transform;
use core_types::color::SRGBA8;
use core_types::lane::LaneSource;
use core_types::list::List;
use core_types::uuid::generate_uuid;
use core_types::{ATTR_TRANSFORM, Color};
use glam::{DAffine2, DVec2};
use graphic_types::Graphic;
use graphic_types::vector_types::gradient::GradientType;
use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod};
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write;
use vector_types::GradientStops;
use vector_types::gradient::GradientSpreadMethod;
use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, GradientStops};
#[derive(Copy, Clone, PartialEq)]
pub enum PaintTarget {
@@ -93,9 +96,9 @@ impl RenderExt for List<GradientStops> {
let mut stop = String::new();
let Some(stops) = self.element(0) else { return 0 };
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
let gradient_type: GradientType = self.attr::<GradientTypeAttr>(0);
let local_gradient_transform: DAffine2 = self.attr::<Transform>(0);
let spread_method: GradientSpreadMethod = self.attr::<SpreadMethod>(0);
for (position, color, original_midpoint) in stops.interpolated_samples() {
stop.push_str("<stop");

View File

@@ -1,32 +1,35 @@
use crate::render_ext::{PaintTarget, RenderExt};
use crate::to_peniko::{BlendModeExt, ToPenikoColor};
use core_types::CacheHash;
use core_types::attribute::{
Background as BackgroundAttr, BlendMode as BlendModeAttr, Clip, ClippingMask, Dimensions, EditorLayerPath, FontSize, LetterSpacing, LetterTilt, LineHeight, Location, MaxHeight, MaxWidth, Opacity,
OpacityFill, Transform,
};
use core_types::blending::BlendMode;
use core_types::bounds::BoundingBox;
use core_types::bounds::RenderBoundingBox;
use core_types::color::Color;
use core_types::color::SRGBA8;
use core_types::consts::DEFAULT_FONT_SIZE;
use core_types::lane::LaneSource;
use core_types::list::{Item, List};
use core_types::math::quad::Quad;
use core_types::render_complexity::RenderComplexity;
use core_types::transform::Footprint;
use core_types::uuid::{NodeId, generate_uuid};
use core_types::{
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT,
ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM,
};
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_TEXT_FRAME, ATTR_TRANSFORM};
use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource;
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
use graphic_types::markers::EditorMergedLayers;
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod};
use graphic_types::vector_types::subpath::Subpath;
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Artboard, Graphic, Vector};
use graphic_types::{ATTR_FILL, ATTR_STROKE, Artboard, Graphic, Vector};
use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts};
use num_traits::Zero;
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
@@ -38,7 +41,7 @@ use std::fmt::Write;
use std::hash::Hash;
use std::ops::Deref;
use std::sync::{Arc, LazyLock};
use text_nodes::{ATTR_FONT, ATTR_TEXT_ALIGN};
use text_nodes::markers::{Font, TextAlign};
use vector_types::gradient::GradientSpreadMethod;
use vector_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD};
use vello::*;
@@ -398,9 +401,9 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientTyp
fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
let stops = gradient_list.element(0)?;
let gradient_type: GradientType = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let spread_method: GradientSpreadMethod = gradient_list.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
let gradient_type: GradientType = gradient_list.attr::<GradientTypeAttr>(0);
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
let spread_method: GradientSpreadMethod = gradient_list.attr::<SpreadMethod>(0);
let mut peniko_stops = peniko::ColorStops::new();
for (position, color, _) in stops.interpolated_samples() {
@@ -584,9 +587,9 @@ impl Render for Graphic {
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: Vec<NodeId> = list.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0);
let layer_path: &[NodeId] = list.attr::<EditorLayerPath>(0);
let layer = layer_path.last().copied();
let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let transform: DAffine2 = list.attr::<Transform>(0);
metadata.first_element_source_id.insert(element_id, layer);
metadata.local_transforms.insert(element_id, transform);
@@ -597,7 +600,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item
if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
metadata.local_transforms.insert(element_id, list.attr::<Transform>(0));
}
}
Graphic::RasterGPU(list) => {
@@ -605,7 +608,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item
if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
metadata.local_transforms.insert(element_id, list.attr::<Transform>(0));
}
}
Graphic::Color(list) => {
@@ -613,7 +616,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item
if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
metadata.local_transforms.insert(element_id, list.attr::<Transform>(0));
}
}
Graphic::Gradient(list) => {
@@ -621,7 +624,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item
if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
metadata.local_transforms.insert(element_id, list.attr::<Transform>(0));
}
}
Graphic::Text(list) => {
@@ -629,7 +632,7 @@ impl Render for Graphic {
// TODO: Find a way to handle more than the first item
if !list.is_empty() {
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
metadata.local_transforms.insert(element_id, list.attr::<Transform>(0));
}
}
}
@@ -702,10 +705,10 @@ impl Render for Graphic {
/// Reads the artboard metadata for the item at `index` from a `List<Artboard>`.
fn read_artboard_attributes(list: &List<Artboard>, index: usize) -> (DVec2, DVec2, Color, bool) {
let location: DVec2 = list.attribute_cloned_or_default(ATTR_LOCATION, index);
let dimensions: DVec2 = list.attribute_cloned_or_default(ATTR_DIMENSIONS, index);
let background: Color = list.attribute_cloned_or_default(ATTR_BACKGROUND, index);
let clip: bool = list.attribute_cloned_or_default(ATTR_CLIP, index);
let location: DVec2 = list.attr::<Location>(index);
let dimensions: DVec2 = list.attr::<Dimensions>(index);
let background: Color = list.attr::<BackgroundAttr>(index);
let clip: bool = list.attr::<Clip>(index);
(location, dimensions, background, clip)
}
@@ -803,7 +806,7 @@ impl Render for List<Artboard> {
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue };
let (location, dimensions, _background, clip) = read_artboard_attributes(self, index);
let layer_path: Vec<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
let layer_path: &[NodeId] = self.attr::<EditorLayerPath>(index);
let element_id = layer_path.last().copied();
if let Some(element_id) = element_id {
@@ -826,7 +829,7 @@ impl Render for List<Artboard> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() {
let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index);
let dimensions: DVec2 = self.attr::<Dimensions>(index);
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions);
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
}
@@ -842,10 +845,10 @@ impl Render for List<Graphic> {
let mut mask_state = None;
for index in 0..self.len() {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let transform: DAffine2 = self.attr::<Transform>(index);
let blend_mode: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let element = self.element(index).unwrap();
render.parent_tag(
@@ -898,11 +901,11 @@ impl Render for List<Graphic> {
let mut mask_element_and_transform = None;
for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let item_transform: DAffine2 = self.attr::<Transform>(index);
let transform = transform * item_transform;
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let element = self.element(index).unwrap();
let mut layer = false;
@@ -974,8 +977,8 @@ impl Render for List<Graphic> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let layer_path: Vec<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
let item_transform: DAffine2 = self.attr::<Transform>(index);
let layer_path: &[NodeId] = self.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied();
let element = self.element(index).unwrap();
@@ -995,7 +998,7 @@ impl Render for List<Graphic> {
let mut all_upstream_outlines = Vec::new();
for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let item_transform: DAffine2 = self.attr::<Transform>(index);
let element = self.element(index).unwrap();
let mut new_click_targets = Vec::new();
@@ -1022,7 +1025,7 @@ impl Render for List<Graphic> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let item_transform: DAffine2 = self.attr::<Transform>(index);
let element = self.element(index).unwrap();
let mut new_click_targets = Vec::new();
@@ -1038,7 +1041,7 @@ impl Render for List<Graphic> {
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
for index in 0..self.len() {
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let item_transform: DAffine2 = self.attr::<Transform>(index);
let element = self.element(index).unwrap();
let mut new_outlines = Vec::new();
@@ -1068,10 +1071,10 @@ impl Render for List<Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue };
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let item_transform: DAffine2 = self.attr::<Transform>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
@@ -1284,10 +1287,10 @@ impl Render for List<Vector> {
use graphic_types::vector_types::vector;
let Some(element) = self.element(index) else { continue };
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let item_transform: DAffine2 = self.attr::<Transform>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let multiplied_transform = parent_transform * item_transform;
let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
@@ -1559,11 +1562,7 @@ impl Render for List<Vector> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
// Aggregate all items' targets per element_id so multi-item lists (e.g. 'Text' node with "Separate Glyphs" active) produce hit areas for every glyph.
// Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`.
let item_zero_transform: DAffine2 = if !self.is_empty() {
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
} else {
DAffine2::IDENTITY
};
let item_zero_transform: DAffine2 = if !self.is_empty() { self.attr::<Transform>(0) } else { DAffine2::IDENTITY };
let item_zero_inverse = if transform_is_invertible(item_zero_transform) {
item_zero_transform.inverse()
} else {
@@ -1575,8 +1574,8 @@ impl Render for List<Vector> {
for index in 0..self.len() {
let Some(source) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let layer_path: Vec<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
let transform: DAffine2 = self.attr::<Transform>(index);
let layer_path: &[NodeId] = self.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied();
if let Some(element_id) = caller_element_id.or(layer) {
@@ -1626,8 +1625,7 @@ impl Render for List<Vector> {
// If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation,
// Flatten Path, Morph, or any other destructive merge), recurse into that snapshot so the editor can
// surface the original child layers' click targets.
let upstream_nested_layers = self.attribute_cloned_or_default::<Option<List<Graphic>>>(ATTR_EDITOR_MERGED_LAYERS, index).unwrap_or_default();
if !upstream_nested_layers.is_empty() {
if let Some(upstream_nested_layers) = self.attr::<EditorMergedLayers>(index).filter(|layers| !layers.is_empty()) {
let mut upstream_footprint = footprint;
upstream_footprint.transform *= transform;
upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None);
@@ -1646,7 +1644,7 @@ impl Render for List<Vector> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() {
let Some(source) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let transform: DAffine2 = self.attr::<Transform>(index);
// Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes)
let vector = self.attribute::<Vector>(ATTR_EDITOR_CLICK_TARGET, index).unwrap_or(source);
@@ -1659,7 +1657,7 @@ impl Render for List<Vector> {
// Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms
for index in 0..self.len() {
let Some(source) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let transform: DAffine2 = self.attr::<Transform>(index);
extend_targets_from_vector(outlines, self, index, source, transform);
}
@@ -1731,10 +1729,10 @@ impl Render for List<Raster<CPU>> {
for index in 0..self.len() {
let Some(image) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let transform: DAffine2 = self.attr::<Transform>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
if image.data.is_empty() {
continue;
@@ -1818,9 +1816,9 @@ impl Render for List<Raster<CPU>> {
continue;
}
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
@@ -1835,7 +1833,7 @@ impl Render for List<Raster<CPU>> {
layer = true;
}
let transform_attribute: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let transform_attribute: DAffine2 = self.attr::<Transform>(index);
if let RenderMode::Outline = render_params.render_mode {
let outline_transform: DAffine2 = transform * transform_attribute;
@@ -1875,7 +1873,7 @@ impl Render for List<Raster<CPU>> {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one item of the `List<Raster<...>>`
if !self.is_empty() {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let transform: DAffine2 = self.attr::<Transform>(0);
metadata.local_transforms.insert(element_id, transform);
// If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize,
@@ -1884,8 +1882,7 @@ impl Render for List<Raster<CPU>> {
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization
// area, so the children are already in the coordinate space matching `footprint` here — we must NOT
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
let upstream_nested_layers = self.attribute_cloned_or_default::<Option<List<Graphic>>>(ATTR_EDITOR_MERGED_LAYERS, 0).unwrap_or_default();
if !upstream_nested_layers.is_empty() {
if let Some(upstream_nested_layers) = self.attr::<EditorMergedLayers>(0).filter(|layers| !layers.is_empty()) {
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}
}
@@ -1907,10 +1904,10 @@ impl Render for List<Raster<GPU>> {
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for index in 0..self.len() {
let Some(raster) = self.element(index) else { continue };
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let clip_attr: bool = self.attribute_cloned_or_default(ATTR_CLIPPING_MASK, index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let clip_attr: bool = self.attr::<ClippingMask>(index);
let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal,
_ => blend_mode_attr.to_peniko(),
@@ -1929,7 +1926,7 @@ impl Render for List<Raster<GPU>> {
layer = true;
}
let transform_attribute: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let transform_attribute: DAffine2 = self.attr::<Transform>(index);
if let RenderMode::Outline = render_params.render_mode {
let outline_transform = transform * transform_attribute;
@@ -1970,7 +1967,7 @@ impl Render for List<Raster<GPU>> {
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one item of the `List<Raster<...>>`
if !self.is_empty() {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let transform: DAffine2 = self.attr::<Transform>(0);
metadata.local_transforms.insert(element_id, transform);
// If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize,
@@ -1979,8 +1976,7 @@ impl Render for List<Raster<GPU>> {
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization
// area, so the children are already in the coordinate space matching `footprint` here — we must NOT
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
let upstream_nested_layers = self.attribute_cloned_or_default::<Option<List<Graphic>>>(ATTR_EDITOR_MERGED_LAYERS, 0).unwrap_or_default();
if !upstream_nested_layers.is_empty() {
if let Some(upstream_nested_layers) = self.attr::<EditorMergedLayers>(0).filter(|layers| !layers.is_empty()) {
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}
}
@@ -2001,9 +1997,9 @@ impl Render for List<Raster<GPU>> {
impl Render for List<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for (index, color) in self.iter_element_values().enumerate() {
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let blend_mode: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
render.leaf_tag("polyline", |attributes| {
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
@@ -2032,9 +2028,9 @@ impl Render for List<Color> {
use vello::peniko;
for (index, color) in self.iter_element_values().enumerate() {
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
@@ -2072,12 +2068,12 @@ impl Render for List<GradientStops> {
for index in 0..self.len() {
let Some(gradient) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, index);
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index);
let transform: DAffine2 = self.attr::<Transform>(index);
let blend_mode: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let spread_method: GradientSpreadMethod = self.attr::<SpreadMethod>(index);
let gradient_type: GradientType = self.attr::<GradientTypeAttr>(index);
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
render.leaf_tag(tag, |attributes| {
if let Some((min, size)) = thumbnail_rect {
@@ -2164,10 +2160,10 @@ impl Render for List<GradientStops> {
.zip(self.iter_attribute_values_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD))
.zip(self.iter_attribute_values_or_default::<GradientType>(ATTR_GRADIENT_TYPE))
{
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let transform: DAffine2 = self.attr::<Transform>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let gradient_transform = parent_transform * transform;
let blend_mode = blend_mode_attr.to_peniko();
@@ -2315,16 +2311,16 @@ fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, ()>, x_offset: f3
fn text_item_size_and_transform(list: &List<String>, index: usize) -> Option<(DVec2, DAffine2)> {
let text = list.element(index)?;
let font: Resource = {
let f: Resource = list.attribute_cloned_or_default(ATTR_FONT, index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
let f = list.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
};
let font_size: f64 = list.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE);
let line_height: f64 = list.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2);
let letter_spacing: f64 = list.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.);
let max_width: Option<f64> = list.attribute_cloned_or(ATTR_MAX_WIDTH, index, None);
let max_height: Option<f64> = list.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None);
let align: text_nodes::TextAlign = list.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index);
let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let font_size: f64 = list.attr::<FontSize>(index);
let line_height: f64 = list.attr::<LineHeight>(index);
let letter_spacing: f64 = list.attr::<LetterSpacing>(index);
let max_width: Option<f64> = list.attr::<MaxWidth>(index);
let max_height: Option<f64> = list.attr::<MaxHeight>(index);
let align: text_nodes::TextAlign = list.attr::<TextAlign>(index);
let transform: DAffine2 = list.attr::<Transform>(index);
let typesetting = text_nodes::TypesettingConfig {
font_size,
@@ -2376,7 +2372,7 @@ pub fn graphic_list_bounding_box(list: &List<Graphic>, transform: DAffine2) -> R
let mut any_infinite = false;
for index in 0..list.len() {
let item_transform = transform * list.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, index);
let item_transform = transform * list.attr::<Transform>(index);
let Some(graphic) = list.element(index) else { continue };
let bounds = match graphic {
Graphic::Text(text_list) => text_list_bounding_box(text_list, item_transform),
@@ -2410,21 +2406,21 @@ impl Render for List<String> {
continue;
}
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let transform: DAffine2 = self.attr::<Transform>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let font: Resource = {
let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
let f = self.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
};
let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE);
let line_height: f64 = self.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2);
let letter_spacing: f64 = self.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.);
let max_width: Option<f64> = self.attribute_cloned_or(ATTR_MAX_WIDTH, index, None);
let max_height: Option<f64> = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None);
let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.);
let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index);
let font_size: f64 = self.attr::<FontSize>(index);
let line_height: f64 = self.attr::<LineHeight>(index);
let letter_spacing: f64 = self.attr::<LetterSpacing>(index);
let max_width: Option<f64> = self.attr::<MaxWidth>(index);
let max_height: Option<f64> = self.attr::<MaxHeight>(index);
let letter_tilt: f64 = self.attr::<LetterTilt>(index);
let align: text_nodes::TextAlign = self.attr::<TextAlign>(index);
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let typesetting = text_nodes::TypesettingConfig {
@@ -2495,21 +2491,21 @@ impl Render for List<String> {
continue;
}
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let item_transform: DAffine2 = self.attr::<Transform>(index);
let font: Resource = {
let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f }
let f = self.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
};
let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE);
let line_height: f64 = self.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2);
let letter_spacing: f64 = self.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.);
let max_width: Option<f64> = self.attribute_cloned_or(ATTR_MAX_WIDTH, index, None);
let max_height: Option<f64> = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None);
let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.);
let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let font_size: f64 = self.attr::<FontSize>(index);
let line_height: f64 = self.attr::<LineHeight>(index);
let letter_spacing: f64 = self.attr::<LetterSpacing>(index);
let max_width: Option<f64> = self.attr::<MaxWidth>(index);
let max_height: Option<f64> = self.attr::<MaxHeight>(index);
let letter_tilt: f64 = self.attr::<LetterTilt>(index);
let align: text_nodes::TextAlign = self.attr::<TextAlign>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index);
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let typesetting = text_nodes::TypesettingConfig {
@@ -2559,11 +2555,7 @@ impl Render for List<String> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
// Click targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`.
let item_zero_transform: DAffine2 = if !self.is_empty() {
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
} else {
DAffine2::IDENTITY
};
let item_zero_transform: DAffine2 = if !self.is_empty() { self.attr::<Transform>(0) } else { DAffine2::IDENTITY };
let item_zero_inverse = if item_zero_transform.matrix2.determinant() != 0. {
item_zero_transform.inverse()
} else {
@@ -2573,7 +2565,7 @@ impl Render for List<String> {
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
for index in 0..self.len() {
let layer_path: Vec<NodeId> = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
let layer_path: &[NodeId] = self.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied();
let Some(element_id) = caller_element_id.or(layer) else { continue };