M5: opaque knockout fills draw once in hard-edge and supersampled raster export

A knockout element with one opaque Normal solid fill (a path or
compound, no mask, effects or isolation) is drawn once under
AntiAlias::None, including the internal hard-edge frame of Art
Optimized supersampled exports, instead of being erased and added back
through per-element backdrop layers. Analytical antialiasing keeps the
existing compositor and rounding.

Exact-pixel tests compare against the general knockout path for paths
and compound holes across precision, supersampling, translucent parents
and copied backdrops, and check that the backdrop rasters are skipped;
translucent fills keep their knockout compositing.

# Conflicts:
#	docs/roadmap.md
#	docs/target-app-parity.md
This commit is contained in:
Don Omondi
2026-10-31 06:28:42 +07:00
committed by bflatastic
parent de72fc3998
commit de703fe722
5 changed files with 124 additions and 3 deletions
+17
View File
@@ -25,6 +25,23 @@ pub(crate) fn single_plain_fill(n: &Node) -> bool {
matches!((painted.next(), painted.next()), (Some(AppearanceItem::Fill(f)), None) if f.blend == vectorcraft_color::BlendMode::Normal && f.opacity >= 1.0 && !matches!(f.paint, vectorcraft_color::Paint::Pattern { .. }))
}
/// A knockout element whose opaque paint covers exactly its knockout shape. The caller limits
/// source-over substitution to binary coverage, retaining analytical AA's intermediate rounding.
pub(crate) fn opaque_knockout_fill(n: &Node) -> bool {
matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. })
&& n.opacity >= 1.0
&& n.blend == vectorcraft_color::BlendMode::Normal
&& !n.isolate
&& n.extras().mask.is_none()
&& !crate::fx::has_fx(n)
&& !crate::fx::has_object_fx(n)
&& single_plain_fill(n)
&& n.appearance
.items
.iter()
.any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && matches!(f.paint, vectorcraft_color::Paint::Solid { .. })))
}
/// Number of visible painted fill/stroke items (opacity folding is exact only for one). A dashed
/// stroke is one paint: its dashes are outlined into one path and filled once, so overlapping
/// dashes don't darken where they overlap. A dashed brush stroke draws its pieces one by one, so