mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 21:44:57 +00:00
3D: effect validation builds each Extrude and Inflate solid once
Checking an Extrude & Bevel (both kinds) or Inflate effect built every shape's profile about the profiles' bounds, then built the fill and stroke solids again about the bounds of the regions they paint. A filled shape's profile is its fill's solid, so that solid was built twice. When a stroke reaches past the shape, the first build also used bounds the effect does not draw it about. The check now builds the profile on its own only where no drawn solid is built from it: every shape for Rotate, and for Extrude & Bevel and Inflate a shape with no fill (strokes only, or no paint). A filled shape's solid is built once, about the bounds the effect draws it about. The shadow check is unchanged. Tests count the solids the check builds: an object with a fill and a stroke, a group of two such members, an effect on the fill and the shadow on; a shape with no fill still has its profile built.
This commit is contained in:
@@ -223,6 +223,10 @@ pub(crate) fn solid_paint_sources(n: &Node) -> (Vec<Option<&AppearanceItem>>, Ve
|
||||
}
|
||||
|
||||
fn facets(id: &str, path: &PathData, rule: FillRule, bounds: Rect, p: &Value) -> Result<Vec<Facet>, String> {
|
||||
#[cfg(test)]
|
||||
if !is_plane(id) {
|
||||
SOLID_GEOMETRY_BUILT.with(|n| n.set(n.get() + 1));
|
||||
}
|
||||
let view = view_options(p);
|
||||
match id {
|
||||
EXTRUDE => vectorcraft_three_d::extrude(path, rule, bounds, extrude_options(p), view),
|
||||
@@ -292,7 +296,7 @@ fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at:
|
||||
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_profiles(id, p, &shapes, b)?;
|
||||
check_solids(id, p, &shapes)?;
|
||||
return cast_shadow(id, p, &shapes).map(|_| ());
|
||||
}
|
||||
@@ -309,12 +313,24 @@ fn validate_solid_for(doc: Option<&Document>, n: &Node, id: &str, p: &Value, at:
|
||||
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_profiles(id, p, &shapes, b)?;
|
||||
check_solids(id, p, &shapes)?;
|
||||
cast_shadow(id, p, &shapes).map(|_| ())
|
||||
}
|
||||
|
||||
/// Do the solids 3D effect `id` with parameters `p` builds from the profiles of `shapes` about `b`
|
||||
/// build? Only the profiles no solid of [`check_solids`] is built from are built here: every
|
||||
/// shape's for Rotate, and for Extrude & Bevel and Inflate the profile of a shape with no fill,
|
||||
/// whose solids are its strokes' outlines or none. A filled shape's profile is its fill's solid,
|
||||
/// which [`check_solids`] builds once, about the bounds the effect draws it about.
|
||||
fn check_profiles(id: &str, p: &Value, shapes: &[Profile], b: Rect) -> Result<(), String> {
|
||||
shapes
|
||||
.iter()
|
||||
.filter(|s| is_plane(id) || !s.items.iter().any(|i| matches!(i, AppearanceItem::Fill(_))))
|
||||
.try_for_each(|s| facets(id, &s.path, s.rule, b, p).map(|_| ()))
|
||||
}
|
||||
|
||||
/// Do the solids 3D effect `id` with parameters `p` builds from the fills and strokes of `shapes`
|
||||
/// fit, each on its own and all of them together ([`built_solids`])? Rotate's planes are not
|
||||
/// counted.
|
||||
@@ -658,6 +674,9 @@ fn solid_parts(shapes: &[Profile], fallback: Rect) -> (Vec<Part<'_>>, Rect) {
|
||||
thread_local! {
|
||||
/// How many solids [`built_solids`] and [`meshes_for`] have built on this thread, for the tests.
|
||||
static SOLIDS_BUILT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
/// How many Extrude & Bevel (Classic included) and Inflate solids [`facets`] and
|
||||
/// [`built_solids`] have built on this thread, for the tests.
|
||||
static SOLID_GEOMETRY_BUILT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The solids Extrude & Bevel (Classic included) or Inflate `id` with parameters `p` builds from
|
||||
@@ -687,7 +706,11 @@ fn built_solids<'a>(
|
||||
#[cfg(test)]
|
||||
SOLIDS_BUILT.with(|n| n.set(n.get() + 1));
|
||||
let built = match inflated.as_mut() {
|
||||
Some(each) => each.next().unwrap_or_else(|| Ok(vec![])),
|
||||
Some(each) => {
|
||||
#[cfg(test)]
|
||||
SOLID_GEOMETRY_BUILT.with(|n| n.set(n.get() + 1));
|
||||
each.next().unwrap_or_else(|| Ok(vec![]))
|
||||
}
|
||||
None => facets(id, area, *rule, bounds, p),
|
||||
};
|
||||
let result = match built {
|
||||
|
||||
@@ -953,6 +953,48 @@ fn an_objects_solids_count_their_faces_together() {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many Extrude & Bevel and Inflate solids `check` builds, and its result.
|
||||
fn geometry_builds(check: impl FnOnce() -> Result<(), String>) -> (usize, Result<(), String>) {
|
||||
SOLID_GEOMETRY_BUILT.with(|b| b.set(0));
|
||||
let result = check();
|
||||
(SOLID_GEOMETRY_BUILT.with(std::cell::Cell::get), result)
|
||||
}
|
||||
|
||||
/// The check builds each solid the effect draws once, about the bounds it draws it about: one for
|
||||
/// a fill and one for each stroke, on an object, on a group's members and on an appearance item,
|
||||
/// with the shadow on or off.
|
||||
#[test]
|
||||
fn the_check_builds_each_drawn_solid_once() {
|
||||
for id in [EXTRUDE, CLASSIC_EXTRUDE, INFLATE] {
|
||||
let n = square_node();
|
||||
assert_eq!(geometry_builds(|| validate_solid(&n, id, &json!({}))), (2, Ok(())), "{id}: a fill and a stroke");
|
||||
let mut other = square_node();
|
||||
other.id = NodeId(2);
|
||||
let g = Node::group(NodeId(9999), vec![Arc::new(square_node()), Arc::new(other)]);
|
||||
assert_eq!(geometry_builds(|| validate_solid(&g, id, &json!({}))), (4, Ok(())), "{id}: two members' fills and strokes");
|
||||
}
|
||||
for id in [EXTRUDE, INFLATE] {
|
||||
let n = square_node();
|
||||
assert_eq!(geometry_builds(|| validate_three_d_at(&n, id, &json!({}), (Some(0), 0))), (1, Ok(())), "{id}: on the fill");
|
||||
let shadow = json!({"shadow": true});
|
||||
assert_eq!(geometry_builds(|| validate_solid(&n, id, &shadow)), (2, Ok(())), "{id}: with the shadow on");
|
||||
}
|
||||
}
|
||||
|
||||
/// A shape with no fill draws no solid of its profile, so the check builds the profile on its own
|
||||
/// as well as the strokes' solids, and an unpainted shape's profile alone.
|
||||
#[test]
|
||||
fn the_check_builds_the_profile_of_a_shape_with_no_fill() {
|
||||
for id in [EXTRUDE, CLASSIC_EXTRUDE, INFLATE] {
|
||||
let mut stroked = square_node();
|
||||
stroked.appearance.items.retain(|i| matches!(i, AppearanceItem::Stroke(_)));
|
||||
assert_eq!(geometry_builds(|| validate_solid(&stroked, id, &json!({}))), (2, Ok(())), "{id}: a stroke alone");
|
||||
let mut bare = square_node();
|
||||
bare.appearance.items.clear();
|
||||
assert_eq!(geometry_builds(|| validate_solid(&bare, id, &json!({}))), (1, Ok(())), "{id}: no paint");
|
||||
}
|
||||
}
|
||||
|
||||
/// A convex polygon of `corners` corners about (20,000, 20,000), filled red, with no stroke.
|
||||
fn polygon_node(id: u64, corners: usize) -> Node {
|
||||
let points: Vec<Point> = (0..corners)
|
||||
|
||||
@@ -1273,6 +1273,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Extrude & Bevel (both kinds) and Rotate keep the art's curves in the faces they draw, export and expand: caps and the edges of walls and bevels where they meet a cap are cubics with smooth anchors where the art's are, fitted within 0.001 pt in perspective and within the sampling tolerance on a bevel's inset. An edge of a twisted wall that is cut into pieces, a piece of curve that does not fit after 8 halvings, and the curves past 1,048,576 for one solid stay straight (VectorCraft's own values; [three-d.md](three-d.md#what-differs-from-the-public-documentation)). Tests `solid::tests::a_cap_keeps_the_curves_of_the_art`, `a_cap_in_perspective_fits_the_projected_curves`, `a_beveled_cap_and_its_walls_share_their_curves`, `twisted_beveled_tiny_and_degenerate_caps_keep_their_curves`.
|
||||
- Export 3D Object (Asset Export's menu): an object with Extrude & Bevel, Revolve, Inflate or Rotate becomes a 3D asset whose Format is USDA (the default), USDZ, glTF or OBJ, and Export writes each of its solids as a mesh with a material in its paint's color (`assets.add3D`, `assets.set3DFormat`, `assets.export3D`; [export 3D vector artwork](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/export-3d-vector-artwork.html)).
|
||||
- Export 3D Object builds each Extrude & Bevel (both kinds) and Inflate solid once: the same build gives the faces that count toward the object's limit and the faces of its mesh, and the meshes match those of a separate build bit for bit. Tests `mesh::tests::a_meshed_solid_is_built_once`, `an_extruded_solid_and_its_mesh_match_the_two_built_apart`, `inflated_solids_and_their_meshes_match_the_two_built_apart`, `solids::tests_mesh::one_build_exports_the_meshes_built_apart`, `a_stroke_past_the_limits_is_left_out_of_the_export`.
|
||||
- Checking an Extrude & Bevel (both kinds) or Inflate effect builds each solid the effect draws once, about the bounds the effect draws it about; a shape with no fill still has its profile built on its own. Tests `solids::tests::the_check_builds_each_drawn_solid_once`, `the_check_builds_the_profile_of_a_shape_with_no_fill`.
|
||||
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Open ends have continuous caps by default, shading is smooth with one to ten colored lights, and intersecting surfaces' visibility is approximate ([revolve.md](revolve.md)).
|
||||
- Revolve takes graphics on its swept side, shaded with the object's light ([the archived reference, page 185](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=185)). By VectorCraft's own rule a graphic wraps around the axis at its own size at every distance from it, narrows toward the silhouette, follows the profile along its length and is hidden where the side faces away (`threeD.mapGraphic`; `revolve::tests_graphics`, `tests::side_faces_lie_on_the_unrolled_surface_and_caps_do_not`, `map_graphic_puts_a_graphic_on_the_side_of_a_revolve`).
|
||||
- Revolve takes type, as its glyph outlines, and groups or layers, whose members sweep about one axis as [the help page](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html) describes; a surface can have up to 16,384 profile points, and curves take fewer samples when a surface would pass 163,840 faces (#3617).
|
||||
|
||||
Reference in New Issue
Block a user