diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index eb227300e0..e9d64864d0 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -1351,7 +1351,7 @@ mod tests { 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(); + let refused = crate::validate_three_d_at(&group, id, &json!({}), (Some(0), 0)).unwrap_err(); assert!(refused.contains("on the group itself"), "{id}: {refused}"); } } diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index 3031228e32..e20dbdba17 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -250,33 +250,50 @@ pub fn validate_solid(n: &Node, id: &str, p: &Value) -> Result<(), String> { /// [`validate_solid`] with the symbols of `doc`: a symbol instance, a blend and a clipping group /// build from the art they show ([`resolve`]). pub fn validate_solid_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value) -> Result<(), String> { - validate_solid_for(doc, n, id, p, None) + validate_solid_for(doc, n, id, p, (None, None)) } -/// [`validate_solid_in`] for the effect on the object (`item` `None`) or on its appearance item -/// `item`, whose shadow is cast by that item's solid alone. -fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, item: Option) -> Result<(), String> { +/// [`validate_solid_in`] for the effect at `(item, index)`: index `index` of the effects of the +/// object (`item` `None`) or of its appearance item `item`, whose shadow is cast by that item's +/// solid alone. The index is the effects' length for a new effect. The solids are checked as +/// [`solid_art_in`] draws them: from the shapes after the geometry effects before the 3D effect. +/// On the object (or group) with `index` `None`, that effect is its first visible Extrude & Bevel, +/// Inflate or Rotate, as drawn, or a new one after its other effects when it has none. +fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at: (Option, Option)) -> Result<(), String> { validate_solid_params(id, p)?; if let Some(doc) = doc { graphics::validate_symbols(doc, p)?; } let label = label_of(id); let n = &*source::resolve_for(doc, n, label, is_plane(id))?; + let effects = &n.appearance.effects; + let index = match at { + (None, Some(index)) => index, + _ => solid(effects).map_or(effects.len(), |(index, _)| index), + }; + let before = effects.get(..index).unwrap_or(effects); if is_container(n) { - let shapes = members(n, label)?; + let mut shapes = members(n, label)?; + reshape_members(n, before, &mut shapes); let b = union_bounds(&shapes).ok_or_else(|| format!("{label} requires a group holding a shape with at least two anchors"))?; shapes.iter().try_for_each(|s| facets(id, &s.path, s.rule, b, p).map(|_| ()))?; check_solids(id, p, &shapes)?; return cast_shadow(id, p, &shapes).map(|_| ()); } let (path, rule) = object_profile(n, label)?; - let b = path.bounds().ok_or_else(|| format!("{label} requires a shape with at least two anchors"))?; - facets(id, &path, rule, b, p)?; + let anchors = || format!("{label} requires a shape with at least two anchors"); + path.bounds().ok_or_else(anchors)?; + let ctx = GeomContext::of(n); // With its shadow on, the shadow of the object's solids (or of the item's) must fit too. - let items = match item.and_then(|i| n.appearance.items.get(i)) { - Some(item) => vec![item.clone()], - None => solid_paints(n), + let (path, items) = match at.0.and_then(|i| Some((n.appearance.items.get(i)?, at.1.unwrap_or(usize::MAX)))) { + Some((item, index)) => { + let path = geometry(&path, effects, &ctx); + (geometry(&path, item.effects().get(..index).unwrap_or(item.effects()), &ctx.item(item)), vec![item.clone()]) + } + None => (geometry(&path, before, &ctx), solid_paints(n)), }; + let b = path.bounds().ok_or_else(anchors)?; + facets(id, &path, rule, b, p)?; let shapes = [Profile { path, rule, items, opacity: 1.0, scopes: vec![] }]; check_solids(id, p, &shapes)?; cast_shadow(id, p, &shapes).map(|_| ()) @@ -468,26 +485,28 @@ pub fn validate_three_d_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value if crate::revolve::is_revolve(id) { crate::revolve::validate_in(doc, n, id, p) } else { validate_solid_in(doc, n, id, p) } } -/// [`validate_three_d`] for an effect at `(item, index)`, index `index` of the effects of -/// 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> { +/// [`validate_three_d`] for an effect at `(item, index)`: index `index` of the effects of +/// appearance item `item` of `n`, or of the object itself when `item` is `None` (the effects' +/// length for a new effect). An Extrude & Bevel, Inflate or Rotate is checked on the shapes after +/// the geometry effects before that index. 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)) -> Result<(), String> { validate_three_d_at_in(None, n, id, p, at) } /// [`validate_three_d_at`] with the symbols of `doc` ([`validate_solid_in`]). A symbol instance, a /// blend and a clipping group take a 3D effect as groups do: on the object itself. -pub fn validate_three_d_at_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at: Option<(usize, usize)>) -> Result<(), String> { +pub fn validate_three_d_at_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at: (Option, usize)) -> Result<(), String> { graphics::validate(id, p)?; match at { - Some(_) if is_container(n) || source::resolves(n) => { + (Some(_), _) if is_container(n) || source::resolves(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 crate::revolve::is_revolve(id) => crate::revolve::validate_item_of(n, id, p, item, index), - Some((item, _)) => validate_solid_for(doc, n, id, p, Some(item)), - None => validate_three_d_in(doc, n, id, p), + (Some(item), index) if crate::revolve::is_revolve(id) => crate::revolve::validate_item_of(n, id, p, item, index), + (None, _) if crate::revolve::is_revolve(id) => crate::revolve::validate_in(doc, n, id, p), + (item, index) => validate_solid_for(doc, n, id, p, (item, Some(index))), } } diff --git a/crates/effects/src/solids/tests.rs b/crates/effects/src/solids/tests.rs index 935cfd26e4..b4e012f977 100644 --- a/crates/effects/src/solids/tests.rs +++ b/crates/effects/src/solids/tests.rs @@ -450,6 +450,45 @@ fn a_fill_and_strokes_together_past_the_limit_draw_the_source() { assert!(solid_art(&n).is_none(), "the source art draws"); } +/// The check builds the solids from the shapes the art draws them from: after the geometry effects +/// before the 3D effect, on an object, on its fill and on a group. Copies a Transform effect before +/// it makes that take the solids past the limit fail the check, as the art then draws the source. +#[test] +fn the_check_counts_the_faces_after_the_geometry_effects_before() { + let mut alone = polygon_node(1, 16_384); + alone.appearance.effects.push(fx(EXTRUDE, json!({}))); + let [Ok(Some(each))] = solid_faces(EXTRUDE, &alone, MAX_OBJECT_FACES)[..] else { panic!("one solid") }; + // Side by side, each copy builds the same solid. + let copies = |k: usize| fx("distort.transform", json!({"moveH": 30_000.0, "copies": k})); + for (k, fits) in [(1, true), (MAX_OBJECT_FACES / each, false)] { + let mut n = polygon_node(1, 16_384); + n.appearance.effects = vec![copies(k), fx(EXTRUDE, json!({}))]; + assert_eq!(validate_solid(&n, EXTRUDE, &json!({})).is_ok(), fits, "on the object, {k} copies"); + assert_eq!(solid_art(&n).is_some(), fits, "on the object, {k} copies"); + let mut f = polygon_node(1, 16_384); + f.appearance.items[0].effects_mut().extend([copies(k), fx(EXTRUDE, json!({}))]); + assert_eq!(validate_three_d_at(&f, EXTRUDE, &json!({}), (Some(0), 1)).is_ok(), fits, "on the fill, {k} copies"); + assert_eq!(solid_art(&f).is_some(), fits, "on the fill, {k} copies"); + let mut g = Node::group(NodeId(9999), vec![Arc::new(polygon_node(1, 16_384))]); + g.appearance.effects = vec![copies(k), fx(EXTRUDE, json!({}))]; + assert_eq!(validate_solid(&g, EXTRUDE, &json!({})).is_ok(), fits, "on a group, {k} copies"); + assert_eq!(solid_art(&g).is_some(), fits, "on a group, {k} copies"); + // A new effect goes after the copies. + let mut new = polygon_node(1, 16_384); + new.appearance.effects = vec![copies(k)]; + assert_eq!(validate_three_d_at(&new, EXTRUDE, &json!({}), (None, 1)).is_ok(), fits, "a new effect, {k} copies"); + } + // A hidden effect checked at its own index counts only the effects before it: copies after it + // copy the drawn art once it is shown again. + let mut hidden = polygon_node(1, 16_384); + let mut extrude = fx(EXTRUDE, json!({})); + extrude.visible = false; + hidden.appearance.effects = vec![extrude, copies(MAX_OBJECT_FACES / each)]; + assert!(validate_three_d_at(&hidden, EXTRUDE, &json!({}), (None, 0)).is_ok()); + hidden.appearance.effects[0].visible = true; + assert!(solid_art(&hidden).is_some()); +} + /// At no volume Inflate's front is flat: turned, the fill's cells are all drawn before the stroke's, /// whatever their depths, as the extrusion's caps are. When the fill puffs, a stroke too narrow to /// puff on the grid stays flat and keeps its depths: the fill's puff is drawn over its far side. diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index 72a72e6411..02cbecae1d 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -457,9 +457,10 @@ pub(crate) fn apply(s: &mut Session, p: &Value) -> Result { for (target, at) in targets.iter().copied().zip(item.per_target(s, &targets)) { let d = &s.doc()?.doc; let n = d.node(target).ok_or(EngineError::NoNode(target))?; - // A new effect goes after the item's others. + // A new effect goes after the item's (or the object's) others. let on = at.and_then(|at| at.effects_item(&n.appearance, C).ok()).flatten(); - let on = on.and_then(|i| Some((i, n.appearance.items.get(i)?.effects().len()))); + let on = on.and_then(|i| Some((Some(i), n.appearance.items.get(i)?.effects().len()))); + let on = on.unwrap_or((None, n.appearance.effects.len())); effects::validate_three_d_at_in(Some(d), n, id, &effect.params, on).map_err(|e| bad(C, e))?; if let Some(name) = effects::missing_symbol(d, &effect.params, None) { return Err(bad(C, format!("no symbol named `{name}`"))); @@ -772,7 +773,7 @@ fn set_params(s: &mut Session, p: &Value) -> Result { return Err(bad(C, format!("no symbol named `{name}`"))); } if visible != Some(false) { - effects::validate_three_d_at_in(Some(d), n, to, &prospective, target.map(|item| (item, index))).map_err(|e| bad(C, e))?; + effects::validate_three_d_at_in(Some(d), n, to, &prospective, (target, index)).map_err(|e| bad(C, e))?; } else { // Hidden, the effect draws nothing, so its object need not fit. The values given are // checked, and after a change of 3D type so is every value it keeps. diff --git a/crates/engine/src/cmd/effectcmd/graphics.rs b/crates/engine/src/cmd/effectcmd/graphics.rs index 1801c83492..3365a1b764 100644 --- a/crates/engine/src/cmd/effectcmd/graphics.rs +++ b/crates/engine/src/cmd/effectcmd/graphics.rs @@ -131,7 +131,7 @@ fn map_graphic(s: &mut Session, p: &Value) -> Result { graphic.scale = frame.fitting_scale(art); } let params = effects::with_graphic(&effects::merged_params(&effect.id, &effect.params), &graphic); - effects::validate_three_d_at_in(Some(d), n, &effect.id, ¶ms, item.map(|i| (i, index))).map_err(|e| bad(C, e))?; + effects::validate_three_d_at_in(Some(d), n, &effect.id, ¶ms, (item, index)).map_err(|e| bad(C, e))?; edits.push((id, item, index, graphic)); } let first = edits.first().map(|(.., g)| g.to_value());