mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 13:36:53 +00:00
3D: check an object's solids after the geometry effects before the effect
Validation built the Extrude & Bevel, Inflate and Rotate solids from the stored shape, while drawing, Export 3D Object and Expand as Wireframes build them after the geometry effects before the 3D effect. A Transform effect with copies before Extrude & Bevel could pass the check and then draw the source art. Validation now reshapes the object, the fill or stroke, or a group's members with those effects first. validate_three_d_at and validate_three_d_at_in take the effect's index for an effect on the object too, so an edit checks the effects before that effect, and effect.apply checks a new effect after the others.
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<usize>) -> 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<usize>, Option<usize>)) -> 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>, 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>, 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))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -457,9 +457,10 @@ pub(crate) fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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<Value> {
|
||||
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.
|
||||
|
||||
@@ -131,7 +131,7 @@ fn map_graphic(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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());
|
||||
|
||||
Reference in New Issue
Block a user