Merge pull request #7032 from addibble/taper-one-face

3D: a tapered Extrude & Bevel wall is one face
This commit is contained in:
BFlat
2026-11-14 05:39:24 +07:00
committed by GitHub
4 changed files with 50 additions and 3 deletions
+3 -2
View File
@@ -514,8 +514,9 @@ fn steps(o: &Extrude) -> Vec<Step> {
} else {
out.push(Step { inset: 0.0, t: 0.0, face: 1.0 });
}
// Twist and taper bend the walls: cut them into slices.
let slices = if o.twist != 0.0 || o.taper != 1.0 { ((o.twist.abs() / 6.0).ceil() as usize).clamp(8, 64) } else { 1 };
// A twist bends the walls: cut them into slices. A taper alone keeps each wall flat (its edge
// only scales toward the center as it goes back), so it needs none.
let slices = if o.twist != 0.0 { ((o.twist.abs() / 6.0).ceil() as usize).clamp(8, 64) } else { 1 };
let back_height = if bevel.is_some_and(|b| b.both_sides) { height } else { 0.0 };
let (t0, t1) = (height / d, 1.0 - back_height / d);
for k in 0..=slices {
+44
View File
@@ -154,6 +154,50 @@ fn taper_shrinks_the_back_and_twist_turns_it() {
assert!(open.len() > 2 * 8, "{}", open.len());
}
/// A taper only scales each wall's far edge toward the center, so the wall stays one flat face;
/// cut into slices, each joint between them showed a light tick. A twist bends the walls, which
/// are still cut into slices.
#[test]
fn a_tapered_wall_is_one_flat_face_and_a_twisted_one_is_sliced() {
let (p, b) = shape(&square(0.0, 0.0, 100.0));
let v = View { rotation_x: -35.0, rotation_y: 25.0, ..front() };
// Every side facet, the ones turned away included.
let sides = |o: Extrude, v: View| -> Vec<Facet> {
extrude(&p, FillRule::NonZero, b, Extrude { hidden: true, ..o }, v).unwrap().into_iter().filter(|f| f.side).collect()
};
let o = Extrude { depth: 40.0, taper: 0.475, ..Extrude::default() };
let walls = sides(o, v);
assert_eq!(walls.len(), 4, "one face for each of a square's four walls");
for f in &walls {
let [ring] = &f.view[..] else { panic!("{} contours", f.view.len()) };
let n = super::build::newell(std::slice::from_ref(ring));
let len = n.0.hypot(n.1).hypot(n.2);
let off = |q: &V3| ((q.0 - ring[0].0) * n.0 + (q.1 - ring[0].1) * n.1 + (q.2 - ring[0].2) * n.2) / len;
assert!(ring.iter().all(|q| off(q).abs() < 1e-9), "a flat wall: {:?}", ring.iter().map(off).collect::<Vec<_>>());
}
// Beveled, in perspective or hollow, a taper adds no faces either.
let bevel = Bevel { width: 8.0, height: 8.0, ..Bevel::default() };
let perspective = View { perspective: 60.0, ..v };
for (o, v) in [
(Extrude { bevel: Some(bevel), ..o }, v),
(Extrude { bevel: Some(Bevel { both_sides: true, ..bevel }), ..o }, v),
(Extrude { bevel: Some(Bevel { inside: true, shape: BevelShape::Step, ..bevel }), ..o }, v),
(o, perspective),
(Extrude { cap: false, ..o }, perspective),
] {
assert_eq!(sides(o, v).len(), sides(Extrude { taper: 1.0, ..o }, v).len(), "{o:?} {v:?}");
}
// A curved outline's walls take one band each too: a taper adds no faces to a circle's.
let (cp, cb) = shape(&circle(Point::new(50.0, 50.0), 50.0));
let round = |o: Extrude| extrude(&cp, FillRule::NonZero, cb, Extrude { hidden: true, ..o }, v).unwrap().into_iter().filter(|f| f.side).count();
assert_eq!(round(o), round(Extrude { taper: 1.0, ..o }), "a tapered circle's walls");
// A twist still cuts each wall into at least 8 slices, tapered or not.
for taper in [1.0, o.taper] {
let twisted = sides(Extrude { twist: 30.0, taper, ..o }, v).len();
assert!(twisted >= 4 * 8, "{twisted} faces for a twisted square's walls");
}
}
/// A flat facet 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 piece of a twisted wall whose corners lie off
/// one plane has none.
+1
View File
@@ -1218,6 +1218,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`.
- 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).
- Extrude & Bevel, Inflate and Rotate (Effect › 3D and Materials): live effects in the `three-d` crate with depth, twist, taper, solid or hollow caps, five original bevel shapes (width, height, repeat, spacing, inside, both sides), Inflate's volume, both sides, twist, taper and cap, Rotate's turned fills and strokes, Position presets, the rotation and light gizmos, SVG/PDF vector output and Expand Appearance; one 3D effect per object; covered by geometry, effect, canvas-versus-export, command and dialog tests ([three-d.md](three-d.md)).
- Extrude & Bevel and Inflate build detailed shapes: each solid takes up to 65,536 outline points and 163,840 faces (VectorCraft's own limits, was 2,048 points and 32,768 faces), counted before any point is sampled; when the faces do not fit, every curve takes fewer pieces, down to 4. A closed path of 2,000 curves extrudes with a bevel and inflates, and paths of 5,000 and 10,000 curves build at the default settings (`crates/three-d/src/solid/tests_limits.rs`).
+2 -1
View File
@@ -77,7 +77,7 @@ object is where the art was.
|---|---|
| Depth | 0–2,000 pt. At 0 the object is flat (the art turned like Rotate). |
| Twist | 0° to 360°: the back face turned against the front, the walls following in slices. |
| Taper | 1–100 %: the back face's size against the front's. |
| Taper | 1–100 %: the back face's size against the front's. Without a twist, each wall (and each bevel band) stays flat, so it is one face, not cut into slices. |
| Cap | **Solid** closes the front and back; **Hollow** leaves them open, showing the insides of the walls. |
| Bevel Shape | **None**, **Classic** (a straight chamfer), **Rounded** (convex quarter round), **Cove** (concave), **Step** (two steps) or **Ogee** (an S curve). Our own cross-sections. |
| Width, Height | How far the bevel reaches across the face and along the depth (0–1,000 pt each). The height is limited to the depth (half of it on each side with Bevel both sides). |
@@ -491,6 +491,7 @@ Release measurements and a repeatable CLI scene are in [3D lighting performance]
| Date | Change |
|---|---|
| 2026-11-13 | Extrude & Bevel cuts its walls into slices only for a twist. A taper alone keeps each wall flat, so it is one face, without the light ticks at the joints between slices or the light line down a wall (#7019). Inflate still slices its sides for a taper. |
| 2026-11-12 | The seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins (the seams of Revolve and of colored lights, and 3D (Classic) Wireframe edges, were already round): no thorns on edge-on facets, and facets with no neighbor now have rounded convex corners ([Seams](#seams)). |
| 2026-11-08 | Contours that cross, touch or overlap are merged by vectorcraft-pathops before Extrude & Bevel, Inflate and Rotate build a solid, sharing its limits; others are built as drawn. |
| 2026-11-06 | Extrude & Bevel, Inflate and Rotate apply to groups and layers: every path, compound path and type in them turns about one shared axis (#4506). |