Read every paint through the cascaded appearance

The renderer, metadata collection, hit testing, and clip-path reduction
all resolve a lane's paint as its own declared appearance or the
nearest ancestor's through the cascade, taking the first fill and
stroke coverages in one walk: the coverage's stroke parameters replace
the element-resident stroke, the paint cells' interior lists feed the
paint renderers, and the paint order reads off the coverage row order,
which the stroke nodes now place from their paint order input. The
paint overlay and its transmute dissolve into an inherited-appearance
parameter on the vector-level bodies, with the Render trait untouched.
Mask clones paint through a single black-fill appearance, and every
row-building node stamps the appearance beside the legacy markers it
still writes, so renders stay identical until the markers go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Kobert
2026-09-09 22:18:50 +00:00
parent 1fff59f380
commit 46e0a4f2f5
6 changed files with 325 additions and 181 deletions

View File

@@ -23,8 +23,9 @@ use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource;
use graphic_types::graphic::{PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path};
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
use graphic_types::appearance::{Appearance, Coverage};
use graphic_types::graphic::{PaintColumns, PaintReach, is_paint_present, paint_cell_rows, set_paint_attribute, vector_can_reduce_to_clip_path};
use graphic_types::markers::{Appearance as AppearanceMarker, 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};
@@ -207,6 +208,11 @@ pub struct RenderContext {
pub resource_overrides: Vec<(peniko::ImageBrush, Texture)>,
}
/// The single black-fill appearance a mask clone paints with, at full alpha so the mask fully covers the interior.
fn black_fill_appearance() -> Appearance {
Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(List::new_from_element(Graphic::Color(Color::BLACK))))
}
/// The alpha multiplier a paint row's opacity attributes apply when it serves as a paint.
/// Fill opacity fades a paint just as opacity does, but a masker drops it so it cannot reach the content clipped to it.
pub(crate) fn paint_row_opacity<T>(list: &List<T>, index: usize, for_mask: bool) -> f32 {
@@ -624,7 +630,7 @@ impl Render for Graphic<'_> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
match self {
Graphic::Graphic(list) => list.render_svg(render, render_params),
Graphic::Vector(vector) => render_vector_svg(&Single(vector), render, render_params),
Graphic::Vector(vector) => render_vector_svg(&Single(vector), None, render, render_params),
Graphic::RasterCPU(raster) => render_raster_cpu_svg(&Single(raster), render, render_params),
Graphic::RasterGPU(_) => (),
Graphic::Color(color) => render_color_svg(&Single(color), render, render_params),
@@ -637,7 +643,7 @@ impl Render for Graphic<'_> {
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
match self {
Graphic::Graphic(list) => list.render_to_vello(scene, transform, context, render_params),
Graphic::Vector(vector) => render_vector_vello(&Single(vector), scene, transform, context, render_params),
Graphic::Vector(vector) => render_vector_vello(&Single(vector), None, scene, transform, context, render_params),
Graphic::RasterCPU(raster) => render_raster_cpu_vello(&Single(raster), scene, transform, render_params),
Graphic::RasterGPU(raster) => render_raster_gpu_vello(&Single(raster), scene, transform, context, render_params),
Graphic::Color(color) => render_color_vello(&Single(color), scene, render_params),
@@ -677,7 +683,7 @@ impl Render for Graphic<'_> {
fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) {
match element {
Graphic::Vector(vector) if reach.applies() => render_vector_svg(&PaintOverlay::new(&Single(vector), reach.paint), render, render_params),
Graphic::Vector(vector) => render_vector_svg(&Single(vector), reach.appearance, render, render_params),
Graphic::Graphic(inner) => render_graphic_svg_with(inner, reach.nested(), render, render_params),
Graphic::Group(group) => render_group_svg(group, reach, render, render_params),
_ => element.render_svg(render, render_params),
@@ -686,7 +692,7 @@ fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &
fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
match element {
Graphic::Vector(vector) if reach.applies() => render_vector_vello(&PaintOverlay::new(&Single(vector), reach.paint), scene, transform, context, render_params),
Graphic::Vector(vector) => render_vector_vello(&Single(vector), reach.appearance, scene, transform, context, render_params),
Graphic::Graphic(inner) => render_graphic_vello_with(inner, reach.nested(), scene, transform, context, render_params),
Graphic::Group(group) => render_group_vello(group, reach, scene, transform, context, render_params),
_ => element.render_to_vello(scene, transform, context, render_params),
@@ -695,13 +701,12 @@ fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene:
fn element_can_reduce_to_clip_path<'a>(element: &'a Graphic, reach: PaintReach<'a>) -> bool {
match element {
Graphic::Vector(vector) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&Single(vector), reach.paint)),
Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&Single(vector), reach.appearance),
Graphic::Group(group) => match RunView::<Vector>::new(&group.content) {
Some(run) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&run, reach.paint)),
Some(run) => vector_can_reduce_to_clip_path(&run),
Some(run) => vector_can_reduce_to_clip_path(&run, reach.appearance),
None => false,
},
_ => element.can_reduce_to_clip_path(),
_ => element.can_reduce_to_clip_path(reach.appearance),
}
}
@@ -732,8 +737,7 @@ fn collect_element_metadata<'a>(
match element {
Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id),
Graphic::Vector(vector) if reach.applies() => collect_vector_metadata(&PaintOverlay::new(&Single(vector), reach.paint), metadata, footprint, element_id),
Graphic::Vector(vector) => collect_vector_metadata(&Single(vector), metadata, footprint, element_id),
Graphic::Vector(vector) => collect_vector_metadata(&Single(vector), reach.appearance, metadata, footprint, element_id),
Graphic::RasterCPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id),
Graphic::RasterGPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id),
Graphic::Color(_) => {}
@@ -775,8 +779,7 @@ fn collect_group_row_metadata(group: &Group, metadata: &mut RenderMetadata, elem
fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) {
match element {
Graphic::Graphic(list) => add_graphic_upstream_click_targets_with(list, reach.nested(), click_targets),
Graphic::Vector(vector) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(&Single(vector), reach.paint), click_targets),
Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), click_targets),
Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), reach.appearance, click_targets),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets),
Graphic::Color(_) | Graphic::Gradient(_) => {}
Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), click_targets),
@@ -787,8 +790,7 @@ fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReac
fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) {
match element {
Graphic::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach.nested(), outlines),
Graphic::Vector(vector) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(&Single(vector), reach.paint), outlines),
Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), outlines),
Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), reach.appearance, outlines),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines),
Graphic::Color(_) | Graphic::Gradient(_) => {}
Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), outlines),
@@ -803,10 +805,7 @@ fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut Sv
if let Some(run) = RunView::<Graphic>::new(item) {
render_graphic_svg_with(&run, reach.into_group_graphics(), render, render_params)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => render_vector_svg(&PaintOverlay::new(&run, reach.paint), render, render_params),
false => render_vector_svg(&run, render, render_params),
}
render_vector_svg(&run, reach.appearance, render, render_params)
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
render_raster_cpu_svg(&run, render, render_params)
} else if item.typed_lanes::<Raster<GPU>>().is_some() {
@@ -824,10 +823,7 @@ fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut S
if let Some(run) = RunView::<Graphic>::new(item) {
render_graphic_vello_with(&run, reach.into_group_graphics(), scene, transform, context, render_params)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => render_vector_vello(&PaintOverlay::new(&run, reach.paint), scene, transform, context, render_params),
false => render_vector_vello(&run, scene, transform, context, render_params),
}
render_vector_vello(&run, reach.appearance, scene, transform, context, render_params)
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
render_raster_cpu_vello(&run, scene, transform, render_params)
} else if let Some(run) = RunView::<Raster<GPU>>::new(item) {
@@ -849,10 +845,7 @@ fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata:
if let Some(run) = RunView::<Graphic>::new(item) {
collect_graphic_metadata_with(&run, reach.into_group_graphics(), metadata, footprint, element_id)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => collect_vector_metadata(&PaintOverlay::new(&run, reach.paint), metadata, footprint, element_id),
false => collect_vector_metadata(&run, metadata, footprint, element_id),
}
collect_vector_metadata(&run, reach.appearance, metadata, footprint, element_id)
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
collect_raster_metadata(&run, metadata, footprint, element_id)
} else if let Some(run) = RunView::<Raster<GPU>>::new(item) {
@@ -868,10 +861,7 @@ fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>,
if let Some(run) = RunView::<Graphic>::new(item) {
add_graphic_upstream_click_targets_with(&run, reach.into_group_graphics(), click_targets)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => add_vector_upstream_click_targets(&PaintOverlay::new(&run, reach.paint), click_targets),
false => add_vector_upstream_click_targets(&run, click_targets),
}
add_vector_upstream_click_targets(&run, reach.appearance, click_targets)
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
add_raster_upstream_click_targets(click_targets)
} else if let Some(run) = RunView::<String>::new(item) {
@@ -884,10 +874,7 @@ fn add_group_upstream_outline_targets<'a>(group: &'a Group, reach: PaintReach<'a
if let Some(run) = RunView::<Graphic>::new(item) {
add_graphic_upstream_outline_targets_with(&run, reach.into_group_graphics(), outlines)
} else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() {
true => add_vector_upstream_outline_targets(&PaintOverlay::new(&run, reach.paint), outlines),
false => add_vector_upstream_outline_targets(&run, outlines),
}
add_vector_upstream_outline_targets(&run, reach.appearance, outlines)
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
add_raster_upstream_click_targets(outlines)
} else if let Some(run) = RunView::<String>::new(item) {
@@ -1343,7 +1330,7 @@ impl Render for List<Graphic<'_>> {
}
}
fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, inherited_appearance: Option<&Appearance>, render: &mut SvgRender, render_params: &RenderParams) {
for index in 0..source.lane_count() {
let Some(vector) = source.element(index) else { continue };
let item_transform: DAffine2 = source.attr::<Transform>(index);
@@ -1351,8 +1338,17 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
// The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade
let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let mut resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
// The paint order rides the coverage list's row order
if let Some(stroke) = &mut resolved.stroke {
stroke.paint_order = if resolved.stroke_below { PaintOrder::StrokeBelow } else { PaintOrder::StrokeAbove };
}
let element_stroke = resolved.stroke.as_ref();
// 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 = element_stroke.filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
let applied_stroke_transform = set_stroke_transform.unwrap_or(item_transform);
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
@@ -1372,26 +1368,26 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
path.push_str(bezpath.to_svg().as_str());
}
let mask_type = if vector.stroke.as_ref().map(|x| x.align) == Some(StrokeAlign::Inside) {
let mask_type = if element_stroke.map(|x| x.align) == Some(StrokeAlign::Inside) {
MaskType::Clip
} else {
MaskType::Mask
};
let fill_graphic_list = paint_graphics::<Fill, _>(source, index);
let fill_graphic_list = resolved.fill_paint.and_then(paint_cell_rows);
let fill_graphic = fill_graphic_list.and_then(|l| l.element(0));
let stroke_graphic_list = paint_graphics::<Stroke, _>(source, index);
let stroke_graphic_list = resolved.stroke_paint.and_then(paint_cell_rows);
let stroke_graphic = stroke_graphic_list.and_then(|l| l.element(0));
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
let can_draw_aligned_stroke = path_is_closed
&& vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
&& element_stroke.is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
&& stroke_graphic.is_some_and(|graphic| !graphic.is_fully_transparent());
let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip);
let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order;
let wants_stroke_below = vector.stroke.as_ref().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
let wants_stroke_below = element_stroke.map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
let override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
let use_face_fill = vector.use_face_fill();
@@ -1418,6 +1414,7 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
// The wrapping SVG group (above) handles the user-set opacity.
let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
mask_item.set_attribute(graphic_types::ATTR_APPEARANCE, black_fill_appearance());
let vector_item = List::new_from_item(mask_item);
(id, mask_type, vector_item)
@@ -1452,7 +1449,7 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
if let Some((ref id, mask_type, ref vector_item)) = push_id {
let mut svg = SvgRender::new();
vector_item.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
let stroke = vector.stroke.as_ref().unwrap();
let stroke = element_stroke.unwrap();
// `push_id` is only `Some` when `can_draw_aligned_stroke`, which is gated on `path_is_closed`
let (largest_scale, _) = singular_values(applied_stroke_transform);
let inflation = stroke.max_aabb_inflation(true) * largest_scale;
@@ -1479,9 +1476,7 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
render_params.aligned_strokes = can_draw_aligned_stroke;
render_params.override_paint_order = override_paint_order;
let stroke_shape_attribute = vector
.stroke
.as_ref()
let stroke_shape_attribute = element_stroke
.map(|stroke| {
if stroke_graphic_list.is_some_and(is_paint_present) {
stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke)
@@ -1492,7 +1487,7 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
.unwrap_or_default();
// Need to avoid generating only paint attribute, otherwise SVG uses 1px width stroke as a fallback
let stroke_visible = vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent());
let stroke_visible = element_stroke.is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent());
let stroke_attribute = if stroke_visible {
stroke_graphic_list
.map(|list| {
@@ -1555,7 +1550,14 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
}
}
fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
fn render_vector_vello<S: LaneSource<Element = Vector>>(
source: &S,
inherited_appearance: Option<&Appearance>,
scene: &mut Scene,
parent_transform: DAffine2,
context: &mut RenderContext,
render_params: &RenderParams,
) {
for index in 0..source.lane_count() {
use graphic_types::vector_types::vector;
@@ -1565,7 +1567,20 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let multiplied_transform = parent_transform * item_transform;
let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
// The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade
let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let mut resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
// The paint order rides the coverage list's row order
if let Some(stroke) = &mut resolved.stroke {
stroke.paint_order = if resolved.stroke_below {
vector::style::PaintOrder::StrokeBelow
} else {
vector::style::PaintOrder::StrokeAbove
};
}
let has_real_stroke = resolved.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 mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
let mut element_transform = set_stroke_transform
@@ -1589,8 +1604,8 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
}
}
let fill_graphic_list = paint_graphics::<Fill, _>(source, index);
let stroke_graphic_list = paint_graphics::<Stroke, _>(source, index);
let fill_graphic_list = resolved.fill_paint.and_then(paint_cell_rows);
let stroke_graphic_list = resolved.stroke_paint.and_then(paint_cell_rows);
// If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.render_mode {
@@ -1602,7 +1617,7 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
// Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path).
// Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
let stroke = element.stroke.as_ref();
let stroke = resolved.stroke.as_ref();
let stroke_fully_transparent = stroke_graphic_list.is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent()));
let can_draw_aligned_stroke = !stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
@@ -1764,6 +1779,7 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
// The outer opacity/blend layer (above) handles the user-set opacity.
let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK));
mask_item.set_attribute(graphic_types::ATTR_APPEARANCE, black_fill_appearance());
let vector_list = List::new_from_item(mask_item);
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
@@ -1833,7 +1849,13 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
}
}
fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
fn collect_vector_metadata<S: LaneSource<Element = Vector>>(
source: &S,
inherited_appearance: Option<&Appearance>,
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 source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
@@ -1852,6 +1874,10 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied();
// The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade
let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
if let Some(element_id) = caller_element_id.or(layer) {
// When recovering element_id from the item's editor:layer_path tag (because the caller
// passed None), also store the transform metadata that Graphic::collect_metadata
@@ -1867,12 +1893,12 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
let item_relative_transform = item_zero_inverse * transform;
let mut click_targets_unwrapped = Vec::new();
extend_targets_from_vector(&mut click_targets_unwrapped, source, index, click_target_vector, item_relative_transform);
extend_targets_from_vector(&mut click_targets_unwrapped, &resolved, click_target_vector, item_relative_transform);
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
let mut outlines_unwrapped = Vec::new();
extend_targets_from_vector(&mut outlines_unwrapped, source, index, element, item_relative_transform);
extend_targets_from_vector(&mut outlines_unwrapped, &resolved, element, item_relative_transform);
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.
@@ -1882,10 +1908,10 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) {
e.insert(Arc::new(element.clone()));
if let Some(fill_graphic) = source.attr::<Fill>(index).filter(|list| is_paint_present(list)) {
if let Some(fill_graphic) = resolved.fill_paint.and_then(paint_cell_rows) {
metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.clone()));
}
if let Some(stroke_graphic) = source.attr::<Stroke>(index).filter(|list| is_paint_present(list)) {
if let Some(stroke_graphic) = resolved.stroke_paint.and_then(paint_cell_rows) {
metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.clone()));
}
}
@@ -1915,7 +1941,7 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
}
}
fn add_vector_upstream_click_targets<S: LaneSource<Element = Vector>>(source: &S, click_targets: &mut Vec<ClickTarget>) {
fn add_vector_upstream_click_targets<S: LaneSource<Element = Vector>>(source: &S, inherited_appearance: Option<&Appearance>, click_targets: &mut Vec<ClickTarget>) {
for index in 0..source.lane_count() {
let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index);
@@ -1923,39 +1949,45 @@ fn add_vector_upstream_click_targets<S: LaneSource<Element = Vector>>(source: &S
// Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes)
let vector = source.attr::<EditorClickTarget>(index).unwrap_or(element);
extend_targets_from_vector(click_targets, source, index, vector, transform);
let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
extend_targets_from_vector(click_targets, &resolved, vector, transform);
}
}
fn add_vector_upstream_outline_targets<S: LaneSource<Element = Vector>>(source: &S, outlines: &mut Vec<ClickTarget>) {
fn add_vector_upstream_outline_targets<S: LaneSource<Element = Vector>>(source: &S, inherited_appearance: Option<&Appearance>, 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);
let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
extend_targets_from_vector(outlines, &resolved, element, transform);
}
}
impl Render for List<Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_vector_svg(self, render, render_params)
render_vector_svg(self, None, 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)
render_vector_vello(self, None, 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)
collect_vector_metadata(self, None, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_vector_upstream_click_targets(self, click_targets)
add_vector_upstream_click_targets(self, None, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_vector_upstream_outline_targets(self, outlines)
add_vector_upstream_outline_targets(self, None, outlines)
}
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
@@ -1967,15 +1999,15 @@ impl Render for List<Vector> {
/// 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.
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::<Fill, _>(source, index);
fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, resolved: &graphic_types::appearance::FillAndStroke<'_>, geometry: &Vector, transform: DAffine2) {
let filled = resolved.fill_paint.and_then(paint_cell_rows).is_some();
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect();
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed());
// Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side,
// so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths.
let stroke_width = geometry.stroke.as_ref().map_or(0., |stroke| {
let stroke_width = resolved.stroke.as_ref().map_or(0., |stroke| {
if stroke.align.is_not_centered() && all_subpaths_closed {
stroke.weight * 2.
} else {
@@ -2954,23 +2986,23 @@ impl Render for RunView<'_, Graphic<'_>> {
impl Render for RunView<'_, Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_vector_svg(self, render, render_params)
render_vector_svg(self, None, 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)
render_vector_vello(self, None, 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)
collect_vector_metadata(self, None, metadata, footprint, caller_element_id)
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_vector_upstream_click_targets(self, click_targets)
add_vector_upstream_click_targets(self, None, click_targets)
}
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_vector_upstream_outline_targets(self, outlines)
add_vector_upstream_outline_targets(self, None, outlines)
}
}
@@ -3120,6 +3152,11 @@ mod group_walk_tests {
List::new_from_element(Graphic::Color(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap()))
}
/// The appearance the fill node stamps beside the legacy fill marker, so test content mirrors node output.
fn fill_appearance(paint: &List<Graphic<'static>>) -> Appearance {
Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(paint.clone()))
}
fn rendered_svg(render: impl FnOnce(&mut SvgRender)) -> (String, String) {
let mut svg_render = SvgRender::new();
render(&mut svg_render);
@@ -3132,9 +3169,11 @@ mod group_walk_tests {
let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(3., 1.))];
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 2).unwrap();
let appearance = fill_appearance(&paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0), FieldWrite::of::<AppearanceMarker>(0)], 2).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
builder.attr::<AppearanceMarker>(lane, Some(&appearance));
builder.push(vectors[1].clone()).unwrap();
let item = builder.finish();
let group = Group { row: None, content: item };
@@ -3151,9 +3190,11 @@ mod group_walk_tests {
let paint = color_paint();
let inner = Graphic::Vector(unit_square_at(DVec2::ZERO));
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Graphic>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let appearance = fill_appearance(&paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Graphic>(), &[FieldWrite::of::<Fill>(0), FieldWrite::of::<AppearanceMarker>(0)], 1).unwrap();
let lane = builder.push(inner.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
builder.attr::<AppearanceMarker>(lane, Some(&appearance));
let item = builder.finish();
let group = Group { row: None, content: item };
@@ -3170,9 +3211,11 @@ mod group_walk_tests {
let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO)];
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap();
let appearance = fill_appearance(&paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0), FieldWrite::of::<AppearanceMarker>(0)], 1).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
builder.attr::<AppearanceMarker>(lane, Some(&appearance));
let item = builder.finish();
let group = Group { row: None, content: item };
@@ -3195,9 +3238,11 @@ mod group_walk_tests {
let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(2., 2.))];
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 2).unwrap();
let appearance = fill_appearance(&paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0), FieldWrite::of::<AppearanceMarker>(0)], 2).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint));
builder.attr::<AppearanceMarker>(lane, Some(&appearance));
builder.push(vectors[1].clone()).unwrap();
let item = builder.finish();
let group = Group { row: None, content: item };