Canvas: a 3D effect on a fill that builds nothing draws without it

An object with an effect of its own (Drop Shadow, Roughen) and a 3D
effect on its fill or stroke that builds nothing (Revolve after
Roughen, a solid too complex) overflowed the stack when drawn, on the
canvas and in Object › Rasterize: the art made for the object kept the
item's 3D effect, so drawing it drew it as an object with effects again,
and that art again, without end.

That art now leaves out the 3D effects on its fills and strokes when
none built anything, as a group's or layer's own are left to the art of
each fill or stroke. The fill draws as without the effect; with a raster
effect the object draws from that art, so its edges differ a little
from the same object without the 3D effect, drawn as a path.
This commit is contained in:
Drew Dibble
2026-11-09 00:44:21 -07:00
parent 3617c678b7
commit d5d04f8684
2 changed files with 49 additions and 1 deletions
+10 -1
View File
@@ -600,7 +600,16 @@ impl Renderer {
None if matches!(a.kind, NodeKind::Text(_) | NodeKind::Image(_) | NodeKind::Path { .. } | NodeKind::Compound { .. }) => (**a).clone(), None if matches!(a.kind, NodeKind::Text(_) | NodeKind::Image(_) | NodeKind::Path { .. } | NodeKind::Compound { .. }) => (**a).clone(),
None => effects::outline_art(Some(doc), a, symbol.as_ref()).unwrap_or_else(|| Node::group(a.id, vec![])), None => effects::outline_art(Some(doc), a, symbol.as_ref()).unwrap_or_else(|| Node::group(a.id, vec![])),
}; };
let art = if effects::has_revolve(a) || effects::has_solid(a) { effects::evaluate_container(Some(doc), &art).unwrap_or(art) } else { art }; let has_three_d = effects::has_revolve(a) || effects::has_solid(a);
let mut art = if has_three_d { effects::evaluate_container(Some(doc), &art).unwrap_or(art) } else { art };
// A 3D effect on a fill or stroke that built nothing (Revolve after Roughen, a solid too
// complex) draws the item as without it: left in the art, it would draw the art as an
// object with effects again, and that art again, without end.
if has_three_d && !three_d && !container {
for item in &mut art.appearance.items {
item.effects_mut().retain(|e| !effects::is_three_d(&e.id));
}
}
// Vector Halftone samples the object with its colour adjustments: its dots keep their inks. // Vector Halftone samples the object with its colour adjustments: its dots keep their inks.
let outer = if effects::addons::halftone::has_halftone(a) { None } else { effects::color_map(&a.appearance.effects) }; let outer = if effects::addons::halftone::has_halftone(a) { None } else { effects::color_map(&a.appearance.effects) };
let mut art = self.adjusted_art(doc, art, outer.as_ref()); let mut art = self.adjusted_art(doc, art, outer.as_ref());
+39
View File
@@ -1700,3 +1700,42 @@ fn an_image_inner_glow_follows_its_visible_pixels() {
assert!(lum(img.pixel(60, 50)) < lum(img.pixel(51, 50)), "threads {threads}: fading inward"); assert!(lum(img.pixel(60, 50)) < lum(img.pixel(51, 50)), "threads {threads}: fading inward");
} }
} }
/// A 3D effect on a path's fill that builds nothing (Revolve after Roughen) draws the fill as
/// without it, under the object's own effects too: the art made for the object is drawn without
/// it, not as an object with effects again, and that art again, without end. With a raster effect
/// the object draws from that art, where without the 3D effect it draws as a path: their edges
/// differ a little. An Extrude may build.
#[test]
fn a_fills_3d_effect_that_builds_nothing_draws_the_fill_as_without_it() {
let rough = |detail: i64| Effect {
id: "distort.roughen".into(),
params: json!({"size": 10, "relative": false, "detail": detail, "points": "smooth"}),
visible: true,
};
let shadow = Effect { id: "stylize.dropShadow".into(), params: json!({}), visible: true };
let revolve = Effect { id: "threeD.revolve".into(), params: json!({}), visible: true };
let extrude = Effect { id: "threeD.extrude".into(), params: json!({"depth": 20.0}), visible: true };
for (object, fill, three) in [
(vec![shadow.clone()], rough(10), revolve.clone()),
(vec![], rough(10), revolve),
(vec![shadow], rough(60), extrude.clone()),
(vec![rough(60)], rough(60), extrude),
] {
let draw = |three: Option<&Effect>| {
let mut d = doc_with(Rect::new(30.0, 30.0, 60.0, 60.0), Color::BLACK, vec![]);
let id = d.layers[0].children().unwrap()[0].id;
let n = d.node_mut(id).unwrap();
n.appearance.effects = object.clone();
n.appearance.fill_mut().unwrap().effects = [Some(&fill), three].into_iter().flatten().cloned().collect();
Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions::default()).pixels
};
let with = draw(Some(&three));
if three.id == "threeD.revolve" {
let without = draw(None);
let differ = with.iter().zip(&without).filter(|(a, b)| a != b).count();
let most = with.iter().zip(&without).map(|(a, b)| a.abs_diff(*b)).max().unwrap_or(0);
assert!(differ * 100 < with.len() && most <= 32, "{object:?}: {differ} channels differ, by up to {most}");
}
}
}