Drive every render body from the lane source

This commit is contained in:
Dennis Kobert
2026-08-26 17:47:50 +00:00
parent d9533de9ea
commit c7605d12a6
2 changed files with 1519 additions and 1450 deletions
@@ -200,21 +200,6 @@ pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty()) graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())
} }
/// Look up the paint graphics stored under attribute for a vector item, in the canonical `List<Graphic>` form.
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> {
list.attribute::<Option<List<Graphic>>>(attribute, index)
.and_then(|optional| optional.as_ref())
.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))
}
/// Whether the item carries a non-blank canonical `List<Graphic>` paint attribute,
/// checked by borrowing without cloning the renderable list.
pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool {
graphic_list_at(list, index, attribute).is_some()
}
/// Look up the paint graphics stored under the marker `A`, in the canonical `List<Graphic>` form. /// 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<Cow<'a, List<Graphic>>>
where where
+326 -242
View File
@@ -21,15 +21,15 @@ use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper; use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource; use graphene_resource::Resource;
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute}; use graphic_types::graphic::{has_paint, is_paint_present, paint_graphics, set_paint_attribute};
use graphic_types::markers::EditorMergedLayers; use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod}; use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod};
use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::subpath::Subpath;
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
use graphic_types::{ATTR_FILL, ATTR_STROKE, Artboard, Graphic, Vector}; use graphic_types::{ATTR_FILL, Artboard, Graphic, Vector};
use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts}; use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts};
use num_traits::Zero; use num_traits::Zero;
use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::instance::{LocationRef, NormalizedCoord, Size};
@@ -703,20 +703,19 @@ impl Render for Graphic {
} }
} }
/// Reads the artboard metadata for the item at `index` from a `List<Artboard>`. /// Reads the artboard metadata for the item at `index`.
fn read_artboard_attributes(list: &List<Artboard>, index: usize) -> (DVec2, DVec2, Color, bool) { fn read_artboard_attributes<S: LaneSource>(source: &S, index: usize) -> (DVec2, DVec2, Color, bool) {
let location: DVec2 = list.attr::<Location>(index); let location: DVec2 = source.attr::<Location>(index);
let dimensions: DVec2 = list.attr::<Dimensions>(index); let dimensions: DVec2 = source.attr::<Dimensions>(index);
let background: Color = list.attr::<BackgroundAttr>(index); let background: Color = source.attr::<BackgroundAttr>(index);
let clip: bool = list.attr::<Clip>(index); let clip: bool = source.attr::<Clip>(index);
(location, dimensions, background, clip) (location, dimensions, background, clip)
} }
impl Render for List<Artboard> { fn render_artboard_svg<S: LaneSource<Element = Artboard>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for index in 0..source.lane_count() {
for index in 0..self.len() { let Some(content) = source.element(index).map(Artboard::as_graphic_list) else { continue };
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue }; let (location, dimensions, background, clip) = read_artboard_attributes(source, index);
let (location, dimensions, background, clip) = read_artboard_attributes(self, index);
let x = location.x.min(location.x + dimensions.x); let x = location.x.min(location.x + dimensions.x);
let y = location.y.min(location.y + dimensions.y); let y = location.y.min(location.y + dimensions.y);
@@ -769,12 +768,12 @@ impl Render for List<Artboard> {
} }
} }
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_artboard_vello<S: LaneSource<Element = Artboard>>(source: &S, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
use vello::peniko; use vello::peniko;
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue }; let Some(content) = source.element(index).map(Artboard::as_graphic_list) else { continue };
let (location, dimensions, background, clip) = read_artboard_attributes(self, index); let (location, dimensions, background, clip) = read_artboard_attributes(source, index);
let [a, b] = [location, location + dimensions]; let [a, b] = [location, location + dimensions];
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)); let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
@@ -801,12 +800,12 @@ impl Render for List<Artboard> {
} }
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) { fn collect_artboard_metadata<S: LaneSource<Element = Artboard>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue }; let Some(content) = source.element(index).map(Artboard::as_graphic_list) else { continue };
let (location, dimensions, _background, clip) = read_artboard_attributes(self, index); let (location, dimensions, _background, clip) = read_artboard_attributes(source, index);
let layer_path: &[NodeId] = self.attr::<EditorLayerPath>(index); let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let element_id = layer_path.last().copied(); let element_id = layer_path.last().copied();
if let Some(element_id) = element_id { if let Some(element_id) = element_id {
@@ -827,29 +826,45 @@ impl Render for List<Artboard> {
} }
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_artboard_upstream_click_targets<S: LaneSource<Element = Artboard>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let dimensions: DVec2 = self.attr::<Dimensions>(index); let dimensions: DVec2 = source.attr::<Dimensions>(index);
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions); let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions);
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
} }
} }
impl Render for List<Artboard> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_artboard_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_artboard_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
collect_artboard_metadata(self, metadata, footprint)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_artboard_upstream_click_targets(self, click_targets)
}
fn contains_artboard(&self) -> bool { fn contains_artboard(&self) -> bool {
!self.is_empty() !self.is_empty()
} }
} }
impl Render for List<Graphic> { fn render_graphic_svg<S: LaneSource<Element = Graphic>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let mut mask_state = None; let mut mask_state = None;
for index in 0..self.len() { for index in 0..source.lane_count() {
let transform: DAffine2 = self.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let element = self.element(index).unwrap(); let element = source.element(index).unwrap();
render.parent_tag( render.parent_tag(
"g", "g",
@@ -868,7 +883,7 @@ impl Render for List<Graphic> {
attributes.push("style", blend_mode.render()); attributes.push("style", blend_mode.render());
} }
let next_clips = index + 1 < self.len() && self.element(index + 1).unwrap().had_clip_enabled(); let next_clips = index + 1 < source.lane_count() && source.element(index + 1).unwrap().had_clip_enabled();
if next_clips && mask_state.is_none() { if next_clips && mask_state.is_none() {
let uuid = generate_uuid(); let uuid = generate_uuid();
@@ -897,16 +912,16 @@ impl Render for List<Graphic> {
} }
} }
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_graphic_vello<S: LaneSource<Element = Graphic>>(source: &S, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
let mut mask_element_and_transform = None; let mut mask_element_and_transform = None;
for index in 0..self.len() { for index in 0..source.lane_count() {
let item_transform: DAffine2 = self.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let transform = transform * item_transform; let transform = transform * item_transform;
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let element = self.element(index).unwrap(); let element = source.element(index).unwrap();
let mut layer = false; let mut layer = false;
@@ -932,7 +947,7 @@ impl Render for List<Graphic> {
} }
} }
let next_clips = index + 1 < self.len() && self.element(index + 1).unwrap().had_clip_enabled(); 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() { if next_clips && mask_element_and_transform.is_none() {
mask_element_and_transform = Some((element, transform)); mask_element_and_transform = Some((element, transform));
@@ -975,12 +990,12 @@ impl Render for List<Graphic> {
} }
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_graphic_metadata<S: LaneSource<Element = Graphic>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let item_transform: DAffine2 = self.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let layer_path: &[NodeId] = self.attr::<EditorLayerPath>(index); let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied(); let layer = layer_path.last().copied();
let element = self.element(index).unwrap(); let element = source.element(index).unwrap();
let mut footprint = footprint; let mut footprint = footprint;
footprint.transform *= item_transform; footprint.transform *= item_transform;
@@ -997,9 +1012,9 @@ impl Render for List<Graphic> {
let mut all_upstream_click_targets = Vec::new(); let mut all_upstream_click_targets = Vec::new();
let mut all_upstream_outlines = Vec::new(); let mut all_upstream_outlines = Vec::new();
for index in 0..self.len() { for index in 0..source.lane_count() {
let item_transform: DAffine2 = self.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let element = self.element(index).unwrap(); let element = source.element(index).unwrap();
let mut new_click_targets = Vec::new(); let mut new_click_targets = Vec::new();
element.add_upstream_click_targets(&mut new_click_targets); element.add_upstream_click_targets(&mut new_click_targets);
@@ -1023,10 +1038,10 @@ impl Render for List<Graphic> {
} }
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_graphic_upstream_click_targets<S: LaneSource<Element = Graphic>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let item_transform: DAffine2 = self.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let element = self.element(index).unwrap(); let element = source.element(index).unwrap();
let mut new_click_targets = Vec::new(); let mut new_click_targets = Vec::new();
element.add_upstream_click_targets(&mut new_click_targets); element.add_upstream_click_targets(&mut new_click_targets);
@@ -1039,10 +1054,10 @@ impl Render for List<Graphic> {
} }
} }
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) { fn add_graphic_upstream_outline_targets<S: LaneSource<Element = Graphic>>(source: &S, outlines: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let item_transform: DAffine2 = self.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let element = self.element(index).unwrap(); let element = source.element(index).unwrap();
let mut new_outlines = Vec::new(); let mut new_outlines = Vec::new();
element.add_upstream_outline_targets(&mut new_outlines); element.add_upstream_outline_targets(&mut new_outlines);
@@ -1055,8 +1070,33 @@ impl Render for List<Graphic> {
} }
} }
fn graphic_contains_artboard<S: LaneSource<Element = Graphic>>(source: &S) -> bool {
(0..source.lane_count()).any(|index| source.element(index).is_some_and(|element| element.contains_artboard()))
}
impl Render for List<Graphic> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_graphic_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_graphic_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_graphic_metadata(self, metadata, footprint, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_graphic_upstream_click_targets(self, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_graphic_upstream_outline_targets(self, outlines)
}
fn contains_artboard(&self) -> bool { fn contains_artboard(&self) -> bool {
self.iter_element_values().any(|element| element.contains_artboard()) graphic_contains_artboard(self)
} }
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) { fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
@@ -1067,14 +1107,13 @@ impl Render for List<Graphic> {
} }
} }
impl Render for List<Vector> { fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for index in 0..source.lane_count() {
for index in 0..self.len() { let Some(vector) = source.element(index) else { continue };
let Some(vector) = self.element(index) else { continue }; let item_transform: DAffine2 = source.attr::<Transform>(index);
let item_transform: DAffine2 = self.attr::<Transform>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(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 // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
@@ -1103,10 +1142,10 @@ impl Render for List<Vector> {
MaskType::Mask MaskType::Mask
}; };
let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL); 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.as_ref().and_then(|l| l.element(0));
let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE); 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.as_ref().and_then(|l| l.element(0));
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
@@ -1282,15 +1321,15 @@ impl Render for List<Vector> {
} }
} }
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for index in 0..self.len() { for index in 0..source.lane_count() {
use graphic_types::vector_types::vector; use graphic_types::vector_types::vector;
let Some(element) = self.element(index) else { continue }; let Some(element) = source.element(index) else { continue };
let item_transform: DAffine2 = self.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let multiplied_transform = parent_transform * item_transform; let multiplied_transform = parent_transform * item_transform;
let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); 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)); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
@@ -1316,8 +1355,8 @@ impl Render for List<Vector> {
} }
} }
let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL); let fill_graphic_list = paint_graphics::<Fill, _>(source, index);
let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE); let stroke_graphic_list = paint_graphics::<Stroke, _>(source, index);
// If we're using opacity or a blend mode, we need to push a layer // If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.render_mode { let blend_mode = match render_params.render_mode {
@@ -1559,10 +1598,10 @@ impl Render for List<Vector> {
} }
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) { fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, 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. // 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]`. // 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.attr::<Transform>(0) } else { DAffine2::IDENTITY }; let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
let item_zero_inverse = if transform_is_invertible(item_zero_transform) { let item_zero_inverse = if transform_is_invertible(item_zero_transform) {
item_zero_transform.inverse() item_zero_transform.inverse()
} else { } else {
@@ -1572,10 +1611,10 @@ impl Render for List<Vector> {
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new(); let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
let mut accumulated_outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new(); let mut accumulated_outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some(source) = self.element(index) else { continue }; let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = self.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
let layer_path: &[NodeId] = self.attr::<EditorLayerPath>(index); let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied(); let layer = layer_path.last().copied();
if let Some(element_id) = caller_element_id.or(layer) { if let Some(element_id) = caller_element_id.or(layer) {
@@ -1588,17 +1627,17 @@ impl Render for List<Vector> {
} }
// Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes) // Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes)
let click_target_vector = self.attr::<EditorClickTarget>(index).unwrap_or(source); let click_target_vector = source.attr::<EditorClickTarget>(index).unwrap_or(element);
let item_relative_transform = item_zero_inverse * transform; let item_relative_transform = item_zero_inverse * transform;
let mut click_targets_unwrapped = Vec::new(); let mut click_targets_unwrapped = Vec::new();
extend_targets_from_vector(&mut click_targets_unwrapped, self, index, click_target_vector, item_relative_transform); extend_targets_from_vector(&mut click_targets_unwrapped, source, index, click_target_vector, item_relative_transform);
accumulated_click_targets.entry(element_id).or_default().extend(click_targets_unwrapped.into_iter().map(Arc::new)); accumulated_click_targets.entry(element_id).or_default().extend(click_targets_unwrapped.into_iter().map(Arc::new));
// Outlines always use source geometry so the visual outline reflects actual letterforms // Outlines always use source geometry so the visual outline reflects actual letterforms
let mut outlines_unwrapped = Vec::new(); let mut outlines_unwrapped = Vec::new();
extend_targets_from_vector(&mut outlines_unwrapped, self, index, source, item_relative_transform); extend_targets_from_vector(&mut outlines_unwrapped, source, index, element, item_relative_transform);
accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new)); accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new));
// Source geometry (not the click-target override) so editing tools work on letterforms. // Source geometry (not the click-target override) so editing tools work on letterforms.
@@ -1606,18 +1645,18 @@ impl Render for List<Vector> {
// Only item 0 is recorded since editing tools can only target a single item currently. // Only item 0 is recorded since editing tools can only target a single item currently.
// If that row has no paint attribute, none is recorded. // If that row has no paint attribute, none is recorded.
if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) { if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) {
e.insert(Arc::new(source.clone())); e.insert(Arc::new(element.clone()));
if let Some(fill_graphic) = graphic_list_at(self, index, ATTR_FILL) { 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.into_owned()));
} }
if let Some(stroke_graphic) = graphic_list_at(self, index, ATTR_STROKE) { 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.into_owned()));
} }
} }
// Surface `editor:text_frame` for the Text tool's drag cage // Surface `editor:text_frame` for the Text tool's drag cage
if let Some(frame) = self.try_attr::<EditorTextFrame>(index) { if let Some(frame) = source.try_attr::<EditorTextFrame>(index) {
metadata.text_frames.entry(element_id).or_insert(frame); metadata.text_frames.entry(element_id).or_insert(frame);
} }
} }
@@ -1625,7 +1664,7 @@ impl Render for List<Vector> {
// If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation, // 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 // Flatten Path, Morph, or any other destructive merge), recurse into that snapshot so the editor can
// surface the original child layers' click targets. // surface the original child layers' click targets.
if let Some(upstream_nested_layers) = self.attr::<EditorMergedLayers>(index).filter(|layers| !layers.is_empty()) { if let Some(upstream_nested_layers) = source.attr::<EditorMergedLayers>(index).filter(|layers| !layers.is_empty()) {
let mut upstream_footprint = footprint; let mut upstream_footprint = footprint;
upstream_footprint.transform *= transform; upstream_footprint.transform *= transform;
upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None); upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None);
@@ -1641,26 +1680,47 @@ impl Render for List<Vector> {
} }
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_vector_upstream_click_targets<S: LaneSource<Element = Vector>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some(source) = self.element(index) else { continue }; let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = self.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
// Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes) // Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes)
let vector = self.attr::<EditorClickTarget>(index).unwrap_or(source); let vector = source.attr::<EditorClickTarget>(index).unwrap_or(element);
extend_targets_from_vector(click_targets, self, index, vector, transform); extend_targets_from_vector(click_targets, source, index, vector, transform);
} }
} }
fn add_vector_upstream_outline_targets<S: LaneSource<Element = Vector>>(source: &S, outlines: &mut Vec<ClickTarget>) {
// Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms
for index in 0..source.lane_count() {
let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index);
extend_targets_from_vector(outlines, source, index, element, transform);
}
}
impl Render for List<Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_vector_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_vector_vello(self, scene, parent_transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_vector_metadata(self, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_vector_upstream_click_targets(self, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) { fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
// Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms add_vector_upstream_outline_targets(self, outlines)
for index in 0..self.len() {
let Some(source) = self.element(index) else { continue };
let transform: DAffine2 = self.attr::<Transform>(index);
extend_targets_from_vector(outlines, self, index, source, transform);
}
} }
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) { fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
@@ -1672,8 +1732,8 @@ impl Render for List<Vector> {
/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work /// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work
/// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append. /// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append.
fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, vector_list: &List<Vector>, index: usize, geometry: &Vector, transform: DAffine2) { fn extend_targets_from_vector<S: LaneSource<Element = Vector>>(targets: &mut Vec<ClickTarget>, source: &S, index: usize, geometry: &Vector, transform: DAffine2) {
let filled = has_paint_at(vector_list, index, ATTR_FILL); let filled = has_paint::<Fill, _>(source, index);
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect(); let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect();
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed()); let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed());
@@ -1724,15 +1784,14 @@ fn extend_free_point_targets(vector: &Vector, transform: DAffine2) -> impl Itera
}) })
} }
impl Render for List<Raster<CPU>> { fn render_raster_cpu_svg<S: LaneSource<Element = Raster<CPU>>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for index in 0..source.lane_count() {
for index in 0..self.len() { let Some(image) = source.element(index) else { continue };
let Some(image) = self.element(index) else { continue };
let transform: DAffine2 = self.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
if image.data.is_empty() { if image.data.is_empty() {
continue; continue;
@@ -1809,23 +1868,23 @@ impl Render for List<Raster<CPU>> {
} }
} }
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) { fn render_raster_cpu_vello<S: LaneSource<Element = Raster<CPU>> + BoundingBox>(source: &S, scene: &mut Scene, transform: DAffine2, render_params: &RenderParams) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some(image) = self.element(index) else { continue }; let Some(image) = source.element(index) else { continue };
if image.data.is_empty() { if image.data.is_empty() {
continue; continue;
} }
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let blend_mode = blend_mode_attr.to_peniko(); let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let mut layer = false; let mut layer = false;
if (opacity < 1. || (render_params.render_mode != RenderMode::Outline && blend_mode_attr != BlendMode::default())) if (opacity < 1. || (render_params.render_mode != RenderMode::Outline && blend_mode_attr != BlendMode::default()))
&& let RenderBoundingBox::Rectangle(bounds) = self.bounding_box(transform, false) && let RenderBoundingBox::Rectangle(bounds) = source.bounding_box(transform, false)
{ {
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
@@ -1833,7 +1892,7 @@ impl Render for List<Raster<CPU>> {
layer = true; layer = true;
} }
let transform_attribute: DAffine2 = self.attr::<Transform>(index); let transform_attribute: DAffine2 = source.attr::<Transform>(index);
if let RenderMode::Outline = render_params.render_mode { if let RenderMode::Outline = render_params.render_mode {
let outline_transform: DAffine2 = transform * transform_attribute; let outline_transform: DAffine2 = transform * transform_attribute;
@@ -1865,15 +1924,15 @@ impl Render for List<Raster<CPU>> {
} }
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_raster_metadata<S: LaneSource>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return }; let Some(element_id) = element_id else { return };
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]); metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one item of the `List<Raster<...>>` // TODO: Find a way to handle more than one item of the `List<Raster<...>>`
if !self.is_empty() { if source.lane_count() > 0 {
let transform: DAffine2 = self.attr::<Transform>(0); let transform: DAffine2 = source.attr::<Transform>(0);
metadata.local_transforms.insert(element_id, transform); metadata.local_transforms.insert(element_id, transform);
// If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize, // If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize,
@@ -1882,32 +1941,44 @@ impl Render for List<Raster<CPU>> {
// The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization // 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 // 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). // multiply in `transform` (which is the rasterization area, not a layer-stack transform).
if let Some(upstream_nested_layers) = self.attr::<EditorMergedLayers>(0).filter(|layers| !layers.is_empty()) { if let Some(upstream_nested_layers) = source.attr::<EditorMergedLayers>(0).filter(|layers| !layers.is_empty()) {
upstream_nested_layers.collect_metadata(metadata, footprint, None); upstream_nested_layers.collect_metadata(metadata, footprint, None);
} }
} }
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_raster_upstream_click_targets(click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
} }
impl Render for List<Raster<CPU>> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_raster_cpu_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) {
render_raster_cpu_vello(self, scene, transform, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
collect_raster_metadata(self, metadata, footprint, element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_raster_upstream_click_targets(click_targets)
}
} }
static LAZY_ARC_VEC_ZERO_U8: LazyLock<Arc<Vec<u8>>> = LazyLock::new(|| Arc::new(Vec::new())); static LAZY_ARC_VEC_ZERO_U8: LazyLock<Arc<Vec<u8>>> = LazyLock::new(|| Arc::new(Vec::new()));
impl Render for List<Raster<GPU>> { fn render_raster_gpu_vello<S: LaneSource<Element = Raster<GPU>> + BoundingBox>(source: &S, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
fn render_svg(&self, _render: &mut SvgRender, _render_params: &RenderParams) { for index in 0..source.lane_count() {
log::warn!("tried to render texture as an svg"); let Some(raster) = source.element(index) else { continue };
} let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index);
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
for index in 0..self.len() { let clip_attr: bool = source.attr::<ClippingMask>(index);
let Some(raster) = self.element(index) else { continue };
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 { let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal, RenderMode::Outline => peniko::Mix::Normal,
_ => blend_mode_attr.to_peniko(), _ => blend_mode_attr.to_peniko(),
@@ -1918,7 +1989,7 @@ impl Render for List<Raster<GPU>> {
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let any_nondefault = blend_mode_attr != BlendMode::default() || opacity < 1. || clip_attr; let any_nondefault = blend_mode_attr != BlendMode::default() || opacity < 1. || clip_attr;
if (render_params.render_mode != RenderMode::Outline && any_nondefault) if (render_params.render_mode != RenderMode::Outline && any_nondefault)
&& let RenderBoundingBox::Rectangle(bounds) = self.bounding_box(transform, true) && let RenderBoundingBox::Rectangle(bounds) = source.bounding_box(transform, true)
{ {
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
@@ -1926,7 +1997,7 @@ impl Render for List<Raster<GPU>> {
layer = true; layer = true;
} }
let transform_attribute: DAffine2 = self.attr::<Transform>(index); let transform_attribute: DAffine2 = source.attr::<Transform>(index);
if let RenderMode::Outline = render_params.render_mode { if let RenderMode::Outline = render_params.render_mode {
let outline_transform = transform * transform_attribute; let outline_transform = transform * transform_attribute;
@@ -1959,32 +2030,21 @@ impl Render for List<Raster<GPU>> {
} }
} }
impl Render for List<Raster<GPU>> {
fn render_svg(&self, _render: &mut SvgRender, _render_params: &RenderParams) {
log::warn!("tried to render texture as an svg");
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_raster_gpu_vello(self, scene, transform, context, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return }; collect_raster_metadata(self, metadata, footprint, element_id)
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one item of the `List<Raster<...>>`
if !self.is_empty() {
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,
// which destructively merges its inputs into pixels), recurse into that snapshot so the editor can
// surface the original child layers' click targets (the same mechanism Boolean Operation uses).
// 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).
if let Some(upstream_nested_layers) = self.attr::<EditorMergedLayers>(0).filter(|layers| !layers.is_empty()) {
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}
}
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); add_raster_upstream_click_targets(click_targets)
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
} }
} }
@@ -2068,8 +2128,7 @@ impl Render for List<Color> {
} }
} }
impl Render for List<GradientStops> { fn render_gradient_svg<S: LaneSource<Element = GradientStops>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
// For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`. // For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`.
// The viewBox crops the overshoot. Canvas rendering keeps the polyline path since Chrome rejects rects larger than ~20 million. // The viewBox crops the overshoot. Canvas rendering keeps the polyline path since Chrome rejects rects larger than ~20 million.
let thumbnail_rect = if render_params.thumbnail { let thumbnail_rect = if render_params.thumbnail {
@@ -2080,14 +2139,14 @@ impl Render for List<GradientStops> {
None None
}; };
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some(gradient) = self.element(index) else { continue }; let Some(gradient) = source.element(index) else { continue };
let transform: DAffine2 = self.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let spread_method: GradientSpreadMethod = self.attr::<SpreadMethod>(index); let spread_method: GradientSpreadMethod = source.attr::<SpreadMethod>(index);
let gradient_type: GradientType = self.attr::<GradientTypeAttr>(index); let gradient_type: GradientType = source.attr::<GradientTypeAttr>(index);
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" }; let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
render.leaf_tag(tag, |attributes| { render.leaf_tag(tag, |attributes| {
if let Some((min, size)) = thumbnail_rect { if let Some((min, size)) = thumbnail_rect {
@@ -2161,21 +2220,21 @@ impl Render for List<GradientStops> {
} }
} }
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { fn render_gradient_vello<S: LaneSource<Element = GradientStops>>(source: &S, scene: &mut Scene, parent_transform: DAffine2, render_params: &RenderParams) {
use vello::peniko; use vello::peniko;
if let RenderMode::Outline = render_params.render_mode { if let RenderMode::Outline = render_params.render_mode {
return; return;
} }
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some(gradient) = self.element(index) else { continue }; let Some(gradient) = source.element(index) else { continue };
let spread_method: GradientSpreadMethod = self.attr::<SpreadMethod>(index); let spread_method: GradientSpreadMethod = source.attr::<SpreadMethod>(index);
let gradient_type: GradientType = self.attr::<GradientTypeAttr>(index); let gradient_type: GradientType = source.attr::<GradientTypeAttr>(index);
let transform: DAffine2 = self.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let gradient_transform = parent_transform * transform; let gradient_transform = parent_transform * transform;
let blend_mode = blend_mode_attr.to_peniko(); let blend_mode = blend_mode_attr.to_peniko();
@@ -2244,6 +2303,15 @@ impl Render for List<GradientStops> {
} }
} }
} }
impl Render for List<GradientStops> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_gradient_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_gradient_vello(self, scene, parent_transform, render_params)
}
} }
/// Builds a `kurbo::BezPath` from a glyph outline, baking in the glyph origin (`ox`, `oy`) and faux-italic shear (`tilt_tan`). /// Builds a `kurbo::BezPath` from a glyph outline, baking in the glyph origin (`ox`, `oy`) and faux-italic shear (`tilt_tan`).
@@ -2317,22 +2385,22 @@ fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, ()>, x_offset: f3
} }
} }
/// Lays out text item `index` of a styled `List<String>` and returns its local size and transform. The `BoundingBox` trait can't do /// Lays out the text item at `index` and returns its local size and transform. The `BoundingBox` trait can't do
/// this since a bare `String` carries no typography, so click-target and bounding-box computation share this. Falls back to an em /// this since a bare `String` carries no typography, so click-target and bounding-box computation share this. Falls back to an em
/// square if the font isn't registered yet. /// square if the font isn't registered yet.
fn text_item_size_and_transform(list: &List<String>, index: usize) -> Option<(DVec2, DAffine2)> { fn text_item_size_and_transform<S: LaneSource<Element = String>>(source: &S, index: usize) -> Option<(DVec2, DAffine2)> {
let text = list.element(index)?; let text = source.element(index)?;
let font: Resource = { let font: Resource = {
let f = list.attr::<Font>(index); let f = source.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() } if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
}; };
let font_size: f64 = list.attr::<FontSize>(index); let font_size: f64 = source.attr::<FontSize>(index);
let line_height: f64 = list.attr::<LineHeight>(index); let line_height: f64 = source.attr::<LineHeight>(index);
let letter_spacing: f64 = list.attr::<LetterSpacing>(index); let letter_spacing: f64 = source.attr::<LetterSpacing>(index);
let max_width: Option<f64> = list.attr::<MaxWidth>(index); let max_width: Option<f64> = source.attr::<MaxWidth>(index);
let max_height: Option<f64> = list.attr::<MaxHeight>(index); let max_height: Option<f64> = source.attr::<MaxHeight>(index);
let align: text_nodes::TextAlign = list.attr::<TextAlign>(index); let align: text_nodes::TextAlign = source.attr::<TextAlign>(index);
let transform: DAffine2 = list.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
let typesetting = text_nodes::TypesettingConfig { let typesetting = text_nodes::TypesettingConfig {
font_size, font_size,
@@ -2356,12 +2424,12 @@ fn text_item_size_and_transform(list: &List<String>, index: usize) -> Option<(DV
Some((DVec2::new(width, height), transform)) Some((DVec2::new(width, height), transform))
} }
/// Union bounding box of a styled `List<String>`, laid out per item. The `BoundingBox` trait returns `None` for `List<String>` /// Union bounding box of a styled text source, laid out per item. The `BoundingBox` trait returns `None` for `List<String>`
/// (a bare `String` has no extent), so text-layer thumbnails and bounds use this instead. Each item is laid out under `outer_transform`. /// (a bare `String` has no extent), so text-layer thumbnails and bounds use this instead. Each item is laid out under `outer_transform`.
pub fn text_list_bounding_box(list: &List<String>, outer_transform: DAffine2) -> RenderBoundingBox { pub fn text_list_bounding_box<S: LaneSource<Element = String>>(source: &S, outer_transform: DAffine2) -> RenderBoundingBox {
let mut bounds: Option<[DVec2; 2]> = None; let mut bounds: Option<[DVec2; 2]> = None;
for index in 0..list.len() { for index in 0..source.lane_count() {
let Some((size, transform)) = text_item_size_and_transform(list, index) else { continue }; let Some((size, transform)) = text_item_size_and_transform(source, index) else { continue };
let full_transform = outer_transform * transform; let full_transform = outer_transform * transform;
for corner in [DVec2::ZERO, DVec2::new(size.x, 0.), DVec2::new(0., size.y), size] { for corner in [DVec2::ZERO, DVec2::new(size.x, 0.), DVec2::new(0., size.y), size] {
let point = full_transform.transform_point2(corner); let point = full_transform.transform_point2(corner);
@@ -2379,13 +2447,13 @@ pub fn text_list_bounding_box(list: &List<String>, outer_transform: DAffine2) ->
/// Like `List<Graphic>::thumbnail_bounding_box`, but lays out `Graphic::Text` items, which the `BoundingBox` trait reports as `None`. /// Like `List<Graphic>::thumbnail_bounding_box`, but lays out `Graphic::Text` items, which the `BoundingBox` trait reports as `None`.
/// Used for layer thumbnails so text layers (whose content is a `List<Graphic>` wrapping the text) frame their content. /// Used for layer thumbnails so text layers (whose content is a `List<Graphic>` wrapping the text) frame their content.
pub fn graphic_list_bounding_box(list: &List<Graphic>, transform: DAffine2) -> RenderBoundingBox { pub fn graphic_list_bounding_box<S: LaneSource<Element = Graphic>>(source: &S, transform: DAffine2) -> RenderBoundingBox {
let mut combined: Option<[DVec2; 2]> = None; let mut combined: Option<[DVec2; 2]> = None;
let mut any_infinite = false; let mut any_infinite = false;
for index in 0..list.len() { for index in 0..source.lane_count() {
let item_transform = transform * list.attr::<Transform>(index); let item_transform = transform * source.attr::<Transform>(index);
let Some(graphic) = list.element(index) else { continue }; let Some(graphic) = source.element(index) else { continue };
let bounds = match graphic { let bounds = match graphic {
Graphic::Text(text_list) => text_list_bounding_box(text_list, item_transform), Graphic::Text(text_list) => text_list_bounding_box(text_list, item_transform),
Graphic::Graphic(sub_list) => graphic_list_bounding_box(sub_list, item_transform), Graphic::Graphic(sub_list) => graphic_list_bounding_box(sub_list, item_transform),
@@ -2410,29 +2478,28 @@ pub fn graphic_list_bounding_box(list: &List<Graphic>, transform: DAffine2) -> R
} }
} }
impl Render for List<String> { fn render_text_svg<S: LaneSource<Element = String>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for index in 0..source.lane_count() {
for index in 0..self.len() { let Some(text) = source.element(index) else { continue };
let Some(text) = self.element(index) else { continue };
if text.is_empty() { if text.is_empty() {
continue; continue;
} }
let transform: DAffine2 = self.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let font: Resource = { let font: Resource = {
let f = self.attr::<Font>(index); let f = source.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() } if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
}; };
let font_size: f64 = self.attr::<FontSize>(index); let font_size: f64 = source.attr::<FontSize>(index);
let line_height: f64 = self.attr::<LineHeight>(index); let line_height: f64 = source.attr::<LineHeight>(index);
let letter_spacing: f64 = self.attr::<LetterSpacing>(index); let letter_spacing: f64 = source.attr::<LetterSpacing>(index);
let max_width: Option<f64> = self.attr::<MaxWidth>(index); let max_width: Option<f64> = source.attr::<MaxWidth>(index);
let max_height: Option<f64> = self.attr::<MaxHeight>(index); let max_height: Option<f64> = source.attr::<MaxHeight>(index);
let letter_tilt: f64 = self.attr::<LetterTilt>(index); let letter_tilt: f64 = source.attr::<LetterTilt>(index);
let align: text_nodes::TextAlign = self.attr::<TextAlign>(index); let align: text_nodes::TextAlign = source.attr::<TextAlign>(index);
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let typesetting = text_nodes::TypesettingConfig { let typesetting = text_nodes::TypesettingConfig {
@@ -2496,28 +2563,28 @@ impl Render for List<String> {
} }
} }
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { fn render_text_vello<S: LaneSource<Element = String>>(source: &S, scene: &mut Scene, transform: DAffine2, render_params: &RenderParams) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some(text) = self.element(index) else { continue }; let Some(text) = source.element(index) else { continue };
if text.is_empty() { if text.is_empty() {
continue; continue;
} }
let item_transform: DAffine2 = self.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let font: Resource = { let font: Resource = {
let f = self.attr::<Font>(index); let f = source.attr::<Font>(index);
if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() } if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() }
}; };
let font_size: f64 = self.attr::<FontSize>(index); let font_size: f64 = source.attr::<FontSize>(index);
let line_height: f64 = self.attr::<LineHeight>(index); let line_height: f64 = source.attr::<LineHeight>(index);
let letter_spacing: f64 = self.attr::<LetterSpacing>(index); let letter_spacing: f64 = source.attr::<LetterSpacing>(index);
let max_width: Option<f64> = self.attr::<MaxWidth>(index); let max_width: Option<f64> = source.attr::<MaxWidth>(index);
let max_height: Option<f64> = self.attr::<MaxHeight>(index); let max_height: Option<f64> = source.attr::<MaxHeight>(index);
let letter_tilt: f64 = self.attr::<LetterTilt>(index); let letter_tilt: f64 = source.attr::<LetterTilt>(index);
let align: text_nodes::TextAlign = self.attr::<TextAlign>(index); let align: text_nodes::TextAlign = source.attr::<TextAlign>(index);
let blend_mode_attr: BlendMode = self.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = self.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = self.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let typesetting = text_nodes::TypesettingConfig { let typesetting = text_nodes::TypesettingConfig {
@@ -2565,9 +2632,9 @@ impl Render for List<String> {
} }
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) { fn collect_text_metadata<S: LaneSource<Element = String>>(source: &S, 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]`. // 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.attr::<Transform>(0) } else { DAffine2::IDENTITY }; let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
let item_zero_inverse = if item_zero_transform.matrix2.determinant() != 0. { let item_zero_inverse = if item_zero_transform.matrix2.determinant() != 0. {
item_zero_transform.inverse() item_zero_transform.inverse()
} else { } else {
@@ -2576,8 +2643,8 @@ impl Render for List<String> {
let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new(); let mut accumulated_click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>> = HashMap::new();
for index in 0..self.len() { for index in 0..source.lane_count() {
let layer_path: &[NodeId] = self.attr::<EditorLayerPath>(index); let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied(); let layer = layer_path.last().copied();
let Some(element_id) = caller_element_id.or(layer) else { continue }; let Some(element_id) = caller_element_id.or(layer) else { continue };
@@ -2587,7 +2654,7 @@ impl Render for List<String> {
metadata.local_transforms.entry(element_id).or_insert(item_zero_transform); metadata.local_transforms.entry(element_id).or_insert(item_zero_transform);
} }
let Some((size, item_transform)) = text_item_size_and_transform(self, index) else { continue }; let Some((size, item_transform)) = text_item_size_and_transform(source, index) else { continue };
let subpath = Subpath::new_rectangle(DVec2::ZERO, size); let subpath = Subpath::new_rectangle(DVec2::ZERO, size);
let mut target = ClickTarget::new_with_subpath(subpath, 0.); let mut target = ClickTarget::new_with_subpath(subpath, 0.);
target.apply_transform(item_zero_inverse * item_transform); target.apply_transform(item_zero_inverse * item_transform);
@@ -2601,15 +2668,32 @@ impl Render for List<String> {
} }
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_text_upstream_click_targets<S: LaneSource<Element = String>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
for index in 0..self.len() { for index in 0..source.lane_count() {
let Some((size, transform)) = text_item_size_and_transform(self, index) else { continue }; let Some((size, transform)) = text_item_size_and_transform(source, index) else { continue };
let subpath = Subpath::new_rectangle(DVec2::ZERO, size); let subpath = Subpath::new_rectangle(DVec2::ZERO, size);
let mut target = ClickTarget::new_with_subpath(subpath, 0.); let mut target = ClickTarget::new_with_subpath(subpath, 0.);
target.apply_transform(transform); target.apply_transform(transform);
click_targets.push(target); click_targets.push(target);
} }
} }
impl Render for List<String> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_text_svg(self, render, render_params)
}
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
render_text_vello(self, scene, transform, render_params)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_text_metadata(self, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_text_upstream_click_targets(self, click_targets)
}
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]