mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 14:41:52 +00:00
Revolve: overlapping faces draw in the order of their depths where they overlap
Revolve sorted its faces by the depth of each face's middle only, so a tilted profile with long edges, or a profile of several contours, could draw a nearer face under a farther one where the two overlap. - A revolved face carries its view-space plane (none when its corners lie off one plane or reach the eye), and Revolve (Classic)'s facets keep it. - Revolve and Revolve (Classic) put each surface's faces through the local depth order Extrude & Bevel uses for its fill and strokes: when two overlapping faces are drawn the wrong way round, every pair of overlapping faces goes in the order of their depths at the middle of the overlap, within the same 4,000,000-comparison limit. A surface already in the right order keeps it. - A group's members, sorted together by depth, go through the same order across members. Docs: docs/revolve.md, docs/effects-parity.md, docs/target-app-parity.md.
This commit is contained in:
@@ -370,7 +370,9 @@ fn group_shadow(id: NodeId, fx: &Effect, shapes: &[Profile], b: Rect, options: R
|
||||
Ok(crate::solids::shadow::scoped_art(id, &caster, settings, cast, crate::solids::has_masks(shapes)))
|
||||
}
|
||||
|
||||
/// One object's surface of revolution about the axis of `b`, and each face's depth. `copies` counts
|
||||
/// One object's surface of revolution about the axis of `b`, and each face it draws ([`Drawn`]),
|
||||
/// back to front: by depth, and where two faces overlap, by their depths there
|
||||
/// ([`crate::solids::surface_order`]). `copies` counts
|
||||
/// the bytes of the paint copies of every surface and graphic of the object. Revolve (Classic) draws every face
|
||||
/// with its classic surface, as Extrude & Bevel (Classic) does. Revolve's graphics, with the
|
||||
/// symbols of `doc`, are drawn on the faces of the side ([`crate::solids::graphics::surface_parts`]):
|
||||
@@ -388,7 +390,7 @@ fn surface(
|
||||
blend: vectorcraft_doc::color::BlendMode,
|
||||
expand: bool,
|
||||
copies: &mut Copies,
|
||||
) -> Option<(Vec<f64>, Node)> {
|
||||
) -> Option<(Vec<Drawn>, Node)> {
|
||||
// A loaded file or a profile edited past the limits keeps drawing its source path instead
|
||||
// of crashing the frame. Commands validate the same evaluator and report its errors.
|
||||
if fx.id == CLASSIC_REVOLVE {
|
||||
@@ -397,9 +399,12 @@ fn surface(
|
||||
if copies.surfaces > MAX_PAINT_COPIES {
|
||||
return None;
|
||||
}
|
||||
let depths = facets.iter().map(|f| f.depth).collect();
|
||||
let order = crate::solids::surface_order(facets.iter().map(|f| (f.contours.first().map_or(&[][..], Vec::as_slice), f.plane, f.depth)));
|
||||
let facets = in_order(facets, &order);
|
||||
let drawn =
|
||||
facets.iter().map(|f| Drawn { depth: f.depth, points: f.contours.first().cloned().unwrap_or_default(), plane: f.plane }).collect();
|
||||
let p = merged_params(&fx.id, &fx.params);
|
||||
return Some((depths, crate::solids::classic::surface(n, facets, &paint, opacity, blend, &p)));
|
||||
return Some((drawn, crate::solids::classic::surface(n, facets, &paint, opacity, blend, &p)));
|
||||
}
|
||||
let colored = options.light_color != [1.0; 3] || options.lights.is_some();
|
||||
let faces = vectorcraft_three_d::revolve(path, b, options).ok()?;
|
||||
@@ -407,6 +412,8 @@ fn surface(
|
||||
if copies.surfaces > MAX_PAINT_COPIES {
|
||||
return None;
|
||||
}
|
||||
let order = crate::solids::surface_order(faces.iter().map(|f| (f.points.as_slice(), f.plane, f.depth)));
|
||||
let faces = in_order(faces, &order);
|
||||
let graphics = crate::solids::mapped_graphics(&merged_params(&fx.id, &fx.params));
|
||||
let shade = |face: &vectorcraft_three_d::Face, c: Color| graphic_paint(face, c, colored);
|
||||
// Graphics past the limits are left out, and the surface draws without them and their paints'
|
||||
@@ -437,21 +444,28 @@ fn surface(
|
||||
&& n.has_default_transparency())
|
||||
.then(|| vectorcraft_three_d::visible_faces(&faces).ok())
|
||||
.flatten();
|
||||
let faces = visible.unwrap_or_else(|| {
|
||||
faces
|
||||
.into_iter()
|
||||
.map(|f| vectorcraft_three_d::VisibleFace {
|
||||
path: f.path(),
|
||||
brightness: f.brightness,
|
||||
depth: f.depth,
|
||||
gradient: f.gradient,
|
||||
illumination: f.illumination,
|
||||
light_gradient: f.light_gradient,
|
||||
rgb_gradient: f.rgb_gradient,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
let depths: Vec<f64> = faces.iter().map(|f: &vectorcraft_three_d::VisibleFace| f.depth).collect();
|
||||
let (faces, keys): (Vec<vectorcraft_three_d::VisibleFace>, Vec<Drawn>) = match visible {
|
||||
Some(visible) => {
|
||||
let keys = visible.iter().map(|f| Drawn { depth: f.depth, points: vec![], plane: None }).collect();
|
||||
(visible, keys)
|
||||
}
|
||||
None => {
|
||||
let keys = faces.iter().map(|f| Drawn { depth: f.depth, points: f.points.clone(), plane: f.plane }).collect();
|
||||
let faces = faces
|
||||
.into_iter()
|
||||
.map(|f| vectorcraft_three_d::VisibleFace {
|
||||
path: f.path(),
|
||||
brightness: f.brightness,
|
||||
depth: f.depth,
|
||||
gradient: f.gradient,
|
||||
illumination: f.illumination,
|
||||
light_gradient: f.light_gradient,
|
||||
rgb_gradient: f.rgb_gradient,
|
||||
})
|
||||
.collect();
|
||||
(faces, keys)
|
||||
}
|
||||
};
|
||||
let children = faces
|
||||
.into_iter()
|
||||
.map(|face| {
|
||||
@@ -517,7 +531,21 @@ fn surface(
|
||||
let mut group = Node::group(n.id, children);
|
||||
group.opacity = opacity;
|
||||
group.blend = blend;
|
||||
Some((depths, group))
|
||||
Some((keys, group))
|
||||
}
|
||||
|
||||
/// A face [`surface`] draws: its depth and, for [`crate::solids::surface_order`], its polygon on the
|
||||
/// drawing and its plane (none for a face trimmed to the part of it that shows).
|
||||
struct Drawn {
|
||||
depth: f64,
|
||||
points: Vec<Point>,
|
||||
plane: Option<vectorcraft_three_d::DepthPlane>,
|
||||
}
|
||||
|
||||
/// `items` in `order`, a permutation of their indices.
|
||||
fn in_order<T>(items: Vec<T>, order: &[usize]) -> Vec<T> {
|
||||
let mut slots: Vec<Option<T>> = items.into_iter().map(Some).collect();
|
||||
order.iter().filter_map(|&k| slots.get_mut(k).and_then(Option::take)).collect()
|
||||
}
|
||||
|
||||
/// A surface of one object about the bounds of `path`, built with options derived from `fx`.
|
||||
@@ -934,14 +962,17 @@ fn group_art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
} else {
|
||||
let count = surfaces.len();
|
||||
let mut faces = vec![];
|
||||
for (member, (depths, s, scopes)) in surfaces.into_iter().enumerate() {
|
||||
for (member, (drawn, s, scopes)) in surfaces.into_iter().enumerate() {
|
||||
let (opacity, blend) = (s.opacity, s.blend);
|
||||
if let NodeKind::Group { children, .. } = s.kind {
|
||||
faces.extend(depths.into_iter().zip(children).map(|(depth, face)| (depth, member, opacity, blend, scopes, face)));
|
||||
faces.extend(drawn.into_iter().zip(children).map(|(drawn, face)| (drawn, member, opacity, blend, scopes, face)));
|
||||
}
|
||||
}
|
||||
// A stable sort: faces at the same depth keep the members' order.
|
||||
faces.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
// A stable sort: faces at the same depth keep the members' order. Overlapping faces, of one
|
||||
// member or of two, then go in the order of their depths where they overlap.
|
||||
faces.sort_by(|a, b| a.0.depth.total_cmp(&b.0.depth));
|
||||
let order = crate::solids::surface_order(faces.iter().map(|(d, ..)| (d.points.as_slice(), d.plane, d.depth)));
|
||||
let faces = in_order(faces, &order);
|
||||
if opaque {
|
||||
vec![Arc::new(Node::group(n.id, faces.into_iter().map(|(.., face)| face).collect()))]
|
||||
} else {
|
||||
|
||||
@@ -1261,6 +1261,35 @@ fn draw_order(faces: &[Placed], sealed: bool) -> Vec<usize> {
|
||||
local_order(faces, order)
|
||||
}
|
||||
|
||||
/// The order, back to front, of the faces of one or more Revolve surfaces `faces`, given back to
|
||||
/// front by depth: each face's polygon on the drawing, its plane and its depth. When two
|
||||
/// overlapping faces are drawn the wrong way round for their depths where they overlap, every pair
|
||||
/// of overlapping faces with a convex polygon and a plane goes in the order of its depths there,
|
||||
/// the face nearer the viewer after the other ([`local_order`]), and each step draws the face that
|
||||
/// comes first in the given order of those whose faces before are drawn, so a face without a plane
|
||||
/// or a convex polygon can also move. Otherwise the order given.
|
||||
pub(crate) fn surface_order<'a>(faces: impl IntoIterator<Item = (&'a [Point], Option<DepthPlane>, f64)>) -> Vec<usize> {
|
||||
let placed: Vec<Placed> = faces
|
||||
.into_iter()
|
||||
.map(|(points, plane, depth)| {
|
||||
let local = plane.is_some() && convex(points).is_some();
|
||||
// Every face counts as one of a stroke's on one shape, so each overlapping pair is compared.
|
||||
Placed {
|
||||
depth,
|
||||
side: true,
|
||||
away: false,
|
||||
bounds: polygon_bounds(points),
|
||||
member: 0,
|
||||
paints: Paints::Other,
|
||||
outline: if local { points.to_vec() } else { vec![] },
|
||||
plane: plane.filter(|_| local),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let order = (0..placed.len()).collect();
|
||||
local_order(&placed, order)
|
||||
}
|
||||
|
||||
/// `shapes`, the members of group or layer `n`, reshaped by the visible geometry effects `before`
|
||||
/// its 3D effect, about the bounds of all of them.
|
||||
pub(crate) fn reshape_members(n: &Node, before: &[Effect], shapes: &mut [Profile]) {
|
||||
@@ -2174,3 +2203,6 @@ mod tests_sources;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_shadow;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_revolve_order;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::revolve::{CLASSIC_REVOLVE, REVOLVE, revolve_art, revolve_options};
|
||||
|
||||
/// The first pair of faces `(polygon, plane)`, in the order given, drawn the wrong way round where
|
||||
/// their polygons overlap: the earlier one nearer the viewer at the middle of the overlap.
|
||||
fn misordered(faces: &[(Vec<Point>, Option<DepthPlane>)]) -> Option<(usize, usize)> {
|
||||
for (i, (a, pa)) in faces.iter().enumerate() {
|
||||
let Some(pa) = pa.filter(|_| convex(a).is_some()) else { continue };
|
||||
for (j, (b, pb)) in faces.iter().enumerate().skip(i + 1) {
|
||||
let Some(pb) = pb else { continue };
|
||||
let Some(turn) = convex(b) else { continue };
|
||||
if !rects_overlap(polygon_bounds(a), polygon_bounds(b)) {
|
||||
continue;
|
||||
}
|
||||
let Some(mid) = centroid(&clip_convex(a, b, turn)) else { continue };
|
||||
let (Some(da), Some(db)) = (pa.depth_at(mid), pb.depth_at(mid)) else { continue };
|
||||
if da - db > 1e-6 * (1.0 + da.abs().max(db.abs())) {
|
||||
return Some((i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// An open, vase-like profile of straight edges.
|
||||
fn vase() -> PathData {
|
||||
let mut b = BezPath::new();
|
||||
b.move_to((30.0, 20.0));
|
||||
for p in [(90.0, 20.0), (55.0, 65.0), (95.0, 110.0), (30.0, 145.0)] {
|
||||
b.line_to(p);
|
||||
}
|
||||
PathData::from_bezpath(&b)
|
||||
}
|
||||
|
||||
/// The `k`th of five closed 14 × 20 pt rectangles stepping down and away from the axis, as the
|
||||
/// dashes of a dashed line's outline do.
|
||||
fn dash(k: usize) -> PathData {
|
||||
let (x, y) = (30.0 + 12.0 * k as f64, 20.0 + 26.0 * k as f64);
|
||||
let mut b = BezPath::new();
|
||||
b.move_to((x, y));
|
||||
b.line_to((x + 14.0, y));
|
||||
b.line_to((x + 14.0, y + 20.0));
|
||||
b.line_to((x, y + 20.0));
|
||||
b.close_path();
|
||||
PathData::from_bezpath(&b)
|
||||
}
|
||||
|
||||
/// The five [`dash`]es as one compound path.
|
||||
fn dashes() -> PathData {
|
||||
PathData::new((0..5).flat_map(|k| dash(k).subpaths).collect())
|
||||
}
|
||||
|
||||
/// A tilted three-quarter turn of [`vase`], whose faces sorted by their middles' depths draw a
|
||||
/// nearer face under a farther one where the two overlap.
|
||||
fn params() -> Value {
|
||||
json!({"angle": 270, "offset": 12, "rotationX": -30, "rotationY": 0, "perspectiveAngle": 0, "segments": 24, "cap": true})
|
||||
}
|
||||
|
||||
/// A three-quarter turn of [`dashes`], steeply tilted and in wide perspective, whose classic facets
|
||||
/// sorted by their middles' depths draw a nearer facet under a farther one where the two overlap.
|
||||
fn classic_params() -> Value {
|
||||
json!({"angle": 270, "offset": 12, "rotationX": -60, "rotationY": 0, "perspectiveAngle": 80, "cap": true})
|
||||
}
|
||||
|
||||
fn node(path: PathData) -> Node {
|
||||
Node::path(NodeId(1), path, Appearance::basic(Paint::solid(Color::rgb(0.1, 0.7, 0.8)), Paint::None, 0.0))
|
||||
}
|
||||
|
||||
/// The faces of `faces` (polygon, plane and outline) in the order `art` draws their outlines.
|
||||
fn drawn_order(art: &Node, faces: &[(Vec<Point>, Option<DepthPlane>, PathData)]) -> Vec<(Vec<Point>, Option<DepthPlane>)> {
|
||||
let mut out = vec![];
|
||||
art.walk(&mut |n| {
|
||||
if let Some(path) = n.path_data()
|
||||
&& let Some((points, plane, _)) = faces.iter().find(|(.., outline)| outline == path)
|
||||
{
|
||||
out.push((points.clone(), *plane));
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Of a tilted, revolved vase, a face sorted by the depth of its middle goes under a farther face
|
||||
/// that it is in front of where the two overlap. The surface draws the nearer face last there, on
|
||||
/// its own and as a member of a revolved group.
|
||||
#[test]
|
||||
fn a_revolved_surface_draws_the_nearer_of_two_overlapping_faces_last() {
|
||||
let path = vase();
|
||||
let faces = vectorcraft_three_d::revolve(&path, path.bounds().unwrap(), revolve_options(¶ms())).unwrap();
|
||||
let by_depth: Vec<_> = faces.iter().map(|f| (f.points.clone(), f.plane)).collect();
|
||||
assert!(misordered(&by_depth).is_some(), "the faces by depth are drawn the wrong way round somewhere");
|
||||
let keyed: Vec<_> = faces.iter().map(|f| (f.points.clone(), f.plane, f.path())).collect();
|
||||
let mut n = node(path.clone());
|
||||
n.appearance.effects.push(crate::new_effect(REVOLVE, ¶ms()).unwrap());
|
||||
let drawn = drawn_order(&revolve_art(&n).unwrap(), &keyed);
|
||||
assert_eq!(drawn.len(), faces.len());
|
||||
assert_eq!(misordered(&drawn), None, "an object");
|
||||
let mut g = Node::group(NodeId(9), vec![Arc::new(node(path))]);
|
||||
g.appearance.effects.push(crate::new_effect(REVOLVE, ¶ms()).unwrap());
|
||||
let drawn = drawn_order(&revolve_art(&g).unwrap(), &keyed);
|
||||
assert_eq!(drawn.len(), faces.len());
|
||||
assert_eq!(misordered(&drawn), None, "a group's member");
|
||||
}
|
||||
|
||||
/// The faces of a group's members, sorted together by the depths of their middles, go in the order
|
||||
/// of their depths where they overlap, those of two members as well as those of one.
|
||||
#[test]
|
||||
fn a_revolved_groups_members_draw_the_nearer_of_two_overlapping_faces_last() {
|
||||
let p = json!({"angle": 270, "offset": 12, "rotationX": -30, "rotationY": 0, "perspectiveAngle": 0, "segments": 16, "cap": true});
|
||||
let members: Vec<PathData> = (0..5).map(dash).collect();
|
||||
let b = dashes().bounds().unwrap();
|
||||
let mut faces: Vec<_> = members.iter().flat_map(|m| vectorcraft_three_d::revolve(m, b, revolve_options(&p)).unwrap()).collect();
|
||||
faces.sort_by(|a, b| a.depth.total_cmp(&b.depth));
|
||||
let by_depth: Vec<_> = faces.iter().map(|f| (f.points.clone(), f.plane)).collect();
|
||||
assert!(misordered(&by_depth).is_some(), "the faces by depth are drawn the wrong way round somewhere");
|
||||
let keyed: Vec<_> = faces.iter().map(|f| (f.points.clone(), f.plane, f.path())).collect();
|
||||
let mut g = Node::group(NodeId(9), members.into_iter().map(|m| Arc::new(node(m))).collect());
|
||||
g.appearance.effects.push(crate::new_effect(REVOLVE, &p).unwrap());
|
||||
let drawn = drawn_order(&revolve_art(&g).unwrap(), &keyed);
|
||||
assert_eq!(drawn.len(), faces.len());
|
||||
assert_eq!(misordered(&drawn), None);
|
||||
}
|
||||
|
||||
/// Revolve (Classic) draws the nearer of two overlapping facets last where they overlap, too.
|
||||
#[test]
|
||||
fn a_classic_revolved_surface_draws_the_nearer_of_two_overlapping_facets_last() {
|
||||
let path = dashes();
|
||||
// Revolve (Classic) sweeps its surface unshaded, with one facet for each cell.
|
||||
let classic = vectorcraft_three_d::Revolve {
|
||||
shade: false,
|
||||
smooth_shading: false,
|
||||
lights: None,
|
||||
light_color: [1.0; 3],
|
||||
..revolve_options(&crate::merged_params(CLASSIC_REVOLVE, &classic_params()))
|
||||
};
|
||||
let facets = vectorcraft_three_d::revolve_facets(&path, path.bounds().unwrap(), classic).unwrap();
|
||||
let by_depth: Vec<_> = facets.iter().map(|f| (f.contours[0].clone(), f.plane)).collect();
|
||||
assert!(misordered(&by_depth).is_some(), "the facets by depth are drawn the wrong way round somewhere");
|
||||
let keyed: Vec<_> = facets.iter().map(|f| (f.contours[0].clone(), f.plane, f.path())).collect();
|
||||
let mut n = node(path);
|
||||
n.appearance.effects.push(crate::new_effect(CLASSIC_REVOLVE, &classic_params()).unwrap());
|
||||
let drawn = drawn_order(&revolve_art(&n).unwrap(), &keyed);
|
||||
assert_eq!(drawn.len(), facets.len());
|
||||
assert_eq!(misordered(&drawn), None);
|
||||
}
|
||||
|
||||
/// A surface whose faces by depth are already drawn the right way round wherever they overlap keeps
|
||||
/// that order.
|
||||
#[test]
|
||||
fn a_surface_already_in_order_keeps_it() {
|
||||
let path = vase();
|
||||
let p = json!({"angle": 360, "offset": 12, "rotationX": 0, "rotationY": 0, "segments": 24, "cap": true});
|
||||
let faces = vectorcraft_three_d::revolve(&path, path.bounds().unwrap(), revolve_options(&p)).unwrap();
|
||||
let by_depth: Vec<_> = faces.iter().map(|f| (f.points.clone(), f.plane)).collect();
|
||||
assert_eq!(misordered(&by_depth), None);
|
||||
let order = surface_order(faces.iter().map(|f| (f.points.as_slice(), f.plane, f.depth)));
|
||||
assert_eq!(order, (0..faces.len()).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
/// Of two overlapping faces, the one nearer the viewer where they overlap goes after the other,
|
||||
/// whatever their depths; a face without a plane, and one that overlaps neither, keep their places.
|
||||
/// Two faces at the same depth there keep their order.
|
||||
#[test]
|
||||
fn surface_order_puts_the_nearer_face_where_they_overlap_last() {
|
||||
let square = |x: f64| vec![Point::new(x, 0.0), Point::new(x + 10.0, 0.0), Point::new(x + 10.0, 10.0), Point::new(x, 10.0)];
|
||||
// z = 10 - x: at the middle of its overlap with the next square (x 7.5) it is at z 2.5.
|
||||
let r = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let sloped = Some(DepthPlane { normal: [r, 0.0, r], offset: 10.0 * r, center: Point::ZERO, eye: None });
|
||||
let flat = |z: f64| Some(DepthPlane { normal: [0.0, 0.0, 1.0], offset: z, center: Point::ZERO, eye: None });
|
||||
let (a, b, apart) = (square(0.0), square(5.0), square(40.0));
|
||||
let faces = [(&a[..], sloped, 5.0), (&apart[..], flat(0.0), 1.0), (&b[..], flat(3.0), 3.0)];
|
||||
assert_eq!(surface_order(faces), vec![0, 1, 2], "the flat face, nearer at the overlap, is already last");
|
||||
let faces = [(&b[..], flat(2.0), 2.0), (&apart[..], flat(0.0), 3.0), (&a[..], sloped, 5.0)];
|
||||
assert_eq!(surface_order(faces), vec![0, 1, 2], "the sloped face, nearer at the overlap, is already last");
|
||||
// The sloped face's middle (x 5) is at z 5, but the flat face at z 3 is nearer at the overlap.
|
||||
let faces = [(&b[..], flat(3.0), 3.0), (&apart[..], flat(0.0), 4.0), (&a[..], sloped, 5.0)];
|
||||
assert_eq!(surface_order(faces), vec![1, 2, 0], "the flat face is nearer at the overlap");
|
||||
let faces = [(&b[..], None, 3.0), (&apart[..], flat(0.0), 4.0), (&a[..], sloped, 5.0)];
|
||||
assert_eq!(surface_order(faces), vec![0, 1, 2], "no plane");
|
||||
let faces = [(&b[..], flat(2.5), 3.0), (&a[..], sloped, 5.0)];
|
||||
assert_eq!(surface_order(faces), vec![0, 1], "the same depth there");
|
||||
}
|
||||
@@ -371,6 +371,10 @@ pub struct Face {
|
||||
/// For a face of the swept side (not an end cap): where its corners lie on the side surface,
|
||||
/// for a graphic mapped onto it.
|
||||
pub surface: Option<SurfaceCell>,
|
||||
/// The face's plane in view space and the projection it was drawn with, for its depth at a
|
||||
/// point of the drawing ([`DepthPlane::depth_at`]). `None` when its corners lie more than
|
||||
/// 0.001 pt off one plane or a corner is at or past the eye.
|
||||
pub plane: Option<DepthPlane>,
|
||||
}
|
||||
|
||||
/// Where a face of Revolve's swept side lies on the side surface, unrolled flat: x runs around the
|
||||
@@ -638,6 +642,14 @@ fn project_face(vertices: &[V3], normals: Option<&[V3]>, o: Revolve, origin: Poi
|
||||
normal = V3(-normal.0, -normal.1, -normal.2);
|
||||
}
|
||||
let flat_brightness = brightness(&o, normal, light);
|
||||
// As for an extruded face: no plane when a corner lies off it, or when the projection holds a
|
||||
// corner just in front of the eye, so the drawing no longer matches the plane's projection.
|
||||
let centre = vertices.iter().fold(V3::default(), |s, v| V3(s.0 + v.0, s.1 + v.1, s.2 + v.2));
|
||||
let centre = V3(centre.0 / vertices.len() as f64, centre.1 / vertices.len() as f64, centre.2 / vertices.len() as f64);
|
||||
let offset = normal.dot(centre);
|
||||
let held = eye.is_some_and(|e| vertices.iter().any(|v| e - v.2 < e * 1e-3));
|
||||
let flat = !held && vertices.iter().all(|v| (normal.dot(*v) - offset).abs() <= 1e-3);
|
||||
let plane = flat.then_some(DepthPlane { normal: [normal.0, normal.1, normal.2], offset, center: origin, eye });
|
||||
let points: Vec<_> = vertices
|
||||
.iter()
|
||||
.map(|v| {
|
||||
@@ -707,6 +719,7 @@ fn project_face(vertices: &[V3], normals: Option<&[V3]>, o: Revolve, origin: Poi
|
||||
cap: false,
|
||||
visibility_pieces: None,
|
||||
surface: None,
|
||||
plane,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -293,8 +293,8 @@ pub struct Facet {
|
||||
/// (its front). `None` for every other facet.
|
||||
pub end: Option<PlanarFace>,
|
||||
/// The face's plane in view space and the projection it was drawn with, for its depth at a
|
||||
/// point of the drawing ([`Facet::depth_at`]). `None` for Revolve's faces and for a face whose
|
||||
/// corners lie more than 0.001 pt off one plane (a piece of a twisted wall).
|
||||
/// point of the drawing ([`Facet::depth_at`]). `None` for a face whose corners lie more than
|
||||
/// 0.001 pt off one plane (a piece of a twisted wall) or reach the eye.
|
||||
pub plane: Option<DepthPlane>,
|
||||
/// The contours in view space, before projection, for tests that measure depths.
|
||||
#[cfg(test)]
|
||||
@@ -365,7 +365,7 @@ impl Facet {
|
||||
PathData::from_bezpath(&bp)
|
||||
}
|
||||
|
||||
/// Revolve's face `face` as a facet: its polygon, curved outline, depth, normal and lighting.
|
||||
/// Revolve's face `face` as a facet: its polygon, curved outline, depth, plane, normal and lighting.
|
||||
/// A face that joins the profile's ends round the axis is a side; a cap is an end.
|
||||
pub(crate) fn of_face(face: crate::Face) -> Self {
|
||||
Facet {
|
||||
@@ -380,7 +380,7 @@ impl Facet {
|
||||
// A revolved cap is not one of the planar faces graphics map onto.
|
||||
end: None,
|
||||
away: false,
|
||||
plane: None,
|
||||
plane: face.plane,
|
||||
#[cfg(test)]
|
||||
view: vec![],
|
||||
}
|
||||
|
||||
@@ -375,3 +375,35 @@ fn malformed_and_degenerate_inputs_are_bounded() {
|
||||
assert!(revolve(&p, Rect::new(f64::NAN, 0.0, 1.0, 1.0), Revolve::default()).is_err());
|
||||
assert!(revolve(&PathData::default(), b, Revolve::default()).unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// A revolved face carries its plane: the depth it gives at each corner of its drawing is the
|
||||
/// corner's view-space depth, with and without perspective. A face whose corners lie off one plane,
|
||||
/// or with a corner at or past the eye, has none.
|
||||
#[test]
|
||||
fn a_revolved_face_measures_its_depth_on_its_plane() {
|
||||
let o = Revolve { rotation_x: -30.0, rotation_y: 25.0, ..Default::default() }.sanitized();
|
||||
let origin = Point::new(50.0, 80.0);
|
||||
let light = V3(0.0, 0.0, 1.0);
|
||||
let [a, b, c, d] = [V3(-20.0, -10.0, 5.0), V3(20.0, -12.0, -15.0), V3(25.0, 15.0, -20.0), V3(-18.0, 14.0, 4.0)];
|
||||
// The fourth corner moved onto the plane of the first three.
|
||||
let n = b.sub(a).cross(c.sub(a));
|
||||
let d = V3(d.0, d.1, a.2 - (n.0 * (d.0 - a.0) + n.1 * (d.1 - a.1)) / n.2);
|
||||
let flat = [a, b, c, d];
|
||||
for eye in [None, Some(400.0)] {
|
||||
let face = project_face(&flat, None, o, origin, eye, light).unwrap().unwrap();
|
||||
let plane = face.plane.unwrap();
|
||||
for (p, v) in face.points.iter().zip(&flat) {
|
||||
let depth = plane.depth_at(*p).unwrap();
|
||||
assert!((depth - v.2).abs() < 1e-6, "{eye:?}: {depth} against {}", v.2);
|
||||
}
|
||||
let twisted = [a, b, c, V3(d.0, d.1, d.2 + 1.0)];
|
||||
assert!(project_face(&twisted, None, o, origin, eye, light).unwrap().unwrap().plane.is_none(), "{eye:?}");
|
||||
}
|
||||
let past = [a, b, V3(c.0, c.1, 500.0)];
|
||||
assert!(project_face(&past, None, o, origin, Some(400.0), light).unwrap().unwrap().plane.is_none());
|
||||
assert!(project_face(&past, None, o, origin, Some(1000.0), light).unwrap().unwrap().plane.is_some());
|
||||
// Every face of a revolved profile of straight edges has one.
|
||||
let (p, b) = cylinder();
|
||||
let faces = revolve(&p, b, Revolve { rotation_x: -30.0, perspective: 35.0, ..Default::default() }).unwrap();
|
||||
assert!(!faces.is_empty() && faces.iter().all(|f| f.plane.is_some()));
|
||||
}
|
||||
|
||||
@@ -303,6 +303,7 @@ mod tests {
|
||||
cap: false,
|
||||
visibility_pieces: None,
|
||||
surface: None,
|
||||
plane: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ VectorCraft choices; an original approximation, never compared with another appl
|
||||
|
||||
| Item | Ours |
|
||||
|---|---|
|
||||
| Revolve | initial: live, editable profile, rotation and light gizmos, default-on Cap with continuous curved rims, smooth vector shading and round seam joins, SVG/PDF output, Expand Appearance ([revolve.md](revolve.md), #846) |
|
||||
| Revolve | initial: live, editable profile, rotation and light gizmos, default-on Cap with continuous curved rims, smooth vector shading and round seam joins, overlapping faces drawn in the order of their depths where they overlap, SVG/PDF output, Expand Appearance ([revolve.md](revolve.md), #846) |
|
||||
| Extrude & Bevel, Inflate, Rotate | initial: live, round seam joins, depth, twist, taper, solid or hollow caps, five original bevel shapes, Inflate volume, both sides, twist, taper and cap, Rotate of every fill and stroke, groups and layers turned about one shared axis (#4506), contours that cross, touch or overlap merged first ([three-d.md](three-d.md)), Position presets, rotation and light gizmos, SVG/PDF output, Expand Appearance ([three-d.md](three-d.md)) |
|
||||
| Materials panel, lighting, ray-traced render | Window › 3D and Materials panel with the object types (Plane, Extrude, Revolve, Inflate) and the Object, Rotation and Lighting settings of the applied effect, and Quick Actions › Expand as Wireframes ([three-d.md](three-d.md#3d-and-materials-panel)); one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; original Standard, Diffuse, Top Left and Right lighting rigs; hard-edged shadows behind or below the object from the first light ([three-d.md](three-d.md#shadows)); the Materials tab's Graphics, document symbols mapped onto Extrude & Bevel's caps and onto a plane, moved with an artboard handle, and wrapped around Revolve's side ([three-d.md](three-d.md#graphics)); no base materials, graphics on Inflate or on Extrude & Bevel's walls and bevels, soft shadows or ray tracing |
|
||||
| 3D (Classic): Extrude & Bevel, Revolve, Rotate | Extrude & Bevel (Classic) initial: live, round seam joins, Extrude Depth, Cap, five original bevel shapes with Height and Bevel Extent In or Out, Position presets and rotation gizmo, Perspective, Wireframe, No Shading, Diffuse and Plastic surfaces with one to ten lights, Light Intensity, Ambient Light, Highlight Intensity and Size, Blend Steps, Black, Custom or no Shading Color, Draw Hidden Faces ([create 3D objects](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html)); Revolve (Classic) initial: live, round seam joins, Angle, Cap, Offset from the left or right edge, Position presets and rotation gizmo, Perspective, the same four surfaces, lights, highlight, Blend Steps and Shading Color, groups and layers revolved about one shared axis (same page); Rotate (Classic) initial: live, round seam joins, every fill and stroke turned as one plane, Position presets and rotation gizmo, Perspective, No Shading and Diffuse surfaces with lights, Ambient Light, Blend Steps and Shading Color ([rotate objects in 3D](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/rotate-objects-in-3d.html)); Map Art: none |
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ with the standard command.
|
||||
- One Revolve per source object. Edit its options rather than applying another.
|
||||
- Object-level Revolve uses the top visible fill, falling back to a stroke. Per-item Revolve uses that item's paint.
|
||||
- Smooth diffuse shading, one to ten directional lights, no textures, cast shadows or physical materials. Smooth shading interpolates lighting in projected triangles; it does not smooth the silhouette or add specular highlights.
|
||||
- Faces use back-to-front depth sorting. Caps facing the camera cover their rim seams; caps facing away sit behind the body. Intersecting profiles can have incorrect visibility.
|
||||
- Faces are drawn back to front by the depths of their centers. When that order draws the nearer of two overlapping faces of a surface first, every pair of overlapping faces goes in the order of their depths at the middle of where they overlap; the sort compares at most 4,000,000 pairs (VectorCraft's own limit), and the pairs past that keep the depths of their centers. Caps facing the camera cover their rim seams; a cap facing away is drawn behind the body except where it is nearer than a body face it overlaps. Intersecting profiles can have incorrect visibility.
|
||||
- Curves use sixteen samples per segment, and as few as four when a surface would otherwise pass its limits. Surfaces are limited to 16,384 profile points and 163,840 faces, the members of a group together. A straight segment takes one point, a curve takes at least four, and each contour takes one. Smooth shaded side quads become two triangles and share the face limit. A continuous cap retains its original radial mesh cost within this limit. Projected ring curves have a 0.001-point fitting tolerance and a shared 32,768-curve limit.
|
||||
- Axis, bounds and ordinary selection handles continue to refer to the editable 2D source profile.
|
||||
- Extrude & Bevel, Inflate and Rotate are described in [three-d.md](three-d.md). Materials remain unimplemented.
|
||||
|
||||
@@ -1244,6 +1244,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- A 3D effect on one fill or stroke ([the help page](https://helpx.adobe.com/illustrator/using/effects.html)) leaves the object's other fills and strokes drawn with the path's or compound path's fill rule, so an even-odd compound path keeps its holes in them; this holds for Extrude & Bevel, Inflate, Rotate and Revolve, and for the Classic Extrude & Bevel, Rotate and Revolve. Tests: `solids::tests::a_fill_or_stroke_without_the_effect_keeps_the_even_odd_hole`, `revolve::tests::a_fill_without_the_revolve_keeps_the_even_odd_hole`.
|
||||
- With caps and opaque paint, Extrude & Bevel (both kinds) and Inflate draw each side face of the fill's solid before the faces of the same object's strokes that overlap it (strokes on closed paths without dashes, a width profile or a brush, in a solid color or an opaque gradient), so the stroke's walls cover the fill's walls at concave corners instead of showing stripes of the fill. Tests: `solids::tests::a_fills_walls_are_drawn_before_the_stroke_faces_over_them`, `solids::tests::the_draw_order_moves_a_fill_wall_before_the_stroke_faces_it_overlaps`, `solid::tests::facets_tell_the_sides_from_the_ends_and_the_faces_turned_away`.
|
||||
- When the fill and strokes are at full opacity with Normal blending, under a stroke that leaves parts of the fill's edge uncovered (dashed, with a width profile or a brush, on an open subpath, or in a paint that may show through), Extrude & Bevel (both kinds) and Inflate cut each stroke face that passes through a face of the fill's solid, or of another of the shape's strokes, where their depths meet, and draw the faces of the shape's fill and strokes that overlap in the order of their depths where they overlap, instead of by the depths of their centers. Inflate orders a plain stroke the same way, since its fill can rise in front of the stroke. The fill no longer paints over a nearer dash, and a dash's end no longer covers the fill wall in front of it; a fill wall in a gap between dashes stays visible. The same order applies inside a group member's own opacity mask or blending mode, to a gradient with transparent stops under a colored light with graphics on its caps, and to Inflate in gradients under a colored light. Tests: `solids::tests::fill_walls_and_strokes_that_leave_gaps_draw_in_the_order_of_their_depths_where_they_overlap`, `solids::tests::local_order_puts_overlapping_fill_walls_and_stroke_faces_in_depth_order`, `solids::tests::a_stroke_face_through_a_fill_wall_is_cut_where_their_depths_meet`, `solids::tests::a_masked_members_fill_and_stroke_faces_go_in_one_order`, `solids::tests::a_colored_inflate_in_gradients_draws_its_faces_in_one_order`, `solids::tests::a_depth_cycle_keeps_the_cap_last`, `solids::graphics::tests::a_transparent_gradient_with_a_graphic_keeps_the_shared_face_order`.
|
||||
- Revolve and Revolve (Classic) draw two overlapping faces of a surface in the order of their depths where they overlap, instead of by the depths of their centers, on one object and across a group's members, so a tilted profile with long edges or a profile of several contours no longer draws a nearer face under a farther one. A surface whose faces were already in the right order keeps it. Tests: `solids::tests_revolve_order::a_revolved_surface_draws_the_nearer_of_two_overlapping_faces_last`, `solids::tests_revolve_order::a_revolved_groups_members_draw_the_nearer_of_two_overlapping_faces_last`, `solids::tests_revolve_order::a_classic_revolved_surface_draws_the_nearer_of_two_overlapping_facets_last`, `solids::tests_revolve_order::a_surface_already_in_order_keeps_it`, `solids::tests_revolve_order::surface_order_puts_the_nearer_face_where_they_overlap_last`, `tests::a_revolved_face_measures_its_depth_on_its_plane`.
|
||||
- Under a colored light, an Inflate fill and strokes in solid colors, opaque with Normal blending, also draw their faces in that one order, inside a group member's own opacity mask too, instead of each solid as one composite with the stroke's faces over every fill face. Each face keeps the composite's paint, light ramps and seam width; a lone solid, the solids of an object with a translucent or blended fill or stroke, and such solids holding more than 30,720 faces together (VectorCraft's own limit) are still drawn as one composite each. Tests: `solids::tests::colored_solid_fills_and_strokes_share_face_depth_order`, `solids::tests::a_colored_solid_face_on_its_own_keeps_the_composites_colors`, `solids::tests::colored_solid_inflate_batches_channels_and_clips_once_per_surface`, `solids::tests::colored_solids_past_the_face_limit_draw_as_composites`.
|
||||
- Without a twist, a tapered Extrude & Bevel wall (and each bevel band) is one flat face; only a twist cuts walls into slices, so no seam between slices paints a light tick or a light line on a tapered, untwisted wall (#7019). Test: `solid::tests::a_tapered_wall_is_one_flat_face_and_a_twisted_one_is_sliced`.
|
||||
- Extrude & Bevel, Inflate and Rotate apply to type: it makes a solid of its glyph outlines, painted with its own fills and strokes or, when it has none, its characters' colour (the first run's), and stays type, so editing the text reshapes the solid (#4715).
|
||||
|
||||
Reference in New Issue
Block a user