diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 6ceab658..35fdb225 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -207,7 +207,7 @@ pub fn effect_catalog() -> Vec { 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( diff --git a/crates/three-d/src/solid.rs b/crates/three-d/src/solid.rs index f12fc2d7..c1ae0217 100644 --- a/crates/three-d/src/solid.rs +++ b/crates/three-d/src/solid.rs @@ -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), diff --git a/crates/three-d/src/tests_solid.rs b/crates/three-d/src/tests_solid.rs index 424303ba..0f16f190 100644 --- a/crates/three-d/src/tests_solid.rs +++ b/crates/three-d/src/tests_solid.rs @@ -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)); +} diff --git a/crates/ui-egui/src/dialogs/revolve.rs b/crates/ui-egui/src/dialogs/revolve.rs index c778f5e5..38ae84bf 100644 --- a/crates/ui-egui/src/dialogs/revolve.rs +++ b/crates/ui-egui/src/dialogs/revolve.rs @@ -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); diff --git a/docs/mcp.md b/docs/mcp.md index 41f9f557..bb6f72ac 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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`. diff --git a/docs/three-d.md b/docs/three-d.md index 91f4dbd3..b2667194 100644 --- a/docs/three-d.md +++ b/docs/three-d.md @@ -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. |