From cddebdf7f67ae206ccc4c78020747ec8d886539f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 9 Nov 2026 17:51:22 -0800 Subject: [PATCH] 3D: a group's fill or stroke takes no 3D effect Revolve, Extrude & Bevel, Inflate and Rotate on a group or layer draw only as the group's own effect. On the group's fill or stroke they drew nothing, yet effect.apply and effect.setParams accepted them. Both now refuse that placement. --- crates/effects/src/revolve.rs | 11 +++++++++++ crates/effects/src/solids.rs | 2 ++ docs/mcp.md | 3 ++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index cf275fc2d..58125c661 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -717,6 +717,17 @@ mod tests { assert_eq!(revolve_art(&both), None, "past the limits the source draws"); } + #[test] + fn a_3d_effect_on_a_groups_fill_is_refused() { + let mut group = Node::group(NodeId(9), vec![member(1, 0.0)]); + group.appearance = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0).into(); + for id in [REVOLVE, crate::EXTRUDE] { + assert!(crate::validate_three_d(&group, id, &json!({})).is_ok(), "{id} on the group itself"); + let refused = crate::validate_three_d_at(&group, id, &json!({}), Some((0, 0))).unwrap_err(); + assert!(refused.contains("on the group itself"), "{id}: {refused}"); + } + } + #[test] fn validation_measures_the_profile_after_the_geometry_effects_before_revolve() { let roughen = crate::new_effect("distort.roughen", &json!({"size": 1, "detail": 100})).unwrap(); diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index 2340446f2..568bf2be9 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -302,8 +302,10 @@ pub fn validate_three_d(n: &Node, id: &str, p: &Value) -> Result<(), String> { /// appearance item `item` of `n` (the effects' length for a new effect), or on the object itself /// when `at` is `None`. A Revolve on a fill or stroke is checked in that item's paint, after the /// item's geometry effects before it, together with the object's other items that carry a Revolve. +/// A group's or layer's fills and strokes take no 3D effect: only the group's own effect draws. pub fn validate_three_d_at(n: &Node, id: &str, p: &Value, at: Option<(usize, usize)>) -> Result<(), String> { match at { + Some(_) if is_container(n) => Err("A 3D effect on a group or layer goes on the group itself, not on its fill or stroke".into()), Some((item, index)) if id == crate::REVOLVE => crate::revolve::validate_item_revolve(n, p, item, index), _ => validate_three_d(n, id, p), } diff --git a/docs/mcp.md b/docs/mcp.md index b76f2da5a..499d02bd4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2461,7 +2461,8 @@ checked as that effect's. It is one undo step. Any other `threeDType` value, or `effect.apply {effect: "threeD.extrude" | "threeD.inflate" | "threeD.rotate", params: {...}, ids?, item?}` applies the other 3D types to a path or compound path, live like Revolve (the source stays editable; SVG/PDF exports and -`effect.expandAppearance` use the same shaded faces). An object takes one 3D effect, Revolve included. +`effect.expandAppearance` use the same shaded faces). An object takes one 3D effect, Revolve included. On a group or +layer the 3D effect goes on the group itself: `effect.apply` and `effect.setParams` refuse one on its fill or stroke. `threeD.extrude`: `depth` (points, 0..2000, default 50), `twist` (degrees, 0..360, 0), `taper` (1..100%, 100), `cap` (true solid, false hollow), `bevel` (`none`, `classic`, `rounded`, `cove`, `step`, `ogee`), `bevelWidth`