AutoLISP entity lists hold angles in radians, as entget does in AutoCAD (fixes #1165) (#1190)

This commit is contained in:
Marco Arnulfo
2026-10-19 02:30:36 -04:00
committed by GitHub
parent 2db6ab069a
commit 27fd074f54
3 changed files with 86 additions and 4 deletions
+28 -3
View File
@@ -4,7 +4,8 @@
//! `angtof`).
//!
//! Entity lists use the DXF groups the file reader and writer use (through
//! [`crate::cmd::file::entity_codec`]), so what `entget` shows is what a DXF file holds.
//! [`crate::cmd::file::entity_codec`]), so what `entget` shows is what a DXF file holds, except
//! for angles, which are radians in a list and degrees in a file.
use std::sync::{Arc, Mutex};
@@ -185,6 +186,27 @@ fn point_group(code: i32) -> bool {
(10..=18).contains(&code) || (110..=112).contains(&code) || code == 210 || (1010..=1013).contains(&code)
}
/// Groups 50–58 are angles: degrees in a DXF file, radians in entity lists (DXF Reference,
/// group code value types). `f` converts each one; an MTEXT's 50 after its column type (75) is
/// a column height.
fn convert_angles(kind: &str, tags: &mut [Tag], f: fn(f64) -> f64) {
let mtext = kind.eq_ignore_ascii_case("MTEXT");
for t in tags {
if mtext && t.code == 75 {
break;
}
if !(50..=58).contains(&t.code) {
continue;
}
let v = match t.value {
cadcraft_dxf::Value::Real(r) => r,
cadcraft_dxf::Value::Int(i) => i as f64,
_ => continue,
};
t.value = cadcraft_dxf::Value::Real(f(v));
}
}
fn tag_value(t: &Tag) -> Value {
match &t.value {
cadcraft_dxf::Value::Str(s) | cadcraft_dxf::Value::Hex(s) => Value::str(s),
@@ -274,7 +296,8 @@ pub fn entget(ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispError> {
let h = ename(&a[0])?;
let Ok(d) = ctx.s.doc() else { return Ok(Flow::Value(Value::Nil)) };
let Some(e) = d.entity(h) else { return Ok(Flow::Value(Value::Nil)) };
let (kind, tags) = (codec()?.groups)(d, e);
let (kind, mut tags) = (codec()?.groups)(d, e);
convert_angles(&kind, &mut tags, f64::to_radians);
let layout = match space_of(d, h) {
Some(Space::Paper(name)) => name,
_ => "Model".into(),
@@ -296,6 +319,7 @@ pub fn entget(ctx: &mut Ctx<'_>, a: Vec<Value>) -> Result<Flow, LispError> {
/// The entity a list describes, as the reader makes it; a missing layer (8) is the current one.
fn entity_from_list(ctx: &Ctx<'_>, list: &Value) -> Result<(String, cadcraft_doc::Common, cadcraft_doc::EntityKind), LispError> {
let (kind, mut tags) = list_to_groups(list)?;
convert_angles(&kind, &mut tags, f64::to_degrees);
if !tags.iter().any(|t| t.code == 8) {
let layer = crate::sysvars::get(ctx.s, "CLAYER").and_then(|v| v.as_str().map(str::to_string)).unwrap_or_else(|| "0".into());
tags.insert(0, Tag::s(8, layer));
@@ -616,7 +640,8 @@ fn parse_filter_item(items: &[Value], i: &mut usize, depth: usize) -> Result<Fil
fn matches_filter(d: &cadcraft_doc::Drawing, e: &Entity, f: &Filter) -> bool {
let groups = || match crate::cmd::file::entity_codec() {
Some(c) => {
let (kind, tags) = (c.groups)(d, e);
let (kind, mut tags) = (c.groups)(d, e);
convert_angles(&kind, &mut tags, f64::to_radians);
let mut l = vec![Value::cons(Value::Int(0), Value::str(&kind))];
l.extend(groups_to_list(&tags));
l
+55
View File
@@ -244,6 +244,61 @@ fn entity_lists_round_trip() {
assert_eq!(ok(&mut s, "(handent \"ZZZZZZ\")"), "nil");
}
#[test]
fn entity_lists_hold_angles_in_radians() {
let mut s = session();
let near = |s: &mut Session, expr: &str, want: &str| {
let code = format!("(equal {expr} {want} 1e-9)");
assert_eq!(ok(s, &code), "T", "{expr} is {}, not {want}", ok(s, expr));
};
// A DXF file holds an ARC's 50 and 51 in degrees; entget gives radians (#1165).
s.script("ARC\nC\n0,0\n10,0\n0,10\n").unwrap();
near(&mut s, "(cdr (assoc 50 (entget (entlast))))", "0.0");
near(&mut s, "(cdr (assoc 51 (entget (entlast))))", "(/ pi 2)");
// ssget filters compare against the values entget shows.
assert_eq!(ok(&mut s, "(sslength (ssget \"X\" '((0 . \"ARC\") (-4 . \"<\") (51 . 2.0))))"), "1");
// entmod takes radians.
ok(&mut s, "(setq d (entget (entlast))) (entmod (subst (cons 51 pi) (assoc 51 d) d))");
let end = s.doc().unwrap().model.iter().find_map(|e| match &e.kind {
cadcraft_doc::EntityKind::Arc(a) => Some(a.end),
_ => None,
});
assert!(end.is_some_and(|a| (a - std::f64::consts::PI).abs() < 1e-9), "{end:?}");
// TEXT rotation (50) and oblique angle (51), through entmake.
ok(&mut s, "(entmake (list '(0 . \"TEXT\") '(10 0.0 0.0 0.0) '(40 . 2.5) '(1 . \"abc\") (cons 50 (/ pi 6)) '(51 . 0.25)))");
let text = s.doc().unwrap().model.iter().find_map(|e| match &e.kind {
cadcraft_doc::EntityKind::Text(t) => Some((t.rotation, t.oblique)),
_ => None,
});
assert!(text.is_some_and(|(r, o)| (r - std::f64::consts::FRAC_PI_6).abs() < 1e-9 && (o - 0.25).abs() < 1e-9), "{text:?}");
near(&mut s, "(cdr (assoc 50 (entget (entlast))))", "(/ pi 6)");
near(&mut s, "(cdr (assoc 51 (entget (entlast))))", "0.25");
// HATCH pattern angle (52) and pattern line angle (53): ANSI31 at 0 has its lines at 45°.
s.script("CIRCLE\n0,0\n10\n-HATCH\nP\nANSI31\n1\n0\n0,0\n\n").unwrap();
assert_eq!(ok(&mut s, "(cdr (assoc 0 (entget (entlast))))"), "\"HATCH\"");
near(&mut s, "(cdr (assoc 52 (entget (entlast))))", "0.0");
near(&mut s, "(cdr (assoc 53 (entget (entlast))))", "(/ pi 4)");
// MTEXT: 50 before the column type (75) is the rotation, after it a column height.
ok(
&mut s,
"(entmake (list '(0 . \"MTEXT\") '(10 0.0 0.0 0.0) '(40 . 2.5) '(41 . 10.0) '(1 . \"abc\") (cons 50 (/ pi 3)) '(75 . 2) '(76 . 2) '(79 . 0) '(48 . 10.0) '(49 . 1.0) '(50 . 30.0) '(50 . 20.0)))",
);
let mtext = s.doc().unwrap().model.iter().find_map(|e| match &e.kind {
cadcraft_doc::EntityKind::MText(t) => Some((t.rotation, t.columns.as_ref().map(|c| c.heights.clone()))),
_ => None,
});
assert!(
mtext.as_ref().is_some_and(|(r, h)| (r - std::f64::consts::FRAC_PI_3).abs() < 1e-9 && h.as_deref() == Some(&[30.0, 20.0][..])),
"{mtext:?}"
);
near(&mut s, "(cdr (assoc 50 (entget (entlast))))", "(/ pi 3)");
let d = ok(&mut s, "(entget (entlast))");
assert!(d.contains("(50 . 30.0) (50 . 20.0)"), "{d}");
}
#[test]
fn selection_sets_and_filters() {
let mut s = session();
+3 -1
View File
@@ -78,7 +78,9 @@ reached", malformed source is a reader error, and every built-in checks its argu
- **No-ops:** `vl-load-com gc redraw`; `ver`
Entity lists use the DXF group codes of CADCraft's DXF files: what `entget` returns is what a
DXF file holds for the object, plus `-1` (its name) and `410` (its layout).
DXF file holds for the object, plus `-1` (its name) and `410` (its layout). Angles (groups 50–58)
are in radians, as in AutoCAD, where the file holds degrees; `entmake`, `entmod` and `ssget`
filters take radians too.
## Testing against the corpus