collective

Browse / github.com/storytold/cadcraft / patch

RECTANG draws the vertices AutoCAD stores

RECTANG draws the vertices AutoCAD stores ## What was wrong I checked RECTANG against AutoCAD 2024 as a black box: 64 typed inputs run in `accoreconsole`, each polyline read back with `entget`. The inputs cover all four quadrants, rotation, Dimensions, Area, chamfer, fillet, width, elevation and thickness, at ordinary, survey (VN-2000), large and sub-millimetre coordinates. On `main`, 18 of the 64 match exactly; with this PR, all 64 do, vertex for vertex. | Rule (measured) | `main` | |---|---| | Vertices start at the **first corner** and run along the first axis towards the other corner. Picking down-right or up-left therefore gives

Status
open
Score
0.00
Adopters
0 other operators
Author
2ez4gcx (imported from pull request #2204, open)
Base
d864a08b013353143086bd8bb568c26cb6256796
Size
+373 −55 in 6 files
Review
pending
Shared
2026-10-28
Id
82096303b9fd526d811b66e0

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd.rs crates/engine/src/cmd/draw2.rs crates/engine/src/cmd/draw2/rect.rs crates/engine/src/cmd/draw2/tests.rs crates/engine/src/cmd/helpers.rs crates/engine/src/last_used.rs

diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs--- a/crates/engine/src/cmd.rs+++ b/crates/engine/src/cmd.rs@@ -31,6 +31,7 @@ mod overkill; mod pline_opts; pub mod plotters; mod props;+pub use draw2::RectangModes; pub use draw2::breakline::BreaklineOptions; pub use props::MatchProps; pub(crate) use props::{current_color_value, set_current_color_value};diff --git a/crates/engine/src/cmd/draw2.rs b/crates/engine/src/cmd/draw2.rs--- a/crates/engine/src/cmd/draw2.rs+++ b/crates/engine/src/cmd/draw2.rs@@ -7,7 +7,7 @@ use cadcraft_geom::{Arc, Circle, Ellipse, Line, PolyVertex, Polyline, Spline, TA use serde_json::{Value, json};  use super::curves::{self, Chain, MAX_GEN, TanObj};-use super::helpers::{arc, circle, line, lwpoly, rect_vertices, rect_with_corners, v3};+use super::helpers::{arc, circle, line, lwpoly, rect_vertices, v3};  use super::machines::number; use super::{CommandSpec, bad, bool_or, f64_or, f64_req, point_param, point_req, point_value, points_param, size_param, str_param, targets};@@ -31,6 +31,7 @@ use centermark::{CenterM, run_centerline, run_centermark}; use curves3d::{HelixM, Poly3dM, SplineCvM, run_3dpoly, run_helix, run_spline_cv}; use divide::{DivideM, run_divide, run_measure}; use ellipse::{run_ellipse_arc, run_ellipse_axis};+pub use rect::RectangModes; pub(crate) use rect::{RectM2, run_rectang2}; use revcloud::{CloudKind, RevcloudM, run_revcloud}; pub(crate) use tancircle::TanCircleM;diff --git a/crates/engine/src/cmd/draw2/rect.rs b/crates/engine/src/cmd/draw2/rect.rs--- a/crates/engine/src/cmd/draw2/rect.rs+++ b/crates/engine/src/cmd/draw2/rect.rs@@ -4,8 +4,89 @@ use super::*; // RECTANG with Area / Dimensions / Rotation / Width / Elevation / Thickness // ===================================================================================== +/// RECTANG's corner options, remembered between invocations as AutoCAD remembers them+/// ("Current rectangle modes:  Fillet=2.0000", "Elevation=7.5000  Thickness=2.2500  Rotation=30.96").+#[derive(Clone, Copy, Debug, Default, PartialEq)]+pub struct RectangModes {+    pub chamfer: (f64, f64),+    pub fillet: f64,+    pub width: f64,+    pub elevation: f64,+    pub thickness: f64,+    /// Radians.+    pub rotation: f64,+}++/// The vertices of the rectangle from the origin to the opposite corner `(w, h)` (signed), as+/// AutoCAD lists them: from the first corner along the first axis, so counterclockwise when `w`+/// and `h` share a sign and clockwise otherwise. A fillet or chamfer replaces each corner by the+/// point on the edge into it (where the fillet arc starts, or the first chamfer distance) and+/// the point on the edge out of it (the second chamfer distance). Corners too large for the+/// shorter side leave each corner as two coincident vertices.+fn rect_corner_vertices((w, h): (f64, f64), fillet: f64, chamfer: (f64, f64)) -> Vec<PolyVertex> {+    let corners = [Vec2::ZERO, Vec2::new(w, 0.0), Vec2::new(w, h), Vec2::new(0.0, h)];+    let Some((d_in, d_out)) = corner_cuts(fillet, chamfer) else { return corners.into_iter().map(PolyVertex::new).collect() };+    // A fillet's quarter circle bulges the way the rectangle runs; a chamfer is straight.+    let quarter = cadcraft_geom::arc_to_bulge(std::f64::consts::FRAC_PI_2);+    let bulge = match (fillet > 0.0, w * h < 0.0) {+        (false, _) => 0.0,+        (true, false) => quarter,+        (true, true) => -quarter,+    };+    let fits = corners_fit((w, h), fillet, chamfer);+    let mut out = Vec::with_capacity(8);+    for (i, c) in corners.iter().enumerate() {+        let (Some(prev), Some(next)) = (corners.get((i + 3) % 4), corners.get((i + 1) % 4)) else { continue };+        if fits {+            out.push(PolyVertex::with_bulge(*c - (*c - *prev).normalized() * d_in, bulge));+            out.push(PolyVertex::new(*c + (*next - *c).normalized() * d_out));+        } else {+            out.extend([PolyVertex::new(*c), PolyVertex::new(*c)]);+        }+    }+    out+}++/// How far a fillet or chamfer cuts into the edges into and out of each corner, if one is set.+fn corner_cuts(fillet: f64, chamfer: (f64, f64)) -> Option<(f64, f64)> {+    if fillet > 0.0 {+        Some((fillet, fillet))+    } else if chamfer.0 > 0.0 && chamfer.1 > 0.0 {+        Some(chamfer)+    } else {+        None+    }+}++/// Whether the fillet or chamfer fits the shorter side of a `w` × `h` rectangle (else its+/// corners stay square).+fn corners_fit((w, h): (f64, f64), fillet: f64, chamfer: (f64, f64)) -> bool {+    corner_cuts(fillet, chamfer).is_none_or(|(d_in, d_out)| d_in + d_out <= w.abs().min(h.abs()) * (1.0 + 1e-12))+}++/// The area the rounded or cut corners take off the full rectangle: AutoCAD's Area option sizes+/// the rectangle so its outline, corners included, encloses the area asked for.+fn corner_cut(fillet: f64, chamfer: (f64, f64)) -> f64 {+    if fillet > 0.0 {+        (4.0 - std::f64::consts::PI) * fillet * fillet+    } else if chamfer.0 > 0.0 && chamfer.1 > 0.0 {+        2.0 * chamfer.0 * chamfer.1+    } else {+        0.0+    }+}++/// The sides of the rectangle of `area` whose length (or, with `breadth`, width) is `side`: the+/// corners' cut is added back so the outline encloses the area, unless the corners then don't fit+/// and stay square, when the area is the full rectangle's.+fn area_sides(area: f64, side: f64, breadth: bool, fillet: f64, chamfer: (f64, f64)) -> (f64, f64) {+    let sides = |a: f64| if breadth { (a / side, side) } else { (side, a / side) };+    let cut = sides(area + corner_cut(fillet, chamfer));+    if corners_fit(cut, fillet, chamfer) { cut } else { sides(area) }+}+ fn rect_kind(first: Vec2, (w, h): (f64, f64), rot: f64, fillet: f64, chamfer: (f64, f64), width: f64, elevation: f64) -> EntityKind {-    let local = rect_with_corners(Vec2::ZERO, Vec2::new(w, h), fillet, chamfer);+    let local = rect_corner_vertices((w, h), fillet, chamfer);     let vs = local.into_iter().map(|v| PolyVertex { p: first + v.p.rotate(rot), ..v }).collect();     let mut k = lwpoly(vs, true);     if let EntityKind::LwPolyline(pl) = &mut k {@@ -16,8 +97,9 @@ fn rect_kind(first: Vec2, (w, h): (f64, f64), rot: f64, fillet: f64, chamfer: (f }  /// The width and height of a rectangle from its `dimensions`, its `p2` corner, its `area` with-/// `length` or `breadth`, or its second corner `p2`, rotated by `rot` about `a`.-fn rect_size(p: &Value, a: Vec2, rot: f64, centered: bool) -> Result<(f64, f64)> {+/// `length` or `breadth` (allowing for the `fillet` or `chamfer` corners), or its second corner+/// `p2`, rotated by `rot` about `a`.+fn rect_size(p: &Value, a: Vec2, rot: f64, centered: bool, fillet: f64, chamfer: (f64, f64)) -> Result<(f64, f64)> {     Ok(if let Some(d) = p.get("dimensions").and_then(Value::as_array) {         let l = d.first().and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad("rectang", "dimensions: [length, width]"))?;         let wd = d.get(1).and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad("rectang", "dimensions: [length, width]"))?;@@ -34,8 +116,8 @@ fn rect_size(p: &Value, a: Vec2, rot: f64, centered: bool) -> Result<(f64, f64)>             return Err(bad("rectang", "area must be positive"));         }         match (p.get("length").and_then(Value::as_f64), p.get("breadth").and_then(Value::as_f64)) {-            (Some(l), _) if l.is_finite() && l > 0.0 => (l, area / l),-            (_, Some(b)) if b.is_finite() && b > 0.0 => (area / b, b),+            (Some(l), _) if l.is_finite() && l > 0.0 => area_sides(area, l, false, fillet, chamfer),+            (_, Some(b)) if b.is_finite() && b > 0.0 => area_sides(area, b, true, fillet, chamfer),             _ => return Err(bad("rectang", "area needs `length` or `breadth`")),         }     } else {@@ -64,7 +146,7 @@ pub(crate) fn run_rectang2(s: &mut Session, p: &Value) -> Result<Value> {     }     let fillet = size_param("rectang", p, "fillet", true)?.unwrap_or(0.0);     let width = size_param("rectang", p, "width", true)?.unwrap_or(0.0);-    let (w, h) = rect_size(p, a, rot, centered)?;+    let (w, h) = rect_size(p, a, rot, centered, fillet, ch)?;     if w.abs() < 1e-12 || h.abs() < 1e-12 || !w.is_finite() || !h.is_finite() {         return Err(bad("rectang", "rectangle has no area"));     }@@ -125,8 +207,20 @@ impl RectM2 {         // A rectangle has its own thickness option and does not take THICKNESS.         let t = self.thickness;         s.doc_mut()?.modify_entity(hd, |e| e.common.thickness = t)?;+        self.remember(s);         Ok(Step::Done)     }+    /// Keeps the corner options for the next RECTANG.+    fn remember(&self, s: &mut Session) {+        s.last_used.rectang = RectangModes {+            chamfer: self.chamfer,+            fillet: self.fillet,+            width: self.width,+            elevation: self.elevation,+            thickness: self.thickness,+            rotation: self.rotation,+        };+    }     /// The rectangle's local origin (the corner `rect_kind` draws from) for a size: the first     /// point itself as a corner, or the first point less half the size around the centre.     fn origin(&self, w: f64, h: f64) -> Option<Vec2> {@@ -152,6 +246,17 @@ impl Interactive for RectM2 {     fn name(&self) -> &'static str {         "RECTANG"     }+    /// Starts from the corner options the last RECTANG used.+    fn begin(&mut self, s: &mut Session) -> Result<Step> {+        let m = s.last_used.rectang;+        self.chamfer = m.chamfer;+        self.fillet = m.fillet;+        self.width = m.width;+        self.elevation = m.elevation;+        self.thickness = m.thickness;+        self.rotation = m.rotation;+        Ok(Step::Continue)+    }     /// Asks for the pending option's value, or for the first and second points; in centre mode     /// the first prompt asks for the centre instead of a corner.     fn prompt(&self, _s: &Session) -> Prompt {@@ -204,8 +309,15 @@ impl Interactive for RectM2 {                     self.chamfer.1 = self.chamfer.0;                     self.asking = Some(RAsk::Chamfer2);                 }-                RAsk::Chamfer2 => self.chamfer.1 = val(&i, self.chamfer.1)?.max(0.0),-                RAsk::Fillet => self.fillet = val(&i, self.fillet)?.max(0.0),+                // A rectangle's corners are chamfered or filleted: the option set last wins.+                RAsk::Chamfer2 => {+                    self.chamfer.1 = val(&i, self.chamfer.1)?.max(0.0);+                    self.fillet = 0.0;+                }+                RAsk::Fillet => {+                    self.fillet = val(&i, self.fillet)?.max(0.0);+                    self.chamfer = (0.0, 0.0);+                }                 RAsk::Width => self.width = val(&i, self.width)?.max(0.0),                 RAsk::Elevation => self.elevation = val(&i, self.elevation)?,                 RAsk::Thickness => self.thickness = val(&i, self.thickness)?,@@ -228,7 +340,7 @@ impl Interactive for RectM2 {                         self.asking = Some(a);                         return Err(other("Value must be positive."));                     }-                    let (w, h) = if a == RAsk::AreaLength { (v, self.area / v) } else { (self.area / v, v) };+                    let (w, h) = area_sides(self.area, v, a == RAsk::AreaWidth, self.fillet, self.chamfer);                     return self.make(s, w, h);                 }                 RAsk::DimL => {@@ -257,6 +369,7 @@ impl Interactive for RectM2 {                     }                 }             }+            self.remember(s);             return Ok(Step::Continue);         }         match i {diff --git a/crates/engine/src/cmd/draw2/tests.rs b/crates/engine/src/cmd/draw2/tests.rs--- a/crates/engine/src/cmd/draw2/tests.rs+++ b/crates/engine/src/cmd/draw2/tests.rs@@ -611,7 +611,8 @@ fn rectang_options_command_line() {     s.cmdline("rectang 0,0 r p 0,0 1,1 d 2 2 5,5").unwrap();     let pts = rect_pts(&s);     assert!(pts.iter().any(|p| near(*p, 0.0, 8f64.sqrt())), "{pts:?}");-    s.cmdline("rectang w 0.5 c 0.5 0.5 0,0 4,4").unwrap();+    // The 45° rotation picked above is remembered, as AutoCAD remembers it: set it back.+    s.cmdline("rectang w 0.5 c 0.5 0.5 0,0 r 0 4,4").unwrap();     match last(&s) {         EntityKind::LwPolyline(p) => assert!(p.const_width == 0.5 && p.vertices.len() == 8),         k => panic!("{k:?}"),@@ -623,6 +624,241 @@ fn rectang_options_command_line() {     } } +/// RECTANG against what AutoCAD 2024 stores for the same typed input (black-box: synthetic+/// input in accoreconsole, read back with `entget`): vertex order, chamfer and fillet corners,+/// corners too large for the sides, Area with the corners taken off, and options remembered+/// between invocations.+#[test]+fn rectang_vertices_match_autocad() {+    type Case = (&'static [&'static str], &'static [[f64; 3]], [f64; 3]);+    // (typed input, vertices [x, y, bulge], [width, elevation, thickness])+    let cases: [Case; 12] = [+        // RECTANG-000: the first corner first, along the first axis: clockwise here.+        (+            &["rectang -2.2557479240466947,-9.488221049171962 _R 0 -31.36203661716106,-47.886714708394166"],+            &[+                [-2.255747924046694, -9.488221049171962, 0.0],+                [-31.36203661716105, -9.488221049171962, 0.0],+                [-31.36203661716105, -47.88671470839417, 0.0],+                [-2.255747924046694, -47.88671470839417, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-001: chamfer: the first distance on the edge into each corner, the second on the edge out.+        (+            &["rectang _C 2.2518104656366162 3.766337568886034 4.8089419289768,29.66648885915597 _R 0 28.114797694689763,43.85884965263156"],+            &[+                [4.8089419289768, 31.91829932479258, 0.0],+                [8.575279497862834, 29.66648885915597, 0.0],+                [25.86298722905314, 29.66648885915597, 0.0],+                [28.11479769468976, 33.432826428042, 0.0],+                [28.11479769468976, 41.60703918699494, 0.0],+                [24.34846012580373, 43.85884965263156, 0.0],+                [7.060752394613416, 43.85884965263156, 0.0],+                [4.8089419289768, 40.09251208374553, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-009: the same, running clockwise.+        (+            &["rectang _C 1.6573799714125943 2.8497357251231277 24.04847098725292,25.59232167490267 _R 0 6.633832923690861,65.99502168115245"],+            &[+                [24.04847098725292, 27.24970164631526, 0.0],+                [21.19873526212979, 25.59232167490267, 0.0],+                [8.291212895103454, 25.59232167490267, 0.0],+                [6.63383292369086, 28.4420574000258, 0.0],+                [6.63383292369086, 64.33764170973986, 0.0],+                [9.48356864881399, 65.99502168115245, 0.0],+                [22.39109101584032, 65.99502168115245, 0.0],+                [24.04847098725292, 63.14528595602933, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-018: a fillet running clockwise bulges the other way.+        (+            &["rectang _F 1.5960102281545085 -8.446136404870288,-1.0540502598928434 _R 0 41.8290524978361,-18.73385540701941"],+            &[+                [-8.446136404870288, -2.650060488047352, -0.414213562373095],+                [-6.850126176715778, -1.054050259892843, 0.0],+                [40.23304226968159, -1.054050259892843, -0.414213562373095],+                [41.8290524978361, -2.650060488047352, 0.0],+                [41.8290524978361, -17.1378451788649, -0.414213562373095],+                [40.23304226968159, -18.7338554070194, 0.0],+                [-6.850126176715778, -18.7338554070194, -0.414213562373095],+                [-8.446136404870288, -17.1378451788649, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-025: Chamfer then Fillet: the fillet wins.+        (+            &["rectang _C 1.0 2.0 _F 3.0 -23.770950967162776,10.634038271168158 _R 0 5.6012940597800664,50.84914942110857"],+            &[+                [-23.77095096716277, 13.63403827116815, 0.414213562373095],+                [-20.77095096716277, 10.63403827116815, 0.0],+                [2.601294059780066, 10.63403827116815, 0.414213562373095],+                [5.601294059780066, 13.63403827116815, 0.0],+                [5.601294059780066, 47.84914942110857, 0.414213562373095],+                [2.601294059780066, 50.84914942110857, 0.0],+                [-20.77095096716277, 50.84914942110857, 0.414213562373095],+                [-23.77095096716277, 47.84914942110857, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-026: Fillet then Chamfer: the chamfer wins.+        (+            &["rectang _F 3.0 _C 1.0 2.0 8.075900385075748,-27.116639225773056 _R 0 20.00095065903931,8.864147463029973"],+            &[+                [8.075900385075748, -26.11663922577305, 0.0],+                [10.07590038507574, -27.11663922577305, 0.0],+                [19.00095065903931, -27.11663922577305, 0.0],+                [20.00095065903931, -25.11663922577305, 0.0],+                [20.00095065903931, 7.864147463029972, 0.0],+                [18.00095065903931, 8.864147463029973, 0.0],+                [9.075900385075748, 8.864147463029973, 0.0],+                [8.075900385075748, 6.864147463029972, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-028: Area counts the chamfered corners off.+        (+            &["rectang _C 1.5 2.5 26.36462345136234,-19.7291241693893 _R 0 _A 1442.9297064464572 _W 35.53488991311575"],+            &[+                [26.36462345136234, -18.2291241693893, 0.0],+                [28.86462345136234, -19.7291241693893, 0.0],+                [65.68168268505678, -19.7291241693893, 0.0],+                [67.18168268505678, -17.2291241693893, 0.0],+                [67.18168268505678, 14.30576574372644, 0.0],+                [64.68168268505678, 15.80576574372644, 0.0],+                [27.86462345136234, 15.80576574372644, 0.0],+                [26.36462345136234, 13.30576574372644, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-035-VN: Area counts the fillets off, at survey coordinates.+        (+            &["rectang _F 2.0 612322.0691653182,2398792.948957577 _R 0 _A 592.8936847254046 _L 19.287858563349488"],+            &[+                [612322.0691653182, 2398794.948957577, 0.414213562373095],+                [612324.0691653182, 2398792.948957577, 0.0],+                [612339.3570238815, 2398792.948957577, 0.414213562373095],+                [612341.3570238815, 2398794.948957577, 0.0],+                [612341.3570238815, 2398821.866195597, 0.414213562373095],+                [612339.3570238815, 2398823.866195598, 0.0],+                [612324.0691653182, 2398823.866195598, 0.414213562373095],+                [612322.0691653182, 2398821.866195597, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-029: a fillet too large: each corner two coincident vertices.+        (+            &["rectang _F 11.37084114989061 -21.46774953784037,-0.023473793210982308 _R 0 -2.5163476213560187,49.88119923361903"],+            &[+                [-21.46774953784036, -0.0234737932109823, 0.0],+                [-21.46774953784036, -0.0234737932109823, 0.0],+                [-2.516347621356018, -0.0234737932109823, 0.0],+                [-2.516347621356018, -0.0234737932109823, 0.0],+                [-2.516347621356018, 49.88119923361903, 0.0],+                [-2.516347621356018, 49.88119923361903, 0.0],+                [-21.46774953784036, 49.88119923361903, 0.0],+                [-21.46774953784036, 49.88119923361903, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-039-VN: a chamfer too large, rotated, with Dimensions.+        (+            &[+                "rectang _C 18.656567079201356 18.656567079201356 612317.9599920243,2398739.0200147885 _R 30 _D 26.652238684573366 32.64533161964191 612344.6122307089,2398771.6653464083",+            ],+            &[+                [612317.9599920243, 2398739.020014788, 0.0],+                [612317.9599920243, 2398739.020014788, 0.0],+                [612341.041507793, 2398752.34613413, 0.0],+                [612341.041507793, 2398752.34613413, 0.0],+                [612324.7188419831, 2398780.617820628, 0.0],+                [612324.7188419831, 2398780.617820628, 0.0],+                [612301.6373262145, 2398767.291701286, 0.0],+                [612301.6373262145, 2398767.291701286, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+        // RECTANG-030: rotation by two points, elevation and thickness.+        (+            &["rectang _E 7.5 _T 2.25 -18.42481827666694,-20.50343704206707 _R _P 0.0,0.0 10.0,6.0 10.197290578707722,25.5222006119985"],+            &[+                [-18.42481827666694, -20.50343704206706, 0.0],+                [22.92627837613747, 4.307220949615576, 0.0],+                [10.19729057870772, 25.5222006119985, 0.0],+                [-31.15380607409669, 0.7115426203158534, 0.0],+            ],+            [0.0, 7.5, 2.25],+        ),+        // RECTANG-024: the fillet is remembered for the next RECTANG.+        (+            &[+                "rectang _F 2.0 -12.364845593042645,8.36037449067684 _R 0 31.044931977794846,17.998089850295315",+                "rectang 41.044931977794846,27.998089850295315 84.45470954863234,37.63580520991379",+            ],+            &[+                [41.04493197779484, 29.99808985029531, 0.414213562373095],+                [43.04493197779485, 27.99808985029531, 0.0],+                [82.45470954863234, 27.99808985029531, 0.414213562373095],+                [84.45470954863234, 29.99808985029531, 0.0],+                [84.45470954863234, 35.63580520991379, 0.414213562373095],+                [82.45470954863234, 37.63580520991379, 0.0],+                [43.04493197779485, 37.63580520991379, 0.414213562373095],+                [41.04493197779484, 35.63580520991379, 0.0],+            ],+            [0.0, 0.0, 0.0],+        ),+    ];+    let same = |a: f64, b: f64| (a - b).abs() <= 1e-9 * a.abs().max(b.abs()).max(1.0);+    for (input, want, [width, elevation, thickness]) in cases {+        let mut s = Session::new();+        for line in input {+            s.cmdline(line).unwrap();+            assert!(s.running.is_none(), "{line}: {}", s.prompt_text());+        }+        let e = s.doc().unwrap().model.last().unwrap().clone();+        let EntityKind::LwPolyline(p) = &e.kind else { panic!("{input:?}: {:?}", e.kind) };+        let got: Vec<[f64; 3]> = p.vertices.iter().map(|v| [v.p.x, v.p.y, v.bulge]).collect();+        let ok = got.len() == want.len() && got.iter().zip(want).all(|(g, w)| g.iter().zip(w).all(|(a, b)| same(*a, *b)));+        assert!(+            ok,+            "{input:?}+ got {got:?}+want {want:?}"+        );+        assert!(p.closed && same(p.const_width, width) && same(p.elevation, elevation) && same(e.common.thickness, thickness), "{input:?}");+    }+    // Rotation, elevation and thickness are remembered too: AutoCAD opens the next RECTANG with+    // "Current rectangle modes:  Elevation=7.5000  Thickness=2.2500  Rotation=30.96".+    let mut s = Session::new();+    s.cmdline("rectang _E 7.5 _T 2.25 0,0 _R _P 0,0 10,6 10,20").unwrap();+    s.cmdline("rectang 100,100 110,105").unwrap();+    let e = s.doc().unwrap().model.last().unwrap().clone();+    let EntityKind::LwPolyline(p) = &e.kind else { panic!("{:?}", e.kind) };+    let edge = p.vertices[1].p - p.vertices[0].p;+    assert!(p.vertices[0].p == Vec2::new(100.0, 100.0) && same(edge.angle(), 6f64.atan2(10.0)), "{p:?}");+    assert!(p.elevation == 7.5 && e.common.thickness == 2.25);+}++/// Area allows for cut corners only while they fit: a fillet too large for the resulting sides+/// leaves square corners, so the rectangle is the full area asked for (4 × 1, not 4 × 1.858).+#[test]+fn rectang_area_with_corners_too_large_stays_exact() {+    let check = |s: &Session| {+        let pts = rect_pts(s);+        let b = cadcraft_geom::Bounds2::from_points(pts.clone());+        assert!(near(b.min, 0.0, 0.0) && near(b.max, 4.0, 1.0), "{pts:?}");+        assert!(pts.len() == 8 && pts.chunks(2).all(|c| c[0] == c[1]), "{pts:?}");+    };+    let mut s = Session::new();+    s.cmdline("rectang _F 2 0,0 _A 4 _L 4").unwrap();+    check(&s);+    s.execute("rectang", &json!({"p1": [0, 0], "area": 4, "length": 4, "fillet": 2})).unwrap();+    check(&s);+}+ /// Guards the JSON `center: true` form for every sizing route: a picked corner mirrored about /// the centre, full-size `dimensions`, full `area`, rotation about the centre, that the default /// corner mode is untouched, and that centre mode without a size is rejected.@@ -658,7 +894,7 @@ fn rectang_center_mode_json() { }  /// Guards that fillet, chamfer, width and thickness still apply in centre mode, and that an-/// oversized fillet falls back to sharp corners without moving the rectangle off centre.+/// oversized fillet leaves sharp corners without moving the rectangle off centre. #[test] fn rectang_center_mode_honours_options() {     let mut s = Session::new();@@ -675,9 +911,11 @@ fn rectang_center_mode_honours_options() {     assert_eq!(rect_pts(&s).len(), 8);     let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));     assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");-    // A fillet too large for the rectangle falls back to sharp corners, still centred.+    // A fillet too large for the rectangle leaves sharp corners, each one two coincident vertices+    // as in AutoCAD, still centred.     s.execute("rectang", &json!({"center": true, "p1": [10, 10], "p2": [14, 16], "fillet": 5})).unwrap();-    assert_eq!(rect_pts(&s).len(), 4);+    let pts = rect_pts(&s);+    assert!(pts.len() == 8 && pts.chunks(2).all(|c| c[0] == c[1]), "{pts:?}");     let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));     assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}"); }@@ -705,8 +943,9 @@ fn rectang_center_mode_command_line() {     assert!(s.running.is_none(), "{}", s.prompt_text());     let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));     assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");-    // Area too: the rectangle of that area is centred on the first point.-    s.cmdline("rectang ce 10,10 a 96 l 8").unwrap();+    // Area too: the rectangle of that area is centred on the first point (without the fillet+    // remembered from above, which would take its corners off the area).+    s.cmdline("rectang f 0 ce 10,10 a 96 l 8").unwrap();     assert!(s.running.is_none(), "{}", s.prompt_text());     let b = cadcraft_geom::Bounds2::from_points(rect_pts(&s));     assert!(near(b.min, 6.0, 4.0) && near(b.max, 14.0, 16.0), "{b:?}");diff --git a/crates/engine/src/cmd/helpers.rs b/crates/engine/src/cmd/helpers.rs--- a/crates/engine/src/cmd/helpers.rs+++ b/crates/engine/src/cmd/helpers.rs@@ -62,45 +62,6 @@ pub fn polygon_from_edge(p1: Vec2, p2: Vec2, sides: usize) -> Vec<PolyVertex> {     pts.into_iter().map(PolyVertex::new).collect() } -/// Rectangle vertices with fillet radius or chamfer distances at every corner.-pub fn rect_with_corners(a: Vec2, b: Vec2, fillet: f64, chamfer: (f64, f64)) -> Vec<PolyVertex> {-    let (x0, x1) = (a.x.min(b.x), a.x.max(b.x));-    let (y0, y1) = (a.y.min(b.y), a.y.max(b.y));-    let w = x1 - x0;-    let h = y1 - y0;-    if fillet > 0.0 && fillet * 2.0 <= w.min(h) + 1e-12 {-        let r = fillet;-        let bul = cadcraft_geom::arc_to_bulge(std::f64::consts::FRAC_PI_2);-        return vec![-            PolyVertex::new(Vec2::new(x0 + r, y0)),-            PolyVertex::with_bulge(Vec2::new(x1 - r, y0), bul),-            PolyVertex::new(Vec2::new(x1, y0 + r)),-            PolyVertex::with_bulge(Vec2::new(x1, y1 - r), bul),-            PolyVertex::new(Vec2::new(x1 - r, y1)),-            PolyVertex::with_bulge(Vec2::new(x0 + r, y1), bul),-            PolyVertex::new(Vec2::new(x0, y1 - r)),-            PolyVertex::with_bulge(Vec2::new(x0, y0 + r), bul),-        ];-    }-    let (c1, c2) = chamfer;-    if c1 > 0.0 && c2 > 0.0 && c1 + c1 <= w + 1e-12 && c2 + c2 <= h + 1e-12 {-        return [-            Vec2::new(x0 + c1, y0),-            Vec2::new(x1 - c1, y0),-            Vec2::new(x1, y0 + c2),-            Vec2::new(x1, y1 - c2),-            Vec2::new(x1 - c1, y1),-            Vec2::new(x0 + c1, y1),-            Vec2::new(x0, y1 - c2),-            Vec2::new(x0, y0 + c2),-        ]-        .into_iter()-        .map(PolyVertex::new)-        .collect();-    }-    rect_vertices(Vec2::new(x0, y0), Vec2::new(x1, y1))-}- // ---------- the user's support files ----------  /// Largest support file (linetype, hatch pattern or alias file) read.diff --git a/crates/engine/src/last_used.rs b/crates/engine/src/last_used.rs--- a/crates/engine/src/last_used.rs+++ b/crates/engine/src/last_used.rs@@ -25,6 +25,8 @@ pub struct LastUsed {     pub mleader_block: String,     /// BREAKLINE's Block, Size and Extension.     pub breakline: cmd::BreaklineOptions,+    /// RECTANG's chamfer, fillet, width, elevation, thickness and rotation.+    pub rectang: cmd::RectangModes, }  impl Default for LastUsed {@@ -39,6 +41,7 @@ impl Default for LastUsed {             mleader: MLeaderOptions::default(),             mleader_block: String::new(),             breakline: cmd::BreaklineOptions::default(),+            rectang: cmd::RectangModes::default(),         }     } }