mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-03 08:48:11 +08:00
Fix clipping masks, click targets, and bounds when rendering Vector[] and flattened graphic wrappers (#4429)
* Honor clipping masks when rendering Vector[] lists, not just Graphic[] groups * Keep per-layer click targets when a Vector[] round-trips through a Graphic[] wrapper * Skip the rebuild and the merged-layers snapshot when flattening a lone anonymous graphic wrapper * Fix a group's bounds excluding its raster children and stretching to the origin * Fix SVG clipping masks landing in the wrong space when sibling layers differ in transform
This commit is contained in:
@@ -9,7 +9,7 @@ pub(crate) use legacy::run_to_legacy_list;
|
|||||||
pub use legacy::{group_to_legacy_graphic, group_to_legacy_list, map_groups_to_legacy, map_paint_attrs_to_legacy, run_to_list};
|
pub use legacy::{group_to_legacy_graphic, group_to_legacy_list, map_groups_to_legacy, map_paint_attrs_to_legacy, run_to_list};
|
||||||
pub use paint::{
|
pub use paint::{
|
||||||
LanePaint, PaintColumns, PaintOverlay, PaintOverlayColumn, PaintReach, bake_paint_transforms, has_paint, is_paint_present, paint_graphics, set_paint_attribute, set_paint_attribute_at,
|
LanePaint, PaintColumns, PaintOverlay, PaintOverlayColumn, PaintReach, bake_paint_transforms, has_paint, is_paint_present, paint_graphics, set_paint_attribute, set_paint_attribute_at,
|
||||||
vector_can_reduce_to_clip_path,
|
vector_can_reduce_to_clip_path, vector_lane_can_reduce_to_clip_path,
|
||||||
};
|
};
|
||||||
pub use walk::{GraphicLevel, GraphicLevelColumn, RowStep, VectorRow, direct_vector_len, flatten_vector_rows, group_is_empty, lane_attributes, run_lane_attributes, walk_vector_rows};
|
pub use walk::{GraphicLevel, GraphicLevelColumn, RowStep, VectorRow, direct_vector_len, flatten_vector_rows, group_is_empty, lane_attributes, run_lane_attributes, walk_vector_rows};
|
||||||
use walk::{group_all_clipped, group_bounding_box, group_is_fully_transparent, group_is_opaque, group_render_complexity};
|
use walk::{group_all_clipped, group_bounding_box, group_is_fully_transparent, group_is_opaque, group_render_complexity};
|
||||||
@@ -157,9 +157,28 @@ impl From<String> for Graphic<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the list is a single leaf item carrying nothing to compose onto its contents, so flattening it
|
||||||
|
/// collapses no structure and rebuilding or snapshotting the result would be busywork.
|
||||||
|
/// Both group forms are excluded: a lone native `Group` still has interior structure to flatten.
|
||||||
|
pub fn is_lone_anonymous_leaf(content: &List<Graphic>) -> bool {
|
||||||
|
content.len() == 1
|
||||||
|
&& !matches!(content.element(0), Some(Graphic::Graphic(_)) | Some(Graphic::Group(_)))
|
||||||
|
&& content.attribute::<DAffine2>(ATTR_TRANSFORM, 0).is_none()
|
||||||
|
&& content.attribute::<f64>(ATTR_OPACITY, 0).is_none()
|
||||||
|
&& content.attribute::<f64>(ATTR_OPACITY_FILL, 0).is_none()
|
||||||
|
&& content.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, 0).is_none()
|
||||||
|
}
|
||||||
|
|
||||||
/// Deeply flattens a `List<Graphic>`, collecting only elements matching a specific variant (extracted by `extract_variant`)
|
/// Deeply flattens a `List<Graphic>`, collecting only elements matching a specific variant (extracted by `extract_variant`)
|
||||||
/// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`List`s composes transforms and opacity.
|
/// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`List`s composes transforms and opacity.
|
||||||
fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) -> List<T> {
|
fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) -> List<T> {
|
||||||
|
// Its list is already the flat answer, so hand it back rather than rebuilding it item by item
|
||||||
|
if is_lone_anonymous_leaf(&content) {
|
||||||
|
let Some(item) = content.into_iter().next() else { return List::new() };
|
||||||
|
|
||||||
|
return extract_variant(item.into_element()).unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>, parent_layer_path: Option<&[NodeId]>) {
|
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>, parent_layer_path: Option<&[NodeId]>) {
|
||||||
for current_graphic_item in current_graphic_list.into_iter() {
|
for current_graphic_item in current_graphic_list.into_iter() {
|
||||||
// Whether the parent carries each attribute: a structural fact (column presence), never a value comparison.
|
// Whether the parent carries each attribute: a structural fact (column presence), never a value comparison.
|
||||||
@@ -179,6 +198,12 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
|||||||
// Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries.
|
// Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries.
|
||||||
// A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op.
|
// A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op.
|
||||||
Graphic::Graphic(mut sub_list) => {
|
Graphic::Graphic(mut sub_list) => {
|
||||||
|
// A group's first child has no preceding sibling, so its clipping flag is inert until splicing
|
||||||
|
// hands it the group's own predecessor. Clear it (keeping the column) to stay clip-neutral.
|
||||||
|
if sub_list.attribute::<bool>(ATTR_CLIPPING_MASK, 0).is_some() {
|
||||||
|
sub_list.set_attribute(ATTR_CLIPPING_MASK, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
if parent_has_transform {
|
if parent_has_transform {
|
||||||
for v in sub_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
for v in sub_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
*v = current_transform * *v;
|
*v = current_transform * *v;
|
||||||
@@ -573,6 +598,45 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
|
|
||||||
|
fn vector_list_stamped_with_layers(layers: [u64; 2]) -> List<Vector> {
|
||||||
|
let mut list = List::new();
|
||||||
|
|
||||||
|
for layer in layers {
|
||||||
|
let mut item = Item::new_from_element(Vector::default());
|
||||||
|
item.set_attribute(ATTR_EDITOR_LAYER_PATH, vec![NodeId(layer)]);
|
||||||
|
list.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_layer_stamps(list: &List<Vector>) -> Vec<Option<NodeId>> {
|
||||||
|
(0..list.len())
|
||||||
|
.map(|index| list.attribute_cloned_or_default::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, index).last().copied())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our coercion de-tables: each item becomes its own graphic lane keeping its own stamp, rather than master's
|
||||||
|
// single wrapper item that had to be kept anonymous so it could not overwrite the inner items' stamps
|
||||||
|
#[test]
|
||||||
|
fn wrapping_a_typed_list_keeps_one_lane_per_item() {
|
||||||
|
let graphic_list = vector_list_stamped_with_layers([7, 9]).into_graphic_list();
|
||||||
|
|
||||||
|
assert_eq!(graphic_list.len(), 2, "the coercion must not collapse the items into one wrapper lane");
|
||||||
|
let layers = (0..graphic_list.len())
|
||||||
|
.map(|index| graphic_list.attribute_cloned_or_default::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, index).last().copied())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(layers, [Some(NodeId(7)), Some(NodeId(9))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round-tripping through the graphic form must not collapse the items' distinct stamps onto item 0's
|
||||||
|
#[test]
|
||||||
|
fn round_trip_through_the_wrapper_preserves_per_item_layer_paths() {
|
||||||
|
let flattened: List<Vector> = vector_list_stamped_with_layers([7, 9]).into_flattened_list();
|
||||||
|
|
||||||
|
assert_eq!(last_layer_stamps(&flattened), [Some(NodeId(7)), Some(NodeId(9))]);
|
||||||
|
}
|
||||||
|
|
||||||
fn vector_graphic() -> Graphic<'static> {
|
fn vector_graphic() -> Graphic<'static> {
|
||||||
Graphic::Vector(Vector::default())
|
Graphic::Vector(Vector::default())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,9 @@ where
|
|||||||
paint_graphics::<A, S>(source, index).is_some()
|
paint_graphics::<A, S>(source, index).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether every lane of a vector source draws as a plain clip path: fully
|
/// Whether one lane of a vector source draws as a plain clip path: fully
|
||||||
/// opaque, fill absent or opaque, stroke invisible or fully transparent.
|
/// opaque, fill absent or opaque, stroke invisible or fully transparent.
|
||||||
pub fn vector_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S) -> bool {
|
pub fn vector_lane_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S, index: usize) -> bool {
|
||||||
(0..source.lane_count()).all(|index| {
|
|
||||||
let Some(element) = source.element(index) else { return false };
|
let Some(element) = source.element(index) else { return false };
|
||||||
let opacity: f64 = source.attr::<Opacity>(index);
|
let opacity: f64 = source.attr::<Opacity>(index);
|
||||||
|
|
||||||
@@ -50,7 +49,11 @@ pub fn vector_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &
|
|||||||
|| paint_graphics::<Stroke, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
|| paint_graphics::<Stroke, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
||||||
|
|
||||||
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
|
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
|
||||||
})
|
}
|
||||||
|
|
||||||
|
/// Whether every lane of a vector source draws as a plain clip path.
|
||||||
|
pub fn vector_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S) -> bool {
|
||||||
|
(0..source.lane_count()).all(|index| vector_lane_can_reduce_to_clip_path(source, index))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The paint a lane carries for its interiors, in the reference form
|
/// The paint a lane carries for its interiors, in the reference form
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ 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::{PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path};
|
use graphic_types::graphic::{
|
||||||
|
PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path, vector_lane_can_reduce_to_clip_path,
|
||||||
|
};
|
||||||
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
|
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::{Gradient, GradientForm, GradientSettings};
|
use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientSettings};
|
||||||
@@ -1103,12 +1105,41 @@ fn render_graphic_svg_with<'a, 'e, S: LaneSource<Element = Graphic<'e>>>(source:
|
|||||||
let element = source.element(index).unwrap();
|
let element = source.element(index).unwrap();
|
||||||
let reach = inherited.for_lane(&paint_columns, index);
|
let reach = inherited.for_lane(&paint_columns, index);
|
||||||
|
|
||||||
|
let matrix = format_transform_matrix(transform);
|
||||||
|
let next_clips = index + 1 < source.lane_count() && source.element(index + 1).unwrap().had_clip_enabled();
|
||||||
|
let mut masked_by = None;
|
||||||
|
|
||||||
|
if next_clips && mask_state.is_none() {
|
||||||
|
let uuid = generate_uuid();
|
||||||
|
let mask_type = if element_can_reduce_to_clip_path(element, reach) { MaskType::Clip } else { MaskType::Mask };
|
||||||
|
|
||||||
|
let mut svg = SvgRender::new();
|
||||||
|
render_element_svg(element, reach, &mut svg, &render_params.for_clipper());
|
||||||
|
|
||||||
|
// The def is resolved in this list's space, so the masker's own transform has to be baked into it
|
||||||
|
let masker = match matrix.is_empty() {
|
||||||
|
true => svg.svg.to_svg_string(),
|
||||||
|
false => format!(r##"<g transform="{matrix}">{}</g>"##, svg.svg.to_svg_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
render.svg_defs.push_str(&svg.svg_defs);
|
||||||
|
mask_type.write_to_defs(&mut render.svg_defs, uuid, masker);
|
||||||
|
|
||||||
|
mask_state = Some((uuid, mask_type));
|
||||||
|
} else if let Some((uuid, mask_type)) = mask_state {
|
||||||
|
if !next_clips {
|
||||||
|
mask_state = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
masked_by = Some((mask_type.to_attribute(), format!("url(#mask-{uuid})")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let render_item = |render: &mut SvgRender| {
|
||||||
render.parent_tag(
|
render.parent_tag(
|
||||||
"g",
|
"g",
|
||||||
|attributes| {
|
|attributes| {
|
||||||
let matrix = format_transform_matrix(transform);
|
|
||||||
if !matrix.is_empty() {
|
if !matrix.is_empty() {
|
||||||
attributes.push(ATTR_TRANSFORM, matrix);
|
attributes.push(ATTR_TRANSFORM, matrix.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -1119,33 +1150,16 @@ fn render_graphic_svg_with<'a, 'e, S: LaneSource<Element = Graphic<'e>>>(source:
|
|||||||
if blend_mode != BlendMode::default() {
|
if blend_mode != BlendMode::default() {
|
||||||
attributes.push("style", blend_mode.render());
|
attributes.push("style", blend_mode.render());
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_clips = index + 1 < source.lane_count() && source.element(index + 1).unwrap().had_clip_enabled();
|
|
||||||
|
|
||||||
if next_clips && mask_state.is_none() {
|
|
||||||
let uuid = generate_uuid();
|
|
||||||
let mask_type = if element_can_reduce_to_clip_path(element, reach) { MaskType::Clip } else { MaskType::Mask };
|
|
||||||
mask_state = Some((uuid, mask_type));
|
|
||||||
let mut svg = SvgRender::new();
|
|
||||||
render_element_svg(element, reach, &mut svg, &render_params.for_clipper());
|
|
||||||
|
|
||||||
write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap();
|
|
||||||
mask_type.write_to_defs(&mut attributes.0.svg_defs, uuid, svg.svg.to_svg_string());
|
|
||||||
} else if let Some((uuid, mask_type)) = mask_state {
|
|
||||||
if !next_clips {
|
|
||||||
mask_state = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = format!("mask-{uuid}");
|
|
||||||
let selector = format!("url(#{id})");
|
|
||||||
|
|
||||||
attributes.push(mask_type.to_attribute(), selector);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|render| {
|
|
||||||
render_element_svg(element, reach, render, render_params);
|
|
||||||
},
|
},
|
||||||
|
|render| render_element_svg(element, reach, render, render_params),
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The mask rides an untransformed wrapper so it resolves in this list's space rather than the item's own
|
||||||
|
match masked_by {
|
||||||
|
Some((attribute, selector)) => render.parent_tag("g", |attributes| attributes.push(attribute, selector), render_item),
|
||||||
|
None => render_item(render),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1382,9 +1396,8 @@ impl Render for List<Graphic<'_>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
|
/// Emits one lane of a vector source as SVG, with no wrapping group of its own.
|
||||||
for index in 0..source.lane_count() {
|
fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: usize, vector: &Vector, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
let Some(vector) = source.element(index) else { continue };
|
|
||||||
let item_transform: DAffine2 = source.attr::<Transform>(index);
|
let item_transform: DAffine2 = source.attr::<Transform>(index);
|
||||||
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
|
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
|
||||||
let opacity_attr: f64 = source.attr::<Opacity>(index);
|
let opacity_attr: f64 = source.attr::<Opacity>(index);
|
||||||
@@ -1592,13 +1605,62 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
|
let mut clip_mask_state: Option<(u64, MaskType)> = None;
|
||||||
|
|
||||||
|
for index in 0..source.lane_count() {
|
||||||
|
let Some(vector) = source.element(index) else { continue };
|
||||||
|
|
||||||
|
// A clip-flagged item is masked by its nearest preceding unflagged sibling, which a consecutive run shares
|
||||||
|
let next_clips = index + 1 < source.lane_count() && source.attr::<ClippingMask>(index + 1);
|
||||||
|
let mut masked_by = None;
|
||||||
|
|
||||||
|
if next_clips && clip_mask_state.is_none() {
|
||||||
|
let mask_type = if vector_lane_can_reduce_to_clip_path(source, index) { MaskType::Clip } else { MaskType::Mask };
|
||||||
|
let uuid = generate_uuid();
|
||||||
|
|
||||||
|
let mut masker_svg = SvgRender::new();
|
||||||
|
render_vector_item_svg(source, index, vector, &mut masker_svg, &render_params.for_clipper());
|
||||||
|
render.svg_defs.push_str(&masker_svg.svg_defs);
|
||||||
|
mask_type.write_to_defs(&mut render.svg_defs, uuid, masker_svg.svg.to_svg_string());
|
||||||
|
|
||||||
|
clip_mask_state = Some((uuid, mask_type));
|
||||||
|
} else if let Some((uuid, mask_type)) = clip_mask_state {
|
||||||
|
if !next_clips {
|
||||||
|
clip_mask_state = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
|
masked_by = Some((mask_type.to_attribute(), format!("url(#mask-{uuid})")));
|
||||||
for index in 0..source.lane_count() {
|
}
|
||||||
|
|
||||||
|
// Item geometry is baked into the path data instead of a group transform, so mask coordinates line up
|
||||||
|
match masked_by {
|
||||||
|
Some((attribute, selector)) => render.parent_tag(
|
||||||
|
"g",
|
||||||
|
|attributes| attributes.push(attribute, selector),
|
||||||
|
|render| render_vector_item_svg(source, index, vector, render, render_params),
|
||||||
|
),
|
||||||
|
None => render_vector_item_svg(source, index, vector, render, render_params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits one lane of a vector source to Vello. `clip_masker` names the lane whose
|
||||||
|
/// paint masks this one; its layers are pushed inside this item's blend layer so the
|
||||||
|
/// mask cuts this item's own paint rather than the composited result.
|
||||||
|
fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
|
||||||
|
source: &S,
|
||||||
|
index: usize,
|
||||||
|
scene: &mut Scene,
|
||||||
|
parent_transform: DAffine2,
|
||||||
|
context: &mut RenderContext,
|
||||||
|
render_params: &RenderParams,
|
||||||
|
clip_masker: Option<usize>,
|
||||||
|
) {
|
||||||
use graphic_types::vector_types::vector;
|
use graphic_types::vector_types::vector;
|
||||||
|
|
||||||
let Some(element) = source.element(index) else { continue };
|
let Some(element) = source.element(index) else { return };
|
||||||
let item_transform: DAffine2 = source.attr::<Transform>(index);
|
let item_transform: DAffine2 = source.attr::<Transform>(index);
|
||||||
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
|
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
|
||||||
let opacity_attr: f64 = source.attr::<Opacity>(index);
|
let opacity_attr: f64 = source.attr::<Opacity>(index);
|
||||||
@@ -1646,22 +1708,43 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
|
|||||||
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());
|
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());
|
||||||
|
|
||||||
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;
|
||||||
if opacity < 1. || blend_mode_attr != BlendMode::default() {
|
let needs_blend_layer = opacity < 1. || blend_mode_attr != BlendMode::default();
|
||||||
layer = true;
|
|
||||||
|
// Shared by the blend and clipping layers below, so it is only worth deriving when one of them is pushed
|
||||||
|
let layer_geometry = (needs_blend_layer || clip_masker.is_some()).then(|| {
|
||||||
// `max_aabb_inflation` is in `applied_stroke_transform`-space; `layer_bounds` is path-local and `push_layer` re-applies `multiplied_transform`.
|
// `max_aabb_inflation` is in `applied_stroke_transform`-space; `layer_bounds` is path-local and `push_layer` re-applies `multiplied_transform`.
|
||||||
// Divide by the smaller axial scale to cover the stroke in both axes after Vello's transform. Skip on a degenerate transform.
|
// Divide by the smaller axial scale to cover the stroke in both axes after Vello's transform. Skip on a degenerate transform.
|
||||||
let (_, smallest_scale) = singular_values(applied_stroke_transform);
|
let (_, smallest_scale) = singular_values(applied_stroke_transform);
|
||||||
let stroke_inflation = stroke.map_or(0., |s| s.max_aabb_inflation(can_draw_aligned_stroke));
|
let stroke_inflation = stroke.map_or(0., |s| s.max_aabb_inflation(can_draw_aligned_stroke));
|
||||||
let inflate_amount = if smallest_scale > 0. { stroke_inflation / smallest_scale } else { 0. };
|
let inflate_amount = if smallest_scale > 0. { stroke_inflation / smallest_scale } else { 0. };
|
||||||
let quad = Quad::from_box(layer_bounds).inflate(inflate_amount);
|
let bounds = Quad::from_box(layer_bounds).inflate(inflate_amount).bounding_box();
|
||||||
let layer_bounds = quad.bounding_box();
|
|
||||||
|
(
|
||||||
|
kurbo::Affine::new(multiplied_transform.to_cols_array()),
|
||||||
|
kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if needs_blend_layer && let Some((layer_affine, layer_rect)) = layer_geometry {
|
||||||
|
layer = true;
|
||||||
|
scene.push_layer(peniko::Fill::NonZero, peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver), opacity, layer_affine, &layer_rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pushed inside the blend layer so the mask cuts this item's own paint rather than the composited result
|
||||||
|
let mut clip_layers = false;
|
||||||
|
if let Some(masker_index) = clip_masker
|
||||||
|
&& let Some((layer_affine, layer_rect)) = layer_geometry
|
||||||
|
{
|
||||||
|
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., layer_affine, &layer_rect);
|
||||||
|
render_vector_item_vello(source, masker_index, scene, parent_transform, context, &render_params.for_clipper(), None);
|
||||||
scene.push_layer(
|
scene.push_layer(
|
||||||
peniko::Fill::NonZero,
|
peniko::Fill::NonZero,
|
||||||
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
|
peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcIn),
|
||||||
opacity,
|
1.,
|
||||||
kurbo::Affine::new(multiplied_transform.to_cols_array()),
|
layer_affine,
|
||||||
&kurbo::Rect::new(layer_bounds[0].x, layer_bounds[0].y, layer_bounds[1].x, layer_bounds[1].y),
|
&layer_rect,
|
||||||
);
|
);
|
||||||
|
clip_layers = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let use_layer = can_draw_aligned_stroke;
|
let use_layer = can_draw_aligned_stroke;
|
||||||
@@ -1862,22 +1945,39 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if clip_layers {
|
||||||
|
scene.pop_layer();
|
||||||
|
scene.pop_layer();
|
||||||
|
}
|
||||||
|
|
||||||
// If we pushed a layer for opacity or a blend mode, we need to pop it
|
// If we pushed a layer for opacity or a blend mode, we need to pop it
|
||||||
if layer {
|
if layer {
|
||||||
scene.pop_layer();
|
scene.pop_layer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
|
||||||
|
let mut clip_masker: Option<usize> = None;
|
||||||
|
|
||||||
|
for index in 0..source.lane_count() {
|
||||||
|
// A clip-flagged item is masked by its nearest preceding unflagged sibling, which a consecutive run shares
|
||||||
|
let next_clips = index + 1 < source.lane_count() && source.attr::<ClippingMask>(index + 1);
|
||||||
|
let becomes_masker = next_clips && clip_masker.is_none();
|
||||||
|
|
||||||
|
render_vector_item_vello(source, index, scene, parent_transform, context, render_params, if becomes_masker { None } else { clip_masker });
|
||||||
|
|
||||||
|
if becomes_masker {
|
||||||
|
clip_masker = Some(index);
|
||||||
|
} else if !next_clips {
|
||||||
|
clip_masker = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
|
||||||
// Aggregate all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) produce hit areas for every glyph.
|
// Aggregate all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) 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 the first item carrying each element_id, since that is the transform recorded as its `local_transforms` entry.
|
||||||
let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::<Transform>(0) } else { DAffine2::IDENTITY };
|
let mut reference_transforms: HashMap<NodeId, DAffine2> = HashMap::new();
|
||||||
let item_zero_inverse = if transform_is_invertible(item_zero_transform) {
|
|
||||||
item_zero_transform.inverse()
|
|
||||||
} else {
|
|
||||||
DAffine2::IDENTITY
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
||||||
@@ -1889,18 +1989,17 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
|
|||||||
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) {
|
||||||
// When recovering element_id from the item's editor:layer_path tag (because the caller
|
let reference_transform = *reference_transforms.entry(element_id).or_insert(transform);
|
||||||
// passed None), also store the transform metadata that Graphic::collect_metadata
|
let reference_inverse = if transform_is_invertible(reference_transform) {
|
||||||
// normally provides but skipped due to the None element_id.
|
reference_transform.inverse()
|
||||||
if caller_element_id.is_none() {
|
} else {
|
||||||
metadata.upstream_footprints.entry(element_id).or_insert(footprint);
|
DAffine2::IDENTITY
|
||||||
metadata.local_transforms.entry(element_id).or_insert(item_zero_transform);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = source.attr::<EditorClickTarget>(index).unwrap_or(element);
|
let click_target_vector = source.attr::<EditorClickTarget>(index).unwrap_or(element);
|
||||||
|
|
||||||
let item_relative_transform = item_zero_inverse * transform;
|
let item_relative_transform = reference_inverse * transform;
|
||||||
|
|
||||||
let mut click_targets_unwrapped = Vec::new();
|
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, source, index, click_target_vector, item_relative_transform);
|
||||||
@@ -1949,6 +2048,15 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
|
|||||||
for (element_id, targets) in accumulated_outlines {
|
for (element_id, targets) in accumulated_outlines {
|
||||||
metadata.outlines.insert(element_id, targets);
|
metadata.outlines.insert(element_id, targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recovering element_id from `editor:layer_path` means `Graphic::collect_metadata` skipped this transform metadata.
|
||||||
|
// It lands after the snapshot recursion above so each element keeps the pair its targets were baked against.
|
||||||
|
if caller_element_id.is_none() {
|
||||||
|
for (element_id, reference_transform) in reference_transforms {
|
||||||
|
metadata.upstream_footprints.insert(element_id, footprint);
|
||||||
|
metadata.local_transforms.insert(element_id, reference_transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, click_targets: &mut Vec<ClickTarget>) {
|
||||||
@@ -2218,6 +2326,17 @@ fn add_raster_upstream_click_targets(click_targets: &mut Vec<ClickTarget>) {
|
|||||||
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
|
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_raster_lane_upstream_click_targets<S: LaneSource>(source: &S, click_targets: &mut Vec<ClickTarget>) {
|
||||||
|
for index in 0..source.lane_count() {
|
||||||
|
// The unit square is the raster's own space, so its placement only exists in the item transform
|
||||||
|
let transform: DAffine2 = source.attr::<Transform>(index);
|
||||||
|
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
||||||
|
subpath.apply_transform(transform);
|
||||||
|
|
||||||
|
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Render for List<Raster<CPU>> {
|
impl Render for List<Raster<CPU>> {
|
||||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
render_raster_cpu_svg(self, render, render_params)
|
render_raster_cpu_svg(self, render, render_params)
|
||||||
@@ -2232,7 +2351,7 @@ impl Render for List<Raster<CPU>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||||
add_raster_upstream_click_targets(click_targets)
|
add_raster_lane_upstream_click_targets(self, click_targets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2310,7 +2429,7 @@ impl Render for List<Raster<GPU>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||||
add_raster_upstream_click_targets(click_targets)
|
add_raster_lane_upstream_click_targets(self, click_targets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3084,7 +3203,7 @@ impl Render for RunView<'_, Raster<CPU>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||||
add_raster_upstream_click_targets(click_targets)
|
add_raster_lane_upstream_click_targets(self, click_targets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use core_types::registry::types::Angle;
|
|||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, ExtractIndex, InjectIndex};
|
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, ExtractIndex, InjectIndex};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::graphic::{Graphic, GraphicLevel, RowStep, TryFromGraphic, walk_vector_rows};
|
use graphic_types::graphic::{Graphic, GraphicLevel, RowStep, TryFromGraphic, is_lone_anonymous_leaf, walk_vector_rows};
|
||||||
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
|
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
|
||||||
use graphic_types::{ATTR_FILL, ATTR_STROKE, Vector};
|
use graphic_types::{ATTR_FILL, ATTR_STROKE, Vector};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
@@ -488,7 +488,8 @@ pub fn flatten_vector<'e>(
|
|||||||
let Some((row, top)) = locate_vector_row(GraphicLevel::Run(&item), lane) else {
|
let Some((row, top)) = locate_vector_row(GraphicLevel::Run(&item), lane) else {
|
||||||
return Err(GraphError::past_end().into());
|
return Err(GraphError::past_end().into());
|
||||||
};
|
};
|
||||||
let snapshot = (lane == 0).then(|| legacy_render_list_of(content));
|
// A lone anonymous leaf flattens to itself, so there is no erased structure to snapshot
|
||||||
|
let snapshot = (lane == 0).then(|| legacy_render_list_of(content)).filter(|list| !is_lone_anonymous_leaf(list));
|
||||||
emit_vector_row(ctx.arena(), content.lane(top), row, snapshot)
|
emit_vector_row(ctx.arena(), content.lane(top), row, snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user