From 2f8b4ddeca6fff710d5316ab5f0be880ba94dbb8 Mon Sep 17 00:00:00 2001 From: Drew Dibble Date: Thu, 12 Nov 2026 19:40:18 -0700 Subject: [PATCH] 3D: a tapered Extrude & Bevel wall is one face Extruded type with a taper showed a row of light ticks along its walls, and with a stroke a long light line down a wall (#7019). A taper only scales each wall's far edge toward the center of the bounds as it goes back: the scale is linear in depth, so without a twist each wall stays a flat trapezoid, bevel bands included. The walls were cut into 8 slices all the same, and at every joint between slices a neighbor's seam painted a light tick onto the wall. Walls are now cut into slices only for a twist, which bends them; the ticks and the long line are gone from tapered, untwisted walls. Twisted walls and Inflate's sides are sliced as before. A tapered wall now draws like an untapered one, with three effects: - A wall seen almost edge-on ends in one longer sliver instead of the last of its slices. With mitered seams that sliver's thorn is longer; round seam joins (#6980) keep it within half the seam's width. - Its seam is as wide as an untapered wall's (the width follows the face's thickness, up to 0.75 pt), wider than a slice's was. - It is sorted by one depth, like an untapered wall, so at a concave corner it can be drawn over a nearer face where the slices had happened to avoid that (#7023). Test: solid::tests::a_tapered_wall_is_one_flat_face_and_a_twisted_one_is_sliced checks that a tapered square's four walls are four flat faces, that a taper adds no faces to a circle's walls, with bevels (both sides, inside), in perspective or hollow, and that a twisted wall is still sliced. With taper slicing it fails (32 wall faces for the square's 4, 608 for the circle's 76). Docs: docs/three-d.md, docs/target-app-parity.md. --- crates/three-d/src/solid/build.rs | 5 ++-- crates/three-d/src/solid/tests.rs | 44 +++++++++++++++++++++++++++++++ docs/target-app-parity.md | 1 + docs/three-d.md | 3 ++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/crates/three-d/src/solid/build.rs b/crates/three-d/src/solid/build.rs index bd4030dfc4..d8c18f292f 100644 --- a/crates/three-d/src/solid/build.rs +++ b/crates/three-d/src/solid/build.rs @@ -505,8 +505,9 @@ fn steps(o: &Extrude) -> Vec { } 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 { diff --git a/crates/three-d/src/solid/tests.rs b/crates/three-d/src/solid/tests.rs index be887180b2..123462b41c 100644 --- a/crates/three-d/src/solid/tests.rs +++ b/crates/three-d/src/solid/tests.rs @@ -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 { + 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::>()); + } + // 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"); + } +} + #[test] fn a_bevel_inside_shrinks_the_cap_and_outside_grows_the_walls() { let (p, b) = shape(&square(0.0, 0.0, 100.0)); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index d5851d78ce..5ea962d294 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1156,6 +1156,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Extrude & Bevel and Inflate treat strokes as outlined: each visible stroke builds a solid from its outline in its own paint beside the top fill's ([three-d.md](three-d.md#fills-and-strokes)). - 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`. + - 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`). diff --git a/docs/three-d.md b/docs/three-d.md index 2eb3e6c7fe..109acc2f45 100644 --- a/docs/three-d.md +++ b/docs/three-d.md @@ -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). | @@ -418,6 +418,7 @@ Release measurements and a repeatable CLI scene are in [3D lighting performance] | Date | Change | |---|---| +| 2026-11-12 | 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-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). | | 2026-10-21 | Four original lighting presets, shared by all four 3D effects and commands, with effective-setting recognition and existing preview/history behavior. |