mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 07:10:13 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user