From 55a80ca480692aa397e087bc7cb9c9d6b38ef1a4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 07:31:39 -0800 Subject: [PATCH] 3D and Materials: Expand as Wireframes looks up swatches for plug-ins after a fill's 3D effect A live-effect plug-in after a fill's or stroke's 3D effect (Extrude & Bevel, Inflate, Rotate or Revolve) that links its pieces to a swatch or pattern was run without the document, so its result was refused and the edges kept the shape before it. The document now goes to those effects, as it does for the effects after an object's 3D effect. --- crates/effects/src/revolve.rs | 7 +++--- crates/effects/src/solids/wireframe.rs | 13 ++++++----- crates/engine/src/cmd/wireframe/tests.rs | 28 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index 8d05ab38ed..aae131dfdc 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -511,8 +511,9 @@ pub(crate) fn revolve_meshes(n: &Node, total: &mut usize, limit: usize) -> Resul /// instances, blends and clipping groups already resolved), in document coordinates: with the /// effect on the object, the surface in its top visible fill (else its stroke); with it on fills and strokes, one surface for each of /// them, reshaped by the geometry effects after each one's Revolve; on a group or layer, one for -/// each member, about one shared axis. -pub(crate) fn revolve_face_paths(n: &Node) -> Result, String> { +/// each member, about one shared axis. The swatches and patterns named by a live-effect plug-in +/// after a fill's or stroke's Revolve are looked up in `doc`. +pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result, String> { let faces = |path: &PathData, b: Rect, options: Revolve| -> Result, String> { Ok(vectorcraft_three_d::revolve(path, b, options)?.iter().map(vectorcraft_three_d::Face::path).collect()) }; @@ -554,7 +555,7 @@ pub(crate) fn revolve_face_paths(n: &Node) -> Result, String> { let p = geometry(&path, item.effects().get(..index).unwrap_or_default(), &ctx.item(item)); let b = p.bounds().ok_or("Revolve requires a shape with at least two anchors")?; let surface = faces(&p, b, fx_options(fx))?; - out.extend(crate::solids::wireframe::reshaped(surface, item.effects().get(index + 1..).unwrap_or_default(), &ctx)); + out.extend(crate::solids::wireframe::reshaped(surface, item.effects().get(index + 1..).unwrap_or_default(), &GeomContext { doc, ..ctx })); } } Ok(out) diff --git a/crates/effects/src/solids/wireframe.rs b/crates/effects/src/solids/wireframe.rs index 5f2275d8eb..33f7f7f018 100644 --- a/crates/effects/src/solids/wireframe.rs +++ b/crates/effects/src/solids/wireframe.rs @@ -44,14 +44,14 @@ pub fn three_d_wireframe_in(doc: Option<&Document>, n: &Node) -> Result, later: &[Effect], ctx: &GeomContext /// The outline of every face [`super::solid_art`] draws for `n` (already resolved): with the effect /// on the object, its solids; with it on fills and strokes, the solid of each; on a group or layer, -/// the solids of its members, about one shared axis. -fn solid_face_paths(n: &Node) -> Result, String> { +/// the solids of its members, about one shared axis. The swatches and patterns named by a live-effect +/// plug-in after a fill's or stroke's 3D effect are looked up in `doc`. +fn solid_face_paths(doc: Option<&Document>, n: &Node) -> Result, String> { if is_container(n) { let (index, fx) = solid(&n.appearance.effects).ok_or("a 3D effect on a group's fill or stroke draws nothing")?; let mut shapes = members(n, label_of(&fx.id))?; @@ -110,7 +111,7 @@ fn solid_face_paths(n: &Node) -> Result, String> { if let Some((index, fx)) = solid(item.effects()) { let p = geometry(&path, item.effects().get(..index).unwrap_or_default(), &ctx.item(item)); let faces = facet_paths(fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0, scopes: vec![] }])?; - out.extend(reshaped(faces, item.effects().get(index + 1..).unwrap_or_default(), &ctx)); + out.extend(reshaped(faces, item.effects().get(index + 1..).unwrap_or_default(), &GeomContext { doc, ..ctx })); } } Ok(out) diff --git a/crates/engine/src/cmd/wireframe/tests.rs b/crates/engine/src/cmd/wireframe/tests.rs index ab51c81282..268fbcd6db 100644 --- a/crates/engine/src/cmd/wireframe/tests.rs +++ b/crates/engine/src/cmd/wireframe/tests.rs @@ -161,6 +161,34 @@ fn objects_without_a_3d_effect_are_left_and_none_at_all_is_an_error() { assert_eq!(s.doc().unwrap().doc.node(plain).unwrap(), &kept); } +/// A live-effect plug-in after a fill's 3D effect that links its pieces to one of the document's +/// swatches reshapes the edges, as it reshapes the drawn surface. +#[test] +fn a_painted_effect_after_a_fill_s_3d_effect_finds_the_document_s_swatches() { + use vectorcraft_geom::{PathData, Point, SubPath}; + let mut leaf = serde_json::to_value(vectorcraft_color::Paint::solid(vectorcraft_color::Color::from_hex("#00ff00").unwrap())).unwrap(); + leaf["swatch"] = json!("Leaf"); + let corners = [Point::new(300.0, 300.0), Point::new(320.0, 300.0), Point::new(320.0, 320.0), Point::new(300.0, 320.0)]; + let output = json!({"objects": [{"path": PathData::single(SubPath::polyline(&corners, true)), "fills": [leaf]}]}).to_string(); + let manifest = r#"{"id":"test.engine.wireleaf","name":"Test wireleaf","version":"1","kind":"effect","api":2,"paintOutput":true,"params":{}}"#; + let wasm = wat::parse_str(vectorcraft_plugins::wat::returning(manifest, &output)).unwrap(); + for effect in ["threeD.extrude", "threeD.revolve"] { + let (mut s, id) = setup(None); + s.execute("plugin.install", &json!({"dataBase64": vectorcraft_format::base64_encode(&wasm), "replace": true})).unwrap(); + s.execute("paint.setStroke", &json!({"none": true})).unwrap(); + s.execute("swatch.new", &json!({"name": "Leaf", "color": "#00ff00", "global": true})).unwrap(); + let items = &s.doc().unwrap().doc.node(id).unwrap().appearance.items; + let fill = items.iter().position(|i| matches!(i, AppearanceItem::Fill(_))).unwrap(); + for e in [effect, "plugin.test.engine.wireleaf"] { + s.execute("effect.apply", &json!({"ids": [id.0], "effect": e, "item": fill, "params": {}})).unwrap(); + } + run(&mut s, json!({"ids": [id.0]})).unwrap_or_else(|e| panic!("{effect}: {e}")); + let b = s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap(); + assert_eq!((b.x0, b.y0, b.x1, b.y1), (300.0, 300.0, 320.0, 320.0), "{effect}: the plug-in's square"); + assert_eq!(segments(&s, id), 4, "{effect}"); + } +} + #[test] fn bad_ids_are_errors() { let (mut s, _) = setup(Some("threeD.extrude"));