mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 04:19:59 -06:00
* feat(inquiry): AREA, PERIMETER and DISTANCE system variables, as AutoCAD sets them AREA, DIST and MEASUREGEOM now leave AREA, PERIMETER and DISTANCE for getvar, read-only and not saved: the area just measured (the running total in Add and Subtract mode) with the last area's perimeter, and the distance (the total of Multiple points). After DIST's two points LASTPOINT stays at the first point, as measured. AREA's Object option measures splines, closed by their chord like open polylines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: spline area over narrow spans, finite distances, list order - AREA of a spline samples it span by span (Spline::tessellate_spans), so a narrow knot span no longer drops out of the area. The spread over the spans is one helper shared with Spline::bounds, whose boxes are unchanged. - DIST refuses a distance that isn't finite on all three paths and leaves DISTANCE as it was. - sysvars::list puts the session's values after the drawing's header, so it shows what get returns (AREA, ORTHOMODE, LWDISPLAY...). - LASTPOINT after DIST's Multiple points is pinned to AutoCAD 2024's measured behaviour: the last point entered, also after Length and Undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: measure every knot span of a spline in AREA A spline with more knot spans than half the sample cap fell back to an even grid over the domain, which can step over a narrow span and measure the wrong area: a 100 x 100 square as a degree-1 spline, its right and top edges two spans a millionth wide after 4,500 spans along the bottom, measured 4,998.75. Spread the samples over the spans at any span count: at least one segment per span, within the cap up to MAX_SAMPLES spans and one each past it, as Spline::length integrates every span. One evaluation per span costs 85-285 ms on a million spans in a release build, against 32-52 s for the length AREA already takes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: share AREA's spline samples by how much each span bends Past the sample cap, tessellate_spans gave each knot span one segment, so a bent span among many straight ones was measured by its chord. Example: a quadratic spline along the x-axis, one control point 1000 above it, past 4,500 spans. It measured 750; the area is 1750/3. Each span now keeps one segment. A span whose control points are collinear is straight, so its chord is exact. tessellate's count is shared among the spans that bend, by the cube root of a measure of their control points' convex hull, which holds the span and its chord. The shares are rounded on the running total, so they use the whole count however many spans share it. The example now measures 583.33343, below the cap and past it. The spreading by parameter width that this replaces goes, and span_params with it: bounds is main's code again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * DIST: a measurement refused as too large leaves LASTPOINT at the first point Session::feed sets LASTPOINT to the second point before DIST checks the distance, and the refusal returned before DIST put the first point back, as it does after a measurement. It now does so before refusing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * AREA: a spline too long to measure is too large, not a perimeter of 0 Spline::length reads a length past f64's range as 0, and AREA took that as the spline's perimeter. Spline::checked_length returns None there, and AREA reports the perimeter too large, as it does a polyline's. The overflow was hidden twice: in the sum, and before it in the derivative, which read as zero speed once past f64's range. The length integral now takes the derivative before that (Spline::velocity), so an overflowing speed counts too. param_at_length keeps the speed it had, and finite lengths are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
272 lines
13 KiB
Rust
272 lines
13 KiB
Rust
//! AREA, PERIMETER and DISTANCE as AutoCAD 2024 leaves them after AREA, DIST and MEASUREGEOM
|
|
//! (measured with accoreconsole, running these scripts on these drawings): AREA is the area just
|
|
//! measured, or the running total in Add and Subtract mode, PERIMETER the perimeter of the last
|
|
//! area; DISTANCE the distance measured, or the total of DIST's Multiple points. After DIST's two
|
|
//! points LASTPOINT is still the first; after Multiple points it is the last.
|
|
|
|
use cadcraft_engine::doc::{Common, EntityKind, Handle};
|
|
use cadcraft_engine::geom::{Spline, Vec2};
|
|
use cadcraft_engine::{Input, Session, sysvars};
|
|
use serde_json::{Value, json};
|
|
|
|
fn var(s: &Session, name: &str) -> f64 {
|
|
sysvars::get(s, name).and_then(|v| v.as_f64()).unwrap_or_else(|| panic!("{name} not a number"))
|
|
}
|
|
|
|
fn lastpoint(s: &Session) -> (f64, f64) {
|
|
let v = sysvars::get(s, "LASTPOINT").unwrap();
|
|
(v[0].as_f64().unwrap(), v[1].as_f64().unwrap())
|
|
}
|
|
|
|
fn close(got: f64, want: f64) -> bool {
|
|
(got - want).abs() <= 1e-9 * (1.0 + want.abs())
|
|
}
|
|
|
|
fn handle(v: &Value) -> Handle {
|
|
Handle::parse_hex(v.get("handle").unwrap_or(&v["handles"][0]).as_str().unwrap()).unwrap()
|
|
}
|
|
|
|
fn lisp(s: &mut Session, expr: &str) {
|
|
s.cmdline(expr).unwrap();
|
|
assert!(s.running.is_none(), "{expr}: {}", s.prompt_text());
|
|
}
|
|
|
|
/// The closed polyline with an arc and the circle the Add-mode scripts pick.
|
|
fn p_and_c(s: &mut Session) -> (Handle, Handle) {
|
|
let p = handle(&s.execute("pline", &json!({ "vertices": [[0, 0], { "p": [40, 0], "bulge": 0.5 }, [40, 30], [0, 30]], "closed": true })).unwrap());
|
|
let c = handle(&s.execute("circle", &json!({ "center": [150, 15], "radius": 10 })).unwrap());
|
|
(p, c)
|
|
}
|
|
|
|
#[test]
|
|
fn area_of_points_sets_area_and_perimeter() {
|
|
let mut s = Session::new();
|
|
lisp(&mut s, "(command \"_.AREA\" '(0.0 0.0) '(40.0 0.0) '(40.0 30.0) '(0.0 30.0) \"\")");
|
|
assert!(close(var(&s, "AREA"), 1200.0) && close(var(&s, "PERIMETER"), 140.0));
|
|
}
|
|
|
|
#[test]
|
|
fn add_mode_totals_the_area_and_keeps_the_last_perimeter() {
|
|
// Points, then the circle as an object.
|
|
let mut s = Session::new();
|
|
let (_, c) = p_and_c(&mut s);
|
|
lisp(
|
|
&mut s,
|
|
&format!(
|
|
"(command \"_.AREA\" \"_A\" '(0.0 0.0) '(40.0 0.0) '(40.0 30.0) '(0.0 30.0) \"\" \"_O\" (list (handent \"{}\") '(160.0 15.0)) \"\" \"\")",
|
|
c.hex()
|
|
),
|
|
);
|
|
assert!(close(var(&s, "AREA"), 1514.159265358979), "{}", var(&s, "AREA"));
|
|
assert!(close(var(&s, "PERIMETER"), 62.83185307179586), "{}", var(&s, "PERIMETER"));
|
|
// Both as objects.
|
|
let mut s = Session::new();
|
|
let (p, c) = p_and_c(&mut s);
|
|
lisp(
|
|
&mut s,
|
|
&format!(
|
|
"(command \"_.AREA\" \"_A\" \"_O\" (list (handent \"{}\") '(0.0 15.0)) (list (handent \"{}\") '(160.0 15.0)) \"\" \"\")",
|
|
p.hex(),
|
|
c.hex()
|
|
),
|
|
);
|
|
assert!(close(var(&s, "AREA"), 1671.411490437671), "{}", var(&s, "AREA"));
|
|
assert!(close(var(&s, "PERIMETER"), 62.83185307179586), "{}", var(&s, "PERIMETER"));
|
|
}
|
|
|
|
#[test]
|
|
fn subtract_mode_takes_the_area_off_the_total() {
|
|
// At large coordinates: the polyline added, the circle subtracted.
|
|
let mut s = Session::new();
|
|
let p = handle(
|
|
&s.execute(
|
|
"pline",
|
|
&json!({ "vertices": [[-35000, 81000], { "p": [-33000, 81000], "bulge": 0.5 }, [-33000, 82500], [-35000, 82500]], "closed": true }),
|
|
)
|
|
.unwrap(),
|
|
);
|
|
let c = handle(&s.execute("circle", &json!({ "center": [-27000, 81750], "radius": 500 })).unwrap());
|
|
lisp(
|
|
&mut s,
|
|
&format!(
|
|
"(command \"_.AREA\" \"_A\" \"_O\" (list (handent \"{}\") '(-35000.0 81750.0)) \"\" \"_S\" \"_O\" (list (handent \"{}\") '(-27000.0 81750.0)) \"\" \"\")",
|
|
p.hex(),
|
|
c.hex()
|
|
),
|
|
);
|
|
assert!(close(var(&s, "AREA"), 2607732.399299283), "{}", var(&s, "AREA"));
|
|
assert!(close(var(&s, "PERIMETER"), 3141.592653589793), "{}", var(&s, "PERIMETER"));
|
|
}
|
|
|
|
#[test]
|
|
fn area_of_an_open_spline_closes_it_by_its_chord() {
|
|
let control = [(0.8, 0.25), (0.82, 0.24), (0.84, 0.25), (0.84, 0.28), (0.82, 0.29), (0.8, 0.28), (0.8, 0.25)];
|
|
let spline = Spline {
|
|
degree: 3,
|
|
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 4.0, 4.0, 4.0],
|
|
control: control.iter().map(|&(x, y)| Vec2::new(x, y)).collect(),
|
|
weights: Vec::new(),
|
|
fit: Vec::new(),
|
|
closed: false,
|
|
fit_opts: Default::default(),
|
|
};
|
|
let mut s = Session::new();
|
|
let space = s.space();
|
|
let h = s.doc_mut().unwrap().add(&space, Common::default(), EntityKind::Spline(spline)).unwrap();
|
|
lisp(&mut s, &format!("(command \"_.AREA\" \"_O\" (list (handent \"{}\") '(0.8 0.265)))", h.hex()));
|
|
assert!(close(var(&s, "AREA"), 0.001308333333333334), "{}", var(&s, "AREA"));
|
|
// The spline's length, 0.13046952991870 by an independent integration. AutoCAD reported
|
|
// 0.1304733129441191, 2.9e-5 longer than that: its own approximation of the length.
|
|
assert!(close(var(&s, "PERIMETER"), 0.1304695299187), "{}", var(&s, "PERIMETER"));
|
|
}
|
|
|
|
#[test]
|
|
fn area_of_a_spline_counts_its_narrow_knot_spans() {
|
|
// A degree-1 spline is its control polygon, here the triangle (0,0) (1,0) (0,1): area 0.5,
|
|
// perimeter 2 + sqrt(2), though two of its three knot spans are a billionth of the domain.
|
|
let control = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0)];
|
|
let spline = Spline {
|
|
degree: 1,
|
|
knots: vec![0.0, 0.0, 1e-9, 2e-9, 1.0, 1.0],
|
|
control: control.iter().map(|&(x, y)| Vec2::new(x, y)).collect(),
|
|
..Spline::default()
|
|
};
|
|
let mut s = Session::new();
|
|
let space = s.space();
|
|
let h = s.doc_mut().unwrap().add(&space, Common::default(), EntityKind::Spline(spline)).unwrap();
|
|
lisp(&mut s, &format!("(command \"_.AREA\" \"_O\" (list (handent \"{}\") '(0.5 0.0)))", h.hex()));
|
|
assert!(close(var(&s, "AREA"), 0.5), "{}", var(&s, "AREA"));
|
|
assert!(close(var(&s, "PERIMETER"), 2.0 + 2f64.sqrt()), "{}", var(&s, "PERIMETER"));
|
|
}
|
|
|
|
#[test]
|
|
fn area_of_a_spline_with_more_spans_than_samples_counts_each() {
|
|
// The square (0,0) (100,0) (100,100) (0,100) as a degree-1 spline: 5000 spans along the bottom,
|
|
// then two a millionth wide up the right side and across the top. Area 10000, perimeter (the
|
|
// open spline's length) 300.
|
|
let mut control: Vec<Vec2> = (0..=5000).map(|i| Vec2::new(i as f64 / 50.0, 0.0)).collect();
|
|
control.extend([Vec2::new(100.0, 100.0), Vec2::new(0.0, 100.0)]);
|
|
let mut knots: Vec<f64> = (0..=5000).map(f64::from).collect();
|
|
knots.insert(0, 0.0);
|
|
knots.extend([5000.0 + 1e-6, 5000.0 + 2e-6, 5000.0 + 2e-6]);
|
|
let spline = Spline { degree: 1, knots, control, ..Spline::default() };
|
|
let mut s = Session::new();
|
|
let space = s.space();
|
|
let h = s.doc_mut().unwrap().add(&space, Common::default(), EntityKind::Spline(spline)).unwrap();
|
|
lisp(&mut s, &format!("(command \"_.AREA\" \"_O\" (list (handent \"{}\") '(50.0 0.0)))", h.hex()));
|
|
assert!(close(var(&s, "AREA"), 1e4), "{}", var(&s, "AREA"));
|
|
assert!(close(var(&s, "PERIMETER"), 300.0), "{}", var(&s, "PERIMETER"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_spline_too_long_to_measure_is_refused_and_not_kept() {
|
|
let mut s = Session::new();
|
|
lisp(&mut s, "(command \"_.AREA\" '(0.0 0.0) '(10.0 0.0) '(10.0 10.0) \"\")");
|
|
// Finite ends 2e308 apart: its chord closes no area, but its length is past f64's range. That is
|
|
// too large to report, as a polyline's is, not a perimeter of 0.
|
|
let spline = Spline::from_line(Vec2::new(-1e308, 0.0), Vec2::new(1e308, 0.0), 1);
|
|
let space = s.space();
|
|
let h = s.doc_mut().unwrap().add(&space, Common::default(), EntityKind::Spline(spline)).unwrap();
|
|
let err = s.execute("area", &json!({ "handle": h.hex() })).unwrap_err();
|
|
assert!(err.to_string().contains("the perimeter is too large"), "{err}");
|
|
lisp(&mut s, &format!("(command \"_.AREA\" \"_O\" (list (handent \"{}\") '(0.0 0.0)))", h.hex()));
|
|
assert!(s.log.iter().any(|l| l.contains("The perimeter is too large.")), "{:?}", s.log);
|
|
assert!(close(var(&s, "AREA"), 50.0), "{}", var(&s, "AREA"));
|
|
assert!(close(var(&s, "PERIMETER"), 20.0 + 200f64.sqrt()), "{}", var(&s, "PERIMETER"));
|
|
}
|
|
|
|
#[test]
|
|
fn dist_sets_the_distance_and_leaves_lastpoint_at_the_first_point() {
|
|
let mut s = Session::new();
|
|
lisp(&mut s, "(command \"_.DIST\" '(0.5 0.25) '(0.53 0.29))");
|
|
assert!(close(var(&s, "DISTANCE"), 0.05), "{}", var(&s, "DISTANCE"));
|
|
let (x, y) = lastpoint(&s);
|
|
assert!(close(x, 0.5) && close(y, 0.25), "{x}, {y}");
|
|
}
|
|
|
|
#[test]
|
|
fn dist_with_multiple_points_sets_the_total_and_lastpoint_at_the_last() {
|
|
let mut s = Session::new();
|
|
lisp(&mut s, "(command \"_.DIST\" '(0.0 0.0) \"_M\" '(40.0 0.0) '(40.0 30.0) '(0.0 30.0) \"\")");
|
|
assert!(close(var(&s, "DISTANCE"), 110.0), "{}", var(&s, "DISTANCE"));
|
|
let (x, y) = lastpoint(&s);
|
|
assert!(close(x, 0.0) && close(y, 30.0), "{x}, {y}");
|
|
}
|
|
|
|
#[test]
|
|
fn measuregeom_sets_them_as_dist_and_area_do() {
|
|
let mut s = Session::new();
|
|
// (AutoCAD's MEASUREGEOM asks for the next option after each measurement until eXit; here it
|
|
// ends after one.)
|
|
lisp(&mut s, "(command \"_.MEASUREGEOM\" \"_D\" '(0.0 0.0) '(30.0 40.0))");
|
|
assert!(close(var(&s, "DISTANCE"), 50.0), "{}", var(&s, "DISTANCE"));
|
|
lisp(&mut s, "(command \"_.MEASUREGEOM\" \"_AR\" '(0.5 0.25) '(0.54 0.25) '(0.54 0.28) '(0.5 0.28) \"\")");
|
|
assert!(close(var(&s, "AREA"), 0.0012) && close(var(&s, "PERIMETER"), 0.14), "{} {}", var(&s, "AREA"), var(&s, "PERIMETER"));
|
|
}
|
|
|
|
#[test]
|
|
fn the_json_forms_set_them_too_and_they_are_read_only() {
|
|
let mut s = Session::new();
|
|
s.execute("area", &json!({ "points": [[0, 0], [40, 0], [40, 30], [0, 30]] })).unwrap();
|
|
s.execute("dist", &json!({ "p1": [0, 0], "p2": [30, 40] })).unwrap();
|
|
assert_eq!((var(&s, "AREA"), var(&s, "PERIMETER"), var(&s, "DISTANCE")), (1200.0, 140.0, 50.0));
|
|
for name in ["AREA", "PERIMETER", "DISTANCE"] {
|
|
assert!(s.execute("setvar", &json!({ "name": name, "value": 1 })).is_err(), "{name}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn lastpoint_after_multiple_points_is_the_last_point_entered() {
|
|
// A length typed with Length extends the path, but LASTPOINT stays at the last point picked;
|
|
// after Undo it stays at the point undone (AutoCAD 2024 leaves LASTPOINT at 10,0 and 10,10).
|
|
let mut s = Session::new();
|
|
lisp(&mut s, "(command \"_.DIST\" '(0.0 0.0) \"_M\" '(10.0 0.0) \"_L\" 5.0 \"\")");
|
|
assert!(close(var(&s, "DISTANCE"), 15.0), "{}", var(&s, "DISTANCE"));
|
|
let (x, y) = lastpoint(&s);
|
|
assert!(close(x, 10.0) && close(y, 0.0), "{x}, {y}");
|
|
let mut s = Session::new();
|
|
lisp(&mut s, "(command \"_.DIST\" '(0.0 0.0) \"_M\" '(10.0 0.0) '(10.0 10.0) \"_U\" \"_T\")");
|
|
assert!(close(var(&s, "DISTANCE"), 10.0), "{}", var(&s, "DISTANCE"));
|
|
let (x, y) = lastpoint(&s);
|
|
assert!(close(x, 10.0) && close(y, 10.0), "{x}, {y}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_distance_too_large_to_measure_is_refused_and_not_kept() {
|
|
let mut s = Session::new();
|
|
lisp(&mut s, "(command \"_.DIST\" '(0.5 0.25) '(0.53 0.29))");
|
|
assert!(close(var(&s, "DISTANCE"), 0.05));
|
|
// Finite points whose distance overflows: the JSON form, two picked points, Multiple points.
|
|
assert!(s.execute("dist", &json!({ "p1": [-1e308, 0], "p2": [1e308, 0] })).is_err());
|
|
s.cmdline("dist").unwrap();
|
|
s.input(Input::Point(Vec2::new(-1e308, 0.0))).unwrap();
|
|
s.input(Input::Point(Vec2::new(1e308, 0.0))).unwrap();
|
|
assert!(s.log.last().is_some_and(|l| l.contains("the distance is too large")), "{:?}", s.log.last());
|
|
// LASTPOINT stays at the first point, as after a measurement.
|
|
assert_eq!(lastpoint(&s), (-1e308, 0.0));
|
|
s.cancel();
|
|
s.cmdline("dist").unwrap();
|
|
s.input(Input::Point(Vec2::new(-1e308, 0.0))).unwrap();
|
|
s.cmdline("m").unwrap();
|
|
s.input(Input::Point(Vec2::new(1e308, 0.0))).unwrap();
|
|
s.input(Input::Enter).unwrap();
|
|
assert!(s.log.last().is_some_and(|l| l.contains("the distance is too large")), "{:?}", s.log.last());
|
|
s.cancel();
|
|
assert!(close(var(&s, "DISTANCE"), 0.05), "{}", var(&s, "DISTANCE"));
|
|
}
|
|
|
|
#[test]
|
|
fn the_variable_list_shows_the_sessions_values() {
|
|
// A drawing's header can carry these names; GETVAR reads the session's, and so does the list.
|
|
let mut s = Session::new();
|
|
s.doc_mut().unwrap().header.set_f64("AREA", 99.0);
|
|
// Every DXF header carries ORTHOMODE; the session's ortho is off.
|
|
s.doc_mut().unwrap().header.set_i64("ORTHOMODE", 1);
|
|
lisp(&mut s, "(command \"_.AREA\" '(0.0 0.0) '(10.0 0.0) '(10.0 10.0) \"\")");
|
|
let list = s.execute("sysvars", &json!({})).unwrap();
|
|
assert!(close(var(&s, "AREA"), 50.0));
|
|
assert_eq!(list["AREA"].as_f64(), Some(var(&s, "AREA")), "{}", list["AREA"]);
|
|
assert_eq!(list["ORTHOMODE"], sysvars::get(&s, "ORTHOMODE").unwrap());
|
|
assert_eq!(list["ORTHOMODE"], 0);
|
|
}
|