3D and Materials: Extrude & Bevel's Twist runs 0° to 360°, as documented

The public 3D and Materials panel options give Twist as up to 360°; it took
-360° to 360°. Values beyond either end now hold at that end (the effect is
a day old, so only fresh files carry a negative twist).
This commit is contained in:
Ayberk A. 2026-10-12 22:09:08 +03:00 committed by bflatastic
parent fb2089aaf5
commit e750891912
6 changed files with 17 additions and 6 deletions

View File

@ -207,7 +207,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
EXTRUDE,
"Extrude & Bevel…",
&["Effect", "3D and Materials"],
"{depth: pt (0..2000, 50), twist: degrees (-360..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: hollow), bevel: none|classic|rounded|cove|step|ogee (none), bevelWidth: pt (0..1000, 4), bevelHeight: pt (0..1000, 4), bevelRepeat: integer (1..32, 1), bevelSpace: % (0..90, 0; flat between repeats), bevelInside: bool (false: added around the shape; true: carved into it), bevelBothSides: bool (false), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspective: % (0..100, 0), shade: bool (true), lightAzimuth: degrees (-45), lightElevation: degrees (45), lightIntensity: % (80), ambient: % (25)}. The front face is the art, extruded backwards about the centre of its bounds; painted with the top fill (else the top stroke).",
"{depth: pt (0..2000, 50), twist: degrees (0..360, 0; the back turned against the front), taper: % (1..100, 100; the back's size), cap: bool (true: solid; false: hollow), bevel: none|classic|rounded|cove|step|ogee (none), bevelWidth: pt (0..1000, 4), bevelHeight: pt (0..1000, 4), bevelRepeat: integer (1..32, 1), bevelSpace: % (0..90, 0; flat between repeats), bevelInside: bool (false: added around the shape; true: carved into it), bevelBothSides: bool (false), rotationX: degrees (-180..180, -21.88), rotationY: degrees (-180..180, -23.4), rotationZ: degrees (-180..180, 9.06; the defaults are the Off-Axis Front position), perspective: % (0..100, 0), shade: bool (true), lightAzimuth: degrees (-45), lightElevation: degrees (45), lightIntensity: % (80), ambient: % (25)}. The front face is the art, extruded backwards about the centre of its bounds; painted with the top fill (else the top stroke).",
json!({"depth":50.0,"twist":0.0,"taper":100.0,"cap":true,"bevel":"none","bevelWidth":4.0,"bevelHeight":4.0,"bevelRepeat":1,"bevelSpace":0.0,"bevelInside":false,"bevelBothSides":false,"rotationX":-21.88,"rotationY":-23.4,"rotationZ":9.06,"perspective":0.0,"shade":true,"lightAzimuth":-45.0,"lightElevation":45.0,"lightIntensity":80.0,"ambient":25.0}),
),
g(

View File

@ -161,7 +161,7 @@ impl Default for Bevel {
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Extrude {
pub depth: f64,
/// The turn of the back face against the front, about the shape's centre.
/// The turn of the back face against the front, about the shape's centre: 0° to 360°.
pub twist: f64,
/// The size of the back face, as a share (0.01..1) of the front's.
pub taper: f64,
@ -233,7 +233,7 @@ impl Extrude {
fn sanitized(self) -> Self {
Self {
depth: finite(self.depth, 50.0, 0.0, 2000.0),
twist: finite(self.twist, 0.0, -360.0, 360.0),
twist: finite(self.twist, 0.0, 0.0, 360.0),
taper: finite(self.taper, 1.0, 0.01, 1.0),
bevel: self.bevel.map(|b| Bevel {
width: finite(b.width, 4.0, 0.0, 1000.0),

View File

@ -362,3 +362,14 @@ fn the_edge_index_looks_along_the_side_from_outside() {
let (fast, slow) = (edges.distance(q), edge_distance(&rings, q));
assert!((fast - slow).abs() < 1e-9, "{fast} against {slow}");
}
/// Twist runs 0° to 360°, as documented: beyond either end it holds at that end.
#[test]
fn twist_holds_between_0_and_360_degrees() {
let (p, b) = shape(&square(0.0, 0.0, 100.0));
let v = View { rotation_x: -20.0, rotation_y: 30.0, ..front() };
let at = |twist: f64| extrude(&p, b, Extrude { twist, ..Extrude::default() }, v).unwrap();
assert_eq!(at(-45.0), at(0.0));
assert_eq!(at(500.0), at(360.0));
assert_ne!(at(45.0), at(0.0));
}

View File

@ -167,7 +167,7 @@ fn solid_controls(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id: &
fn extrude_controls(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
heading(ui, tl!("Object"));
number(ui, d, "depth", tl!("Depth"), (0.0, 2000.0), "", 1, Some(unit));
number(ui, d, "twist", tl!("Twist"), (-360.0, 360.0), "°", 1, None);
number(ui, d, "twist", tl!("Twist"), (0.0, 360.0), "°", 1, None);
number(ui, d, "taper", tl!("Taper"), (1.0, 100.0), "%", 1, None);
choice(ui, d, "cap", c3("Cap"), &[(json!(true), c3("Solid")), (json!(false), c3("Hollow"))]);
ui.add_space(10.0);

View File

@ -949,7 +949,7 @@ See [Revolve](revolve.md) for the scope and limitations.
the other 3D types to a path or compound path, live like Revolve (the source stays editable; SVG/PDF exports and
`effect.expandAppearance` use the same shaded faces). An object takes one 3D effect, Revolve included.
`threeD.extrude`: `depth` (points, 0..2000, default 50), `twist` (degrees, -360..360, 0), `taper` (1..100%, 100),
`threeD.extrude`: `depth` (points, 0..2000, default 50), `twist` (degrees, 0..360, 0), `taper` (1..100%, 100),
`cap` (true solid, false hollow), `bevel` (`none`, `classic`, `rounded`, `cove`, `step`, `ogee`), `bevelWidth`
and `bevelHeight` (points, default 4), `bevelRepeat` (1..32), `bevelSpace` (0..90%), `bevelInside`,
`bevelBothSides`. `threeD.inflate`: `depth` (points, default 0), `volume` (0..100%, default 50), `bothSides`.

View File

@ -24,7 +24,7 @@ object is where the art was.
| Option | What it does |
|---|---|
| Depth | 0–2,000 pt. At 0 the object is flat (the art turned like Rotate). |
| Twist | −360° to 360°: the back face turned against the front, the walls following in slices. |
| 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. |
| 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. |