Merge pull request #7432 from dotCooCoo/fix/wireframe-transform-measures-shadow

3D: Expand as Wireframes measures later effects with the cast shadow
This commit is contained in:
BFlat
2026-11-15 03:21:13 +07:00
committed by GitHub
4 changed files with 95 additions and 35 deletions
+14 -12
View File
@@ -663,10 +663,11 @@ 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`. Each face carries the width of the
/// stroke the canvas draws around it ([`surface`]).
pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<Vec<WireFace>, 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`. Each face carries the width of the stroke the canvas
/// draws around it ([`surface`]).
pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<crate::solids::wireframe::FacesAndShadow, String> {
let faces = |path: &PathData, b: Rect, fx: &Effect, options: Revolve| -> Result<Vec<WireFace>, String> {
if fx.id == CLASSIC_REVOLVE {
let p = merged_params(&fx.id, &fx.params);
@@ -682,7 +683,7 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<Vec
.iter()
.position(|e| e.visible && is_revolve(&e.id))
.ok_or("a 3D effect on a group's fill or stroke draws nothing")?;
let Some(fx) = n.appearance.effects.get(index) else { return Ok(vec![]) };
let Some(fx) = n.appearance.effects.get(index) else { return Ok((vec![], vec![])) };
let mut shapes = members(n, "Revolve")?;
reshape_members(n, n.appearance.effects.get(..index).unwrap_or_default(), &mut shapes);
let b = union_bounds(&shapes).ok_or("Revolve requires a group holding a shape with at least two anchors")?;
@@ -691,18 +692,19 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<Vec
for s in shapes.iter().filter(|s| surface_item(&s.items).is_some()) {
out.extend(faces(&s.path, b, fx, options)?);
}
return Ok(out);
return Ok((out, group_shadow(n.id, fx, &shapes, b, options)?));
}
let (path, _) = object_profile(n, "Revolve")?;
let ctx = GeomContext::of(n);
if let Some(index) = n.appearance.effects.iter().position(|e| e.visible && is_revolve(&e.id)) {
let Some(fx) = n.appearance.effects.get(index) else { return Ok(vec![]) };
let Some(fx) = n.appearance.effects.get(index) else { return Ok((vec![], vec![])) };
let path = geometry(&path, n.appearance.effects.get(..index).unwrap_or_default(), &ctx);
if surface_item(&solid_paints(n)).is_none() {
return Err("the revolved object has no fill or stroke".into());
}
let paints = solid_paints(n);
let item = surface_item(&paints).ok_or("the revolved object has no fill or stroke")?;
let b = path.bounds().ok_or("Revolve requires a shape with at least two anchors")?;
return faces(&path, b, fx, fx_options(fx));
let surface = faces(&path, b, fx, fx_options(fx))?;
let shadow = shadow_of(n.id, fx, &path, item, b, fx_options(fx))?;
return Ok((surface, shadow.into_iter().map(Arc::new).collect()));
}
let path = geometry(&path, &n.appearance.effects, &ctx);
let mut out = vec![];
@@ -716,7 +718,7 @@ pub(crate) fn revolve_face_paths(doc: Option<&Document>, n: &Node) -> Result<Vec
out.extend(crate::solids::wireframe::reshaped(surface, item.effects().get(index + 1..).unwrap_or_default(), &GeomContext { doc, ..ctx }));
}
}
Ok(out)
Ok((out, vec![]))
}
/// [`revolve_meshes`] of group or layer `n`: one mesh for each member, about one shared axis.
+10 -4
View File
@@ -1369,13 +1369,19 @@ fn art_for(doc: Option<&Document>, 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<Vec<Arc<Node>>, 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
+45 -19
View File
@@ -53,8 +53,8 @@ pub fn three_d_wireframe_in(doc: Option<&Document>, n: &Node) -> Result<Vec<Path
// Revolve's art is cut for Revolve whatever other 3D effect the object carries, as the canvas
// cuts it ([`crate::revolve_art_in`]).
let n = &*if revolve { resolve_for(doc, n, label, false)? } else { resolve(doc, n, label)? };
let faces = if revolve { crate::revolve::revolve_face_paths(doc, n)? } else { solid_face_paths(doc, n)? };
let faces: Vec<PathData> = after_geometry(doc, n, faces, revolve).into_iter().map(|f| f.path).collect();
let (faces, shadow) = if revolve { crate::revolve::revolve_face_paths(doc, n)? } else { solid_face_paths(doc, n)? };
let faces: Vec<PathData> = after_geometry(doc, n, faces, shadow, revolve).into_iter().map(|f| f.path).collect();
let wire = edges(&faces)?;
if wire.is_empty() {
return Err(format!("{label} draws no faces for this object"));
@@ -63,13 +63,14 @@ pub fn three_d_wireframe_in(doc: Option<&Document>, n: &Node) -> Result<Vec<Path
}
/// `faces` reshaped by the visible geometry effects after the 3D effect of `n`'s own that drew them
/// ([`reshaped`]): its first visible Revolve when `revolve` is set, else its first visible Extrude &
/// Bevel, Inflate or Rotate.
fn after_geometry(doc: Option<&Document>, n: &Node, faces: Vec<WireFace>, revolve: bool) -> Vec<WireFace> {
/// ([`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<WireFace>, shadow: Vec<Arc<Node>>, revolve: bool) -> Vec<WireFace> {
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) })
}
/// One face of a drawn 3D surface: its outline and the width of the stroke the canvas draws around
@@ -86,6 +87,15 @@ pub(crate) struct WireFace {
/// the canvas, so an Outline Stroke outlines that stroke. `ctx` gives the object's id, the seed of a
/// random Transform, and the document.
pub(crate) fn reshaped(faces: Vec<WireFace>, later: &[Effect], ctx: &GeomContext) -> Vec<WireFace> {
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<WireFace>, shadow: Vec<Arc<Node>>, later: &[Effect], ctx: &GeomContext) -> Vec<WireFace> {
let after: Vec<&Effect> = later.iter().filter(|e| e.visible && is_geometry(&e.id)).collect();
if after.is_empty() {
return faces;
@@ -95,29 +105,44 @@ pub(crate) fn reshaped(faces: Vec<WireFace>, later: &[Effect], ctx: &GeomContext
let face = |f: WireFace| {
let items =
vec![AppearanceItem::Fill(FillLayer::new(paint.clone())), AppearanceItem::Stroke(crate::lighting::seam_stroke(paint.clone(), f.stroke))];
Arc::new(Node::path(id, f.path, Appearance { items, ..Default::default() }))
let mut face = Node::path(id, f.path, Appearance { items, ..Default::default() });
face.name = Some(FACE.into());
Arc::new(face)
};
let mut art = Node::group(id, faces.into_iter().map(face).collect());
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| {
if let Some(path) = c.path_data() {
out.push(WireFace { path: path.clone(), stroke: c.appearance.stroke().map_or(0.0, |s| s.width) });
}
});
face_paths(&art, false, &mut out);
out
}
/// The faces in `n` ([`FACE`]), `inside` one or not, each with the width of its stroke: 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<WireFace>) {
let inside = inside || n.name.as_deref() == Some(FACE);
if inside && let Some(path) = n.path_data() {
out.push(WireFace { path: path.clone(), stroke: n.appearance.stroke().map_or(0.0, |s| s.width) });
}
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<WireFace>, Vec<Arc<Node>>);
/// 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<Vec<WireFace>, 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<FacesAndShadow, 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))?;
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)?;
@@ -125,7 +150,8 @@ fn solid_face_paths(doc: Option<&Document>, n: &Node) -> Result<Vec<WireFace>, 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![];
@@ -136,7 +162,7 @@ fn solid_face_paths(doc: Option<&Document>, n: &Node) -> Result<Vec<WireFace>, 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
@@ -222,6 +222,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<PathData> = 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]