From d5d04f86843fe450643a491494a1c5f77d028773 Mon Sep 17 00:00:00 2001 From: Drew Dibble Date: Mon, 9 Nov 2026 00:44:21 -0700 Subject: [PATCH] Canvas: a 3D effect on a fill that builds nothing draws without it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/render/src/fx/renderer.rs | 11 ++++++++- crates/render/src/fx/tests.rs | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/render/src/fx/renderer.rs b/crates/render/src/fx/renderer.rs index 99ea4e97f..75143f9a0 100644 --- a/crates/render/src/fx/renderer.rs +++ b/crates/render/src/fx/renderer.rs @@ -600,7 +600,16 @@ impl Renderer { 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![])), }; - 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. 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()); diff --git a/crates/render/src/fx/tests.rs b/crates/render/src/fx/tests.rs index b18462820..71c0ad412 100644 --- a/crates/render/src/fx/tests.rs +++ b/crates/render/src/fx/tests.rs @@ -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"); } } + +/// 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}"); + } + } +}