mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +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:
committed by
Dennis Kobert
parent
ae1ff71f45
commit
1c88592fa4
@@ -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 paint::{
|
||||
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};
|
||||
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`)
|
||||
/// 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> {
|
||||
// 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]>) {
|
||||
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.
|
||||
@@ -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.
|
||||
// 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) => {
|
||||
// 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 {
|
||||
for v in sub_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*v = current_transform * *v;
|
||||
@@ -573,6 +598,45 @@ mod tests {
|
||||
use super::*;
|
||||
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> {
|
||||
Graphic::Vector(Vector::default())
|
||||
}
|
||||
|
||||
@@ -37,20 +37,23 @@ where
|
||||
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.
|
||||
pub fn vector_lane_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S, index: usize) -> bool {
|
||||
let Some(element) = source.element(index) else { return false };
|
||||
let opacity: f64 = source.attr::<Opacity>(index);
|
||||
|
||||
let fill_opaque_or_absent = paint_graphics::<Fill, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
|
||||
|
||||
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
||||
|| paint_graphics::<Stroke, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
||||
|
||||
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
|
||||
}
|
||||
|
||||
/// 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| {
|
||||
let Some(element) = source.element(index) else { return false };
|
||||
let opacity: f64 = source.attr::<Opacity>(index);
|
||||
|
||||
let fill_opaque_or_absent = paint_graphics::<Fill, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
|
||||
|
||||
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
||||
|| paint_graphics::<Stroke, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
||||
|
||||
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
|
||||
})
|
||||
(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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ use core_types::registry::types::Angle;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, ExtractIndex, InjectIndex};
|
||||
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::{ATTR_FILL, ATTR_STROKE, Vector};
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user