From dd6ab903e7938b2848e903ae3cdc93186bf48dab Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 20:32:36 -0800 Subject: [PATCH] 3D: Expand as Wireframes measures later effects with the cast shadow A geometry effect after an object's 3D effect, such as a Transform that scales or rotates about the center, measures the drawn art, cast shadow included. Expand as Wireframes measured the faces alone, so with a shadow on, the expanded edges landed away from the surfaces on the canvas. The faces are now reshaped together with the shadow the canvas draws behind them (Extrude & Bevel, Inflate, Rotate and Revolve, on an object, group or layer), and only the faces are kept. --- crates/effects/src/revolve.rs | 24 ++++---- crates/effects/src/solids.rs | 14 +++-- crates/effects/src/solids/wireframe.rs | 61 +++++++++++++++----- crates/effects/src/solids/wireframe/tests.rs | 26 +++++++++ 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index 74b23340cd..e626c757a7 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -634,9 +634,10 @@ 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. 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> { +/// each member, about one shared axis. With the effect on the object, group or layer, also the +/// shadow drawn behind them. 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 { 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()) }; @@ -647,7 +648,7 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result, n: &Node) -> Result, n: &Node) -> Result, n: &Node, fx: &Effect, shapes: &[Profile]) -> let art = surfaces_for(doc, n, fx, shapes)?; // A shadow past the limits draws the source path, as a solid past them does; commands report // the error ([`validate_solid_in`]). - let shadow = match cast_shadow(&fx.id, &crate::merged_params(&fx.id, &fx.params), shapes).ok()? { - Some((caster, settings, cast)) => shadow::scoped_art(n.id, &caster, settings, cast, has_masks(shapes)), - None => vec![], - }; + let shadow = shadow_for(n.id, fx, shapes).ok()?; Some((shadow, art)) } +/// The shadow [`art_for`] draws for effect `fx` on `shapes` behind the surfaces of object `id`: +/// none with the shadow off, and an error when it is past the limits. +pub(crate) fn shadow_for(id: NodeId, fx: &Effect, shapes: &[Profile]) -> Result>, String> { + Ok(match cast_shadow(&fx.id, &crate::merged_params(&fx.id, &fx.params), shapes)? { + Some((caster, settings, cast)) => shadow::scoped_art(id, &caster, settings, cast, has_masks(shapes)), + None => vec![], + }) +} + /// What the shadow of the solids [`surfaces_for`] builds for effect `id` with parameters `p` on /// `shapes` is cast from, on one plane: the faces of every solid, the shadow's settings and the /// faces of each solid of visible art with its member's scopes and ink. `None` with the shadow diff --git a/crates/effects/src/solids/wireframe.rs b/crates/effects/src/solids/wireframe.rs index e6e7d89ab5..9d256ee4fc 100644 --- a/crates/effects/src/solids/wireframe.rs +++ b/crates/effects/src/solids/wireframe.rs @@ -53,8 +53,8 @@ pub fn three_d_wireframe_in(doc: Option<&Document>, n: &Node) -> Result, n: &Node) -> Result, n: &Node, faces: Vec, revolve: bool) -> Vec { +/// ([`reshaped_beside`]): its first visible Revolve when `revolve` is set, else its first visible +/// Extrude & Bevel, Inflate or Rotate. The effects measure the faces together with `shadow`, the +/// shadow the canvas draws behind them, as they measure the drawn art. +fn after_geometry(doc: Option<&Document>, n: &Node, faces: Vec, shadow: Vec>, revolve: bool) -> Vec { let fx = &n.appearance.effects; let drawn = if revolve { fx.iter().position(|e| e.visible && crate::revolve::is_revolve(&e.id)) } else { solid(fx).map(|(i, _)| i) }; let Some(index) = drawn else { return faces }; - reshaped(faces, fx.get(index + 1..).unwrap_or_default(), &GeomContext { doc, ..GeomContext::of(n) }) + reshaped_beside(faces, shadow, fx.get(index + 1..).unwrap_or_default(), &GeomContext { doc, ..GeomContext::of(n) }) } /// `faces` reshaped by the visible geometry effects in `later` (the effects after a 3D effect), as @@ -78,29 +79,60 @@ fn after_geometry(doc: Option<&Document>, n: &Node, faces: Vec, revolv /// to one budget of anchors together. `ctx` gives the object's id, the seed of a random Transform, /// and the document. pub(crate) fn reshaped(faces: Vec, later: &[Effect], ctx: &GeomContext) -> Vec { + reshaped_beside(faces, vec![], later, ctx) +} + +/// The name the faces carry through [`reshaped_beside`], which tells them from the shadow. +const FACE: &str = "face"; + +/// [`reshaped`] with `shadow`, the shadow drawn behind the faces, in the art the effects reshape: +/// each effect measures the faces and the shadow together, and only the faces are returned. +fn reshaped_beside(faces: Vec, shadow: Vec>, later: &[Effect], ctx: &GeomContext) -> Vec { let after: Vec<&Effect> = later.iter().filter(|e| e.visible && is_geometry(&e.id)).collect(); if after.is_empty() { return faces; } let id = NodeId(ctx.seed); let fill = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0); - let mut art = Node::group(id, faces.into_iter().map(|f| Arc::new(Node::path(id, f, fill.clone()))).collect()); + let face = |f: PathData| { + let mut face = Node::path(id, f, fill.clone()); + face.name = Some(FACE.into()); + Arc::new(face) + }; + let mut art = Node::group(id, shadow.into_iter().chain(faces.into_iter().map(face)).collect()); crate::reshape::reshape_leaves(&mut art, &after, later, ctx.doc); let mut out = vec![]; - art.walk(&mut |c| out.extend(c.path_data().cloned())); + face_paths(&art, false, &mut out); out } +/// The paths of the faces in `n` ([`FACE`]), `inside` one or not: a face that a Transform effect +/// copies or a live-effect plug-in cuts into pieces becomes a group with its name. +fn face_paths(n: &Node, inside: bool, out: &mut Vec) { + let inside = inside || n.name.as_deref() == Some(FACE); + if inside { + out.extend(n.path_data().cloned()); + } + for c in n.children().into_iter().flatten() { + face_paths(c, inside, out); + } +} + +/// The faces of a 3D effect on an object, group or layer itself, and the shadow the canvas draws +/// behind them (none with the shadow off, or with the effect on fills and strokes). +pub(crate) type FacesAndShadow = (Vec, Vec>); + /// 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. 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> { +/// the solids of its members, about one shared axis. With the effect on the object, group or layer, +/// also the shadow drawn behind them. 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 { 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))?; reshape_members(n, n.appearance.effects.get(..index).unwrap_or_default(), &mut shapes); - return facet_paths(fx, &shapes); + return Ok((facet_paths(fx, &shapes)?, super::shadow_for(n.id, fx, &shapes)?)); } let label = n.appearance.effects.iter().find(|e| e.visible && is_three_d(&e.id)).map_or("This effect", |e| label_of(&e.id)); let (path, rule) = object_profile(n, label)?; @@ -108,7 +140,8 @@ fn solid_face_paths(doc: Option<&Document>, n: &Node) -> Result, S let (sources, paints) = solid_paint_sources(n); if let Some((index, fx)) = solid(&n.appearance.effects) { let path = geometry(&path, n.appearance.effects.get(..index).unwrap_or_default(), &ctx); - return facet_paths(fx, &[Profile { path, rule, items: paints, opacity: 1.0, scopes: vec![] }]); + let shapes = [Profile { path, rule, items: paints, opacity: 1.0, scopes: vec![] }]; + return Ok((facet_paths(fx, &shapes)?, super::shadow_for(n.id, fx, &shapes)?)); } let path = geometry(&path, &n.appearance.effects, &ctx); let mut out = vec![]; @@ -119,7 +152,7 @@ fn solid_face_paths(doc: Option<&Document>, n: &Node) -> Result, S out.extend(reshaped(faces, item.effects().get(index + 1..).unwrap_or_default(), &GeomContext { doc, ..ctx })); } } - Ok(out) + Ok((out, vec![])) } /// The outlines of the facets effect `fx` builds from `shapes`, as `art_for` builds them: Rotate and diff --git a/crates/effects/src/solids/wireframe/tests.rs b/crates/effects/src/solids/wireframe/tests.rs index 792fbdb28a..5835503451 100644 --- a/crates/effects/src/solids/wireframe/tests.rs +++ b/crates/effects/src/solids/wireframe/tests.rs @@ -192,6 +192,32 @@ fn a_geometry_effect_after_a_fill_s_3d_effect_reshapes_its_edges() { } } +/// A Transform effect after the 3D effect scales the faces about the center of the drawn art with +/// its shadow, as it scales the surfaces on the canvas. +#[test] +fn a_later_transform_measures_the_faces_with_their_shadow() { + for id in [super::super::EXTRUDE, crate::REVOLVE] { + let scale = crate::new_effect("distort.transform", &json!({"scaleH": 50.0, "scaleV": 50.0})).unwrap(); + let mut n = solid_square(id, json!({"shadow": true, "shadowDistance": 100})); + n.appearance.effects.push(scale.clone()); + let wire = bounds(&three_d_wireframe_in(None, &n).unwrap_or_else(|e| panic!("{id}: {e}"))); + let art = if id == crate::REVOLVE { crate::revolve_art(&n) } else { super::super::solid_art(&n) }.unwrap(); + let mut drawn = art.clone(); + let fx: Vec<&Effect> = art.appearance.effects.iter().collect(); + crate::reshape::reshape_leaves(&mut drawn, &fx, &art.appearance.effects, None); + // The shadow is the art's first member; the surfaces follow it. + let surfaces: Vec = drawn.children().unwrap().iter().skip(1).flat_map(|c| face_paths(c)).collect(); + let a = bounds(&surfaces); + for (x, y) in [(wire.x0, a.x0), (wire.y0, a.y0), (wire.x1, a.x1), (wire.y1, a.y1)] { + assert!((x - y).abs() < 1e-6, "{id}: {wire:?} {a:?}"); + } + // Without the shadow, the faces are scaled about their own center. + let mut alone = solid_square(id, json!({})); + alone.appearance.effects.push(scale); + assert!((bounds(&three_d_wireframe_in(None, &alone).unwrap()).x0 - wire.x0).abs() > 1.0, "{id}"); + } +} + /// With two 3D effects, a geometry effect between them reshapes the faces of the one drawn only /// once: Revolve is drawn, so a Transform before it moves its profile and not its faces again. #[test]