Effects: nested groups' Transform effects round a member's direction once

A member's light or stroke direction under groups that each have a
Transform effect is now rounded once for the turn of all of them. The
inner group's members are evaluated first, so each group rounded the
direction again: two 30-degree turns of a top light gave Left instead
of Top Left. While a group's members are evaluated, the group's own
Transform effects are counted among those the members still go through.
This commit is contained in:
dotCooCoo
2026-11-14 20:12:25 -08:00
parent e3b1d4d3eb
commit a327587d64
3 changed files with 62 additions and 4 deletions
+5 -1
View File
@@ -868,7 +868,11 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option<Node> {
}
if !fx.is_empty() {
m = crate::reshape::outlined_members(doc, &m);
members_as_drawn(doc, &mut m);
{
// The members' raster effects then follow the group's own Transform effects as well.
let _later = crate::raster::LaterGroups::enter(&n.appearance.effects);
members_as_drawn(doc, &mut m);
}
let carry = crate::reshape::reshape_leaves_carrying(&mut m, &fx, &n.appearance.effects, doc);
// The group's own fills and strokes paint the members as a whole: their gradients follow
// the effects as the members' do.
+34 -3
View File
@@ -613,12 +613,43 @@ pub(crate) fn follows(id: &str) -> bool {
crate::is_raster(id) && id != crate::SVG_FILTER && id != RASTERIZE
}
thread_local! {
/// The linear part of the Transform effects of the groups and layers whose members are being
/// evaluated ([`LaterGroups`]), which those groups apply to the members' raster effects once
/// the members are evaluated: the members' light and stroke directions are rounded for them as
/// well ([`follow_transform`]).
static LATER_GROUPS: std::cell::Cell<Affine> = const { std::cell::Cell::new(Affine::IDENTITY) };
}
/// The Transform effects of a group or layer whose members are being evaluated before the group's
/// own effects are applied to them, added to [`LATER_GROUPS`] until it is dropped.
pub(crate) struct LaterGroups(Affine);
impl LaterGroups {
/// Add the Transform effects of `owner`, a group's or layer's own effects ([`contents_step`]),
/// to those the members will still go through.
pub(crate) fn enter(owner: &[Effect]) -> Self {
let was = LATER_GROUPS.with(|l| l.get());
if let Some(step) = contents_step(owner) {
LATER_GROUPS.with(|l| l.set(was * step));
}
LaterGroups(was)
}
}
impl Drop for LaterGroups {
fn drop(&mut self) {
LATER_GROUPS.with(|l| l.set(self.0));
}
}
/// Make raster effect `e` follow `a`, the linear part of a Transform effect listed after it, as
/// that effect transforms what `e` paints: its distances and raster sizes scale by `a`'s mean
/// scale ([`crate::scale_effect`]), a Drop Shadow's offset turns, reflects and scales with `a`,
/// and a pixel filter's light direction, stroke direction or screen angles turn and reflect with
/// it ([`pixel::turned_directions`]; `later` is the map `e` still goes through when it is drawn).
/// Effects that [`follows`] leaves out stay as they are.
/// it ([`pixel::turned_directions`]; `later` is the map `e` still goes through when it is drawn,
/// before the Transform effects of the groups around it in [`LATER_GROUPS`]). Effects that
/// [`follows`] leaves out stay as they are.
fn follow_transform(e: &mut Effect, a: Affine, later: Affine) {
if !follows(&e.id) {
return;
@@ -627,7 +658,7 @@ fn follow_transform(e: &mut Effect, a: Affine, later: Affine) {
let p = merged_params(&e.id, &e.params);
a * Point::new(num(&p, "x", 7.0), num(&p, "y", 7.0))
});
let turned = pixel::turned_directions(&e.id, &e.params, a, later);
let turned = pixel::turned_directions(&e.id, &e.params, a, LATER_GROUPS.with(|l| l.get()) * later);
if let Some(k) = vectorcraft_doc::Scaling::factor(a) {
crate::scale_effect(e, k);
}
+23
View File
@@ -580,6 +580,29 @@ fn nested_and_baked_directions_match_canvas() {
}
}
/// A member's light under two nested groups that each turn 30° turns 60° in all, rounded once:
/// Top Left, not Left (Top Left after the inner group's turn, then Left after the outer one's).
#[test]
fn nested_group_turns_round_the_light_once() {
let relief = || fx("sketch.basRelief", json!({"lightDirection": "top"}));
let thirty = || transform(json!({"rotate": 30}));
let inner = group(41, vec![square(42, vec![relief()]), fill_square(43, vec![relief()])], vec![thirty()]);
let outer = group(40, vec![inner], vec![thirty()]);
let art = crate::evaluate_container(None, &outer).unwrap();
assert_eq!(lights_in(&art), vec![Light::TopLeft; 2]);
let mut d = Document::new(800.0, 800.0);
let layer = d.layers[0].id;
d.insert(Some(layer), 0, outer).unwrap();
let baked = bake_document(&d).unwrap();
for id in [42, 43] {
assert_eq!(lights_in(baked.node(NodeId(id)).unwrap()), vec![Light::TopLeft], "{id}");
}
// Three groups make 90°: Left, not Bottom Left.
let three = group(44, vec![group(45, vec![group(46, vec![square(47, vec![relief()])], vec![thirty()])], vec![thirty()])], vec![thirty()]);
let art = crate::evaluate_container(None, &three).unwrap();
assert_eq!(lights_in(&art), vec![Light::Left]);
}
/// A Transform effect after a 3D effect that makes copies or moves at random holds back the shadow
/// before the 3D effect, as it does without the 3D effect.
#[test]