Render: a mask art root, symbol art root or thumbnail keeps its own opacity mask

The renderer drew the root node of an opacity mask's art, of placed symbol
art, of an art brush piece, of an Image Trace source image and of a node
thumbnail without its own opacity mask. Make Opacity Mask uses the top
object as the mask art, so a mask made from an object that already had an
opacity mask ignored that inner mask. These roots now draw through the
same masked group as any other node, still drawn whatever their visibility
and bounds, and an incomplete inner mask still leaves out the outer object.
This commit is contained in:
dotCooCoo
2026-10-31 19:43:15 +07:00
committed by bflatastic
parent b1ed0a512b
commit 8aa8bb4d9d
4 changed files with 184 additions and 17 deletions
+31 -15
View File
@@ -157,23 +157,39 @@ impl Renderer {
self.stats.drawn += 1;
return;
}
if let Some(m) = a.extras().mask.as_deref()
&& !m.disabled
&& !f.opts.outline
&& !self.is_shape(a)
{
// A masked object is a non-isolated group of its own, drawn over its own pixels: the
// mask is rendered over those only.
let bounds = self.bounds_of(f.symbols, a);
let Some((r, sub)) = group::area(ctx, f, bounds) else { return };
let Some(mask) = self.mask_values(&sub, m, r.w, r.h) else { return };
let blends = a.blend != vectorcraft_color::BlendMode::Normal || (!a.isolate && self.blends_through(a));
self.group(ctx, f, Composite { mask: Some(mask), blends, bounds, ..Default::default() }, &mut |r, c, fr| r.draw_arc_body(c, fr, a));
if self.draw_masked_arc(ctx, f, a, false) {
return;
}
self.draw_arc_body(ctx, f, a);
}
/// Draw `a` through its opacity mask when it has one. Forced roots draw regardless of their
/// visibility and bounds.
fn draw_masked_arc(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, force: bool) -> bool {
let Some(m) = a.extras().mask.as_deref().filter(|m| !m.disabled && !f.opts.outline && !self.is_shape(a)) else { return false };
// A masked object is a non-isolated group of its own, drawn over its own pixels: the
// mask is rendered over those only.
let bounds = self.bounds_of(f.symbols, a);
let Some((r, sub)) = group::area(ctx, f, bounds) else { return true };
let Some(mask) = self.mask_values(&sub, m, r.w, r.h) else { return true };
let blends = a.blend != vectorcraft_color::BlendMode::Normal || (!a.isolate && self.blends_through(a));
self.group(ctx, f, Composite { mask: Some(mask), blends, bounds, ..Default::default() }, &mut |r, c, fr| {
if force {
r.draw_node(c, fr, a, true);
} else {
r.draw_arc_body(c, fr, a);
}
});
true
}
/// Draw an `Arc` regardless of its visibility and bounds, applying its opacity mask.
pub(crate) fn draw_forced_arc(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>) {
if !self.draw_masked_arc(ctx, f, a, true) {
self.draw_node(ctx, f, a, true);
}
}
/// The coverage of opacity mask `m` per output pixel (row-major): its art rendered offscreen,
/// as luminance ([`mask_value`]).
pub(super) fn mask_values(&mut self, f: &Frame, m: &vectorcraft_doc::OpacityMask, w: u16, h: u16) -> Option<Vec<u8>> {
@@ -489,7 +505,7 @@ impl Renderer {
if kept {
self.draw_kept_node(ctx, f, art);
} else {
self.draw_node(ctx, f, art, true);
self.draw_forced_arc(ctx, f, art);
}
self.made_again = outer;
}
@@ -500,7 +516,7 @@ impl Renderer {
pub(crate) fn draw_kept_node(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>) {
let live = vectorcraft_doc::live::is_live(a).then(|| a.clone());
let outer = std::mem::replace(&mut self.live_root, live);
self.draw_node(ctx, f, a, true);
self.draw_forced_arc(ctx, f, a);
self.live_root = outer;
}
@@ -518,7 +534,7 @@ impl Renderer {
fn draw_trace_view(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>], view: TraceView, knockout: bool) {
let Some((source, shapes)) = children.split_first() else { return };
if view.shows_source() && matches!(source.kind, NodeKind::Image(_)) {
self.draw_node(ctx, f, source, true);
self.draw_forced_arc(ctx, f, source);
}
if view.shows_result() {
self.draw_children(ctx, f, shapes, knockout);