mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
M8: a group's effects reach past what its children paint
A group or an unclipped frame applies its layer blur, drop shadows and texture to everything its children paint, including children reaching past its box. The renderer sized the container's layer from the union of its children without growing it by the container's own effects, so the blur or shadow was cut where a child's paint ended; exports of unclipped frames lost part of the shadow the same way. bounds_within now grows the children's union by the node's effects reach, and render_bounds_at grows an unclipped frame's children by its effects. The retained partial-render test drew this very cut (61 levels in its group child scene); with the shadow counted the damage passes the partial share of its 300 x 220 view, so the view is widened to keep it a partial render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
019058ffd5
commit
81f89b314b
@@ -738,7 +738,8 @@ impl Document {
|
||||
|
||||
/// Bounds of everything painted by a node, effects and strokes included (page coordinates):
|
||||
/// a clipping frame's own area, else that and what its visible children show (masked
|
||||
/// children only within their mask). A group is what its children show, grown by its effects.
|
||||
/// children only within their mask). A group, or an unclipped frame past its box, is what its
|
||||
/// children show grown by its effects, which apply to all of it.
|
||||
pub fn world_render_bounds(&self, id: NodeId) -> Option<Rect> {
|
||||
self.render_bounds_at(id, 0)
|
||||
}
|
||||
@@ -751,7 +752,8 @@ impl Document {
|
||||
}
|
||||
Some(match (self.children_render_bounds(n, depth + 1), &n.kind) {
|
||||
(Some(kids), NodeKind::Group) => n.grow_by_effects(kids),
|
||||
(Some(kids), _) => own.union(kids),
|
||||
// An unclipped frame's effects apply to its children too, past its box included.
|
||||
(Some(kids), _) => own.union(n.grow_by_effects(kids)),
|
||||
(None, _) => own,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -828,9 +828,14 @@ impl Node {
|
||||
|
||||
/// Maximum distance (in local units) painting can reach outside the node box.
|
||||
pub fn paint_reach(&self) -> f64 {
|
||||
let layer = self.layer_effects_reach();
|
||||
self.outline_reach() + self.effects_reach() + self.spread_miter_reach()
|
||||
}
|
||||
|
||||
/// How far (local units) this node's effects paint past what they apply to: the layer effects
|
||||
/// together (see [`Node::layer_effects_reach`]), then the largest of the others.
|
||||
pub fn effects_reach(&self) -> f64 {
|
||||
let fx = self.effects.iter().filter(|e| !e.moves_the_layer()).map(|e| effect_reach(e.reach())).fold(0.0, f64::max);
|
||||
self.outline_reach() + layer + fx + self.spread_miter_reach()
|
||||
self.layer_effects_reach() + fx
|
||||
}
|
||||
|
||||
/// How far the layer blurs and texture move the layer's pixels together: they apply one after
|
||||
|
||||
@@ -444,13 +444,20 @@ pub(crate) fn bounds_within<'a>(
|
||||
if (!clips || n.kind.bounds_follow_children()) && !n.hides_contents() {
|
||||
if depth < MAX_BOUNDS_DEPTH {
|
||||
let mut below = Some(0);
|
||||
let mut kids: Option<Rect> = None;
|
||||
for c in n.children.iter().filter_map(|c| memo.map_or_else(|| doc.node(*c), |m| m.node(doc, *c))) {
|
||||
if !c.visible {
|
||||
continue;
|
||||
}
|
||||
let (cb, c_levels) = bounds_within(doc, c, depth + 1, greek_scale, memo);
|
||||
below = below.zip(c_levels).map(|(b, l)| b.max(l));
|
||||
r = r.union(figcraft_geom::transform_rect_bbox(c.transform, cb));
|
||||
let cb = figcraft_geom::transform_rect_bbox(c.transform, cb);
|
||||
kids = Some(kids.map_or(cb, |k| k.union(cb)));
|
||||
}
|
||||
// This layer's effects apply to what its children paint, past its box included.
|
||||
if let Some(k) = kids {
|
||||
let e = n.effects_reach();
|
||||
r = r.union(if e > 0.0 { k.inflate(e, e) } else { k });
|
||||
}
|
||||
levels = below.map(|b| b + 1);
|
||||
} else if !n.children.is_empty() {
|
||||
|
||||
@@ -228,6 +228,59 @@ fn effects_on_a_group_apply_to_the_group_as_a_whole() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A group's or an unclipped frame's effects apply to what its children paint, children reaching
|
||||
/// past its box included: its layer holds that too, grown by the effects, so nothing is cut where
|
||||
/// a child's paint ends; and an unclipped frame's export holds it.
|
||||
#[test]
|
||||
fn a_containers_effects_reach_past_what_its_children_paint() {
|
||||
let blur = || fx(EffectKind::LayerBlur(Blur::uniform(20.0)));
|
||||
let shadow = |dy: f64| fx(EffectKind::DropShadow(sharp((0.0, dy), 0.0)));
|
||||
// (why, container, its effects, the child's box, the child's effects)
|
||||
let rows = [
|
||||
("a blurred group over a child's shadow", NodeKind::Group, vec![blur()], (20.0, 20.0, 60.0, 60.0), vec![shadow(80.0)]),
|
||||
("an unclipped frame's shadow of a child sticking out", NodeKind::Frame, vec![shadow(40.0)], (60.0, 60.0, 80.0, 80.0), vec![]),
|
||||
("an unclipped frame's blur of a child sticking out", NodeKind::Frame, vec![blur()], (60.0, 60.0, 80.0, 80.0), vec![]),
|
||||
];
|
||||
for (why, kind, effects, (x, y, w, h), child_fx) in rows {
|
||||
// `room`: an empty child as large as the view, so the layer can't be cut: what it should draw.
|
||||
let scene = |room: bool| {
|
||||
let (mut d, p) = page();
|
||||
let c = d.create(p, kind.clone(), "C", 0.0, 0.0, 100.0, 100.0).unwrap();
|
||||
let node = d.get_mut(c).unwrap();
|
||||
node.clip = false;
|
||||
node.fills.clear();
|
||||
node.effects = effects.clone();
|
||||
let r = rect(&mut d, c, (x, y, w, h), vec![Paint::solid(hex("FF0000"))]);
|
||||
d.get_mut(r).unwrap().effects = child_fx.clone();
|
||||
if room {
|
||||
rect(&mut d, c, (-200.0, -200.0, 600.0, 600.0), vec![]);
|
||||
}
|
||||
(d, c)
|
||||
};
|
||||
let ((d, c), (truth, _)) = (scene(false), scene(true));
|
||||
let v = View { origin: (-100.0, -100.0), scale: 1.0, width: 400, height: 400 };
|
||||
let (a, b) = (
|
||||
render_view_single(&d, page_of(&d), &v, &Options::default()).unwrap(),
|
||||
render_view_single(&truth, page_of(&truth), &v, &Options::default()).unwrap(),
|
||||
);
|
||||
let worst = a.data().iter().zip(b.data()).map(|(x, y)| x.abs_diff(*y)).max().unwrap_or(0);
|
||||
// The export holds all of it: as much paint as the export with room to spare.
|
||||
let ink = |p: Pixmap| p.pixels().iter().map(|c| u64::from(c.alpha())).sum::<u64>();
|
||||
let (got, all) = (ink(render_node(&d, c, 1.0).unwrap()), ink(render_node(&truth, page_child(&truth), 1.0).unwrap()));
|
||||
assert!(worst <= 1, "{why}: the canvas draws {worst} levels off");
|
||||
assert!(got * 1000 >= all * 999, "{why}: the export holds {got} of {all} of the paint");
|
||||
}
|
||||
}
|
||||
|
||||
fn page_of(d: &Document) -> NodeId {
|
||||
d.first_page().unwrap()
|
||||
}
|
||||
|
||||
/// The page's first layer.
|
||||
fn page_child(d: &Document) -> NodeId {
|
||||
*d.node(page_of(d)).unwrap().children.first().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_shadows_paint_inside_over_the_fills_under_strokes_and_children() {
|
||||
let green = [0, 255, 0];
|
||||
|
||||
@@ -187,9 +187,10 @@ fn edits_under_and_beside_effects_match() {
|
||||
assert!(partial(check(&mut r, &across, s.page, &v)));
|
||||
let beside = edit(&across, |d| d.get_mut(s.rect).unwrap().transform = Affine::translate((10.0, 110.0)));
|
||||
assert!(partial(check(&mut r, &beside, s.page, &v)));
|
||||
// A child of a group with a drop shadow: the shadow moves too.
|
||||
// A child of a group with a drop shadow: the shadow moves too, past the group's box with the
|
||||
// child (a view large enough that this stays a part of it).
|
||||
let g1 = s.doc.find_by_name("G1", None)[0];
|
||||
let v = View { origin: (260.0, 0.0), scale: 1.0, width: 300, height: 220 };
|
||||
let v = View { origin: (260.0, 0.0), scale: 1.0, width: 400, height: 300 };
|
||||
check(&mut r, &beside, s.page, &v);
|
||||
let shadowed = edit(&beside, |d| d.get_mut(g1).unwrap().transform = Affine::translate((30.0, 20.0)));
|
||||
assert!(partial(check(&mut r, &shadowed, s.page, &v)));
|
||||
|
||||
Reference in New Issue
Block a user