mirror of
https://github.com/penpot/penpot.git
synced 2026-09-19 02:16:14 +00:00
🐛 Skip content clip on WASM SVG drop silhouettes (#11655)
Clipped frames (show-content=false) were applying clipPath to the drop silhouette. Spread/offset fills were outset correctly but then truncated to the true selrect, so spread rings vanished on export. Match the GPU: paint drop silhouettes outside the content clip; keep clipping on the real content pass only. Closes #11653
This commit is contained in:
parent
b8af89c1b8
commit
fcae641c7b
@ -72,7 +72,10 @@ fn render_frame_body(
|
|||||||
scale: f32,
|
scale: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let spread = builder.silhouette_spread;
|
let spread = builder.silhouette_spread;
|
||||||
let clipped = element.clip_content;
|
// Clip only on the real content pass. Drop silhouettes are painted outside
|
||||||
|
// the content clip (GPU), so spread/offset rings are not truncated by an
|
||||||
|
// un-outset / unshifted clipPath.
|
||||||
|
let clipped = element.clip_content && !builder.suppress_filters;
|
||||||
if clipped {
|
if clipped {
|
||||||
let clip_id = builder.unique("clip");
|
let clip_id = builder.unique("clip");
|
||||||
builder.push_clip_path(&clip_id, element, tree);
|
builder.push_clip_path(&clip_id, element, tree);
|
||||||
|
|||||||
@ -4,13 +4,11 @@ expression: svg
|
|||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="144" viewBox="0 0 200 144"><defs><filter id="fx0" filterUnits="userSpaceOnUse" x="0" y="0" width="200" height="144" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="alpha"/><feOffset in="alpha" dx="0" dy="0" result="off"/><feGaussianBlur in="off" stdDeviation="0" result="blurred"/><feColorMatrix in="blurred" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.54901963 0" result="colored"/><feBlend mode="normal" in="colored" in2="bg" result="drop0"/></filter><clipPath id="clip1" clipPathUnits="userSpaceOnUse">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="144" viewBox="0 0 200 144"><defs><filter id="fx0" filterUnits="userSpaceOnUse" x="0" y="0" width="200" height="144" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="alpha"/><feOffset in="alpha" dx="0" dy="0" result="off"/><feGaussianBlur in="off" stdDeviation="0" result="blurred"/><feColorMatrix in="blurred" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.54901963 0" result="colored"/><feBlend mode="normal" in="colored" in2="bg" result="drop0"/></filter><clipPath id="clip1" clipPathUnits="userSpaceOnUse">
|
||||||
<rect transform="translate(0 24)" width="200" height="120"/>
|
|
||||||
</clipPath><clipPath id="clip2" clipPathUnits="userSpaceOnUse">
|
|
||||||
<rect width="200" height="120"/>
|
<rect width="200" height="120"/>
|
||||||
</clipPath></defs><g filter="url(#fx0)"><g clip-path="url(#clip1)">
|
</clipPath></defs><g filter="url(#fx0)">
|
||||||
<rect fill="#F0F0F0" transform="translate(0 24)" width="200" height="120"/>
|
<rect fill="#F0F0F0" transform="translate(0 24)" width="200" height="120"/>
|
||||||
<rect fill="#00C800" transform="translate(0 24)" x="20" y="20" width="160" height="80"/>
|
<rect fill="#00C800" transform="translate(0 24)" x="20" y="20" width="160" height="80"/>
|
||||||
</g></g><g clip-path="url(#clip2)">
|
</g><g clip-path="url(#clip1)">
|
||||||
<rect fill="#F0F0F0" width="200" height="120"/>
|
<rect fill="#F0F0F0" width="200" height="120"/>
|
||||||
<rect fill="#00C800" x="20" y="20" width="160" height="80"/>
|
<rect fill="#00C800" x="20" y="20" width="160" height="80"/>
|
||||||
</g></svg>
|
</g></svg>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
source: src/render/svg/tests.rs
|
||||||
|
expression: svg
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="248" height="168" viewBox="0 0 248 168"><defs><filter id="fx0" filterUnits="userSpaceOnUse" x="0" y="0" width="248" height="168" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="bg"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="alpha"/><feOffset in="alpha" dx="0" dy="0" result="off"/><feGaussianBlur in="off" stdDeviation="0" result="blurred"/><feColorMatrix in="blurred" type="matrix" values="0 0 0 0 0.8980392 0 0 0 0 0.0627451 0 0 0 0 0.13725491 0 0 0 1 0" result="colored"/><feBlend mode="normal" in="colored" in2="bg" result="drop0"/></filter><clipPath id="clip1" clipPathUnits="userSpaceOnUse">
|
||||||
|
<rect transform="translate(24 24)" width="200" height="120"/>
|
||||||
|
</clipPath></defs><g filter="url(#fx0)">
|
||||||
|
<rect fill="#F0F0F0" transform="translate(24 24)" x="-24" y="-24" width="248" height="168"/>
|
||||||
|
<rect fill="#00C800" transform="translate(24 24)" x="-4" y="-4" width="208" height="128"/>
|
||||||
|
</g><g clip-path="url(#clip1)">
|
||||||
|
<rect fill="#F0F0F0" transform="translate(24 24)" width="200" height="120"/>
|
||||||
|
<rect fill="#00C800" transform="translate(24 24)" x="20" y="20" width="160" height="80"/>
|
||||||
|
</g></svg>
|
||||||
@ -500,10 +500,10 @@ fn exports_frame_drop_shadow_wrapping_children() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clipped_frame_drop_shadow_clip_follows_silhouette_offset() {
|
fn clipped_frame_drop_shadow_silhouette_skips_content_clip() {
|
||||||
// clip=ON + drop offset: silhouette fills/children move with
|
// clip=ON + drop offset: GPU paints the drop outside the content clip, so
|
||||||
// silhouette_draw_matrix, so the board clipPath must move too — otherwise
|
// the silhouette must not use clipPath (F1a). Otherwise an unshifted clip
|
||||||
// the unshifted clip truncates the shadow (F1a / show-content=false).
|
// truncates the offset shadow.
|
||||||
let mut pool = ShapesPool::new();
|
let mut pool = ShapesPool::new();
|
||||||
let frame_id = uid(1);
|
let frame_id = uid(1);
|
||||||
let child = uid(2);
|
let child = uid(2);
|
||||||
@ -543,10 +543,6 @@ fn clipped_frame_drop_shadow_clip_follows_silhouette_offset() {
|
|||||||
svg.contains("filter=\"url(#fx"),
|
svg.contains("filter=\"url(#fx"),
|
||||||
"clipped frame drop shadow must emit a filter: {svg}"
|
"clipped frame drop shadow must emit a filter: {svg}"
|
||||||
);
|
);
|
||||||
assert!(
|
|
||||||
svg.matches("<clipPath").count() >= 2,
|
|
||||||
"silhouette and content each need a clipPath: {svg}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let filter_open = svg.find("filter=\"url(#fx").expect("frame filter");
|
let filter_open = svg.find("filter=\"url(#fx").expect("frame filter");
|
||||||
let filter_close = svg[filter_open..]
|
let filter_close = svg[filter_open..]
|
||||||
@ -554,29 +550,78 @@ fn clipped_frame_drop_shadow_clip_follows_silhouette_offset() {
|
|||||||
.map(|i| filter_open + i)
|
.map(|i| filter_open + i)
|
||||||
.expect("silhouette group close");
|
.expect("silhouette group close");
|
||||||
let silhouette = &svg[filter_open..=filter_close];
|
let silhouette = &svg[filter_open..=filter_close];
|
||||||
let clip_ref = silhouette
|
|
||||||
.find("clip-path=\"url(#")
|
|
||||||
.and_then(|i| {
|
|
||||||
let start = i + "clip-path=\"url(#".len();
|
|
||||||
let end = silhouette[start..].find(')')?;
|
|
||||||
Some(&silhouette[start..start + end])
|
|
||||||
})
|
|
||||||
.expect("silhouette must reference a clipPath");
|
|
||||||
|
|
||||||
let clip_def_start = svg
|
|
||||||
.find(&format!("<clipPath id=\"{clip_ref}\""))
|
|
||||||
.expect("silhouette clipPath def");
|
|
||||||
let clip_def_end = svg[clip_def_start..]
|
|
||||||
.find("</clipPath>")
|
|
||||||
.map(|i| clip_def_start + i)
|
|
||||||
.expect("clipPath close");
|
|
||||||
let clip_geom = &svg[clip_def_start..clip_def_end];
|
|
||||||
|
|
||||||
// Content clip (second clipPath) stays unshifted; silhouette clip must
|
|
||||||
// carry the local drop offset (0, 24) like silhouette fills.
|
|
||||||
assert!(
|
assert!(
|
||||||
clip_geom.contains("translate(") && clip_geom.contains(" 24"),
|
!silhouette.contains("clip-path="),
|
||||||
"silhouette clipPath must follow drop offset (0,24): {clip_geom}\nfull: {svg}"
|
"drop silhouette must not use content clip: {silhouette}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
silhouette.contains(r#"translate(0 24)"#),
|
||||||
|
"silhouette fills must still apply drop offset: {silhouette}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
svg.contains("<clipPath") && svg.contains("clip-path=\"url(#"),
|
||||||
|
"content pass must still clip: {svg}"
|
||||||
|
);
|
||||||
|
insta::assert_snapshot!(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clipped_frame_drop_spread_is_not_truncated_by_content_clip() {
|
||||||
|
// clip=ON + spread 24, offset 0: fills outset in the silhouette, but if the
|
||||||
|
// content clipPath stays at the true selrect the red ring is cut away.
|
||||||
|
let mut pool = ShapesPool::new();
|
||||||
|
let frame_id = uid(1);
|
||||||
|
let child = uid(2);
|
||||||
|
add_frame(
|
||||||
|
&mut pool,
|
||||||
|
frame_id,
|
||||||
|
Uuid::nil(),
|
||||||
|
(0.0, 0.0, 200.0, 120.0),
|
||||||
|
skia::Color::from_rgb(240, 240, 240),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let frame = pool.get_mut(&frame_id).unwrap();
|
||||||
|
frame.add_shadow(Shadow::new(
|
||||||
|
skia::Color::from_rgb(229, 16, 35),
|
||||||
|
0.0,
|
||||||
|
24.0,
|
||||||
|
(0.0, 0.0),
|
||||||
|
ShadowStyle::Drop,
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
add_solid_rect(
|
||||||
|
&mut pool,
|
||||||
|
child,
|
||||||
|
frame_id,
|
||||||
|
(20.0, 20.0, 180.0, 100.0),
|
||||||
|
skia::Color::from_rgb(0, 200, 0),
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let frame = pool.get_mut(&frame_id).unwrap();
|
||||||
|
frame.add_child(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = render(&pool, frame_id);
|
||||||
|
let filter_open = svg.find("filter=\"url(#fx").expect("frame filter");
|
||||||
|
let filter_close = svg[filter_open..]
|
||||||
|
.find("</g>")
|
||||||
|
.map(|i| filter_open + i)
|
||||||
|
.expect("silhouette group close");
|
||||||
|
let silhouette = &svg[filter_open..=filter_close];
|
||||||
|
assert!(
|
||||||
|
!silhouette.contains("clip-path="),
|
||||||
|
"spread silhouette must not be content-clipped: {silhouette}"
|
||||||
|
);
|
||||||
|
// 200×120 + 2×24 spread, and child 160×80 + 2×24.
|
||||||
|
assert!(
|
||||||
|
silhouette.contains(r#"width="248""#) && silhouette.contains(r#"height="168""#),
|
||||||
|
"frame fill must outset by spread 24: {silhouette}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
silhouette.contains(r#"width="208""#) && silhouette.contains(r#"height="128""#),
|
||||||
|
"child fill must outset by inherited spread 24: {silhouette}"
|
||||||
);
|
);
|
||||||
insta::assert_snapshot!(svg);
|
insta::assert_snapshot!(svg);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user