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
@@ -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};@@ -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;@@ -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 {@@ -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:?}");@@ -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.@@ -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(), } } }