From 27fd074f548232f79da58dc5cbe9846a9ee7268b Mon Sep 17 00:00:00 2001 From: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Date: Mon, 19 Oct 2026 08:30:36 +0200 Subject: [PATCH] AutoLISP entity lists hold angles in radians, as entget does in AutoCAD (fixes #1165) (#1190) --- crates/engine/src/lisp/drawing.rs | 31 +++++++++++++++-- crates/engine/src/lisp/tests.rs | 55 +++++++++++++++++++++++++++++++ docs/autolisp.md | 4 ++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/lisp/drawing.rs b/crates/engine/src/lisp/drawing.rs index 68922181..573eaa24 100644 --- a/crates/engine/src/lisp/drawing.rs +++ b/crates/engine/src/lisp/drawing.rs @@ -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) -> Result { 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) -> Result { /// 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 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 diff --git a/crates/engine/src/lisp/tests.rs b/crates/engine/src/lisp/tests.rs index f56f02aa..5f44ef5d 100644 --- a/crates/engine/src/lisp/tests.rs +++ b/crates/engine/src/lisp/tests.rs @@ -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(); diff --git a/docs/autolisp.md b/docs/autolisp.md index b75ad7ed..e26702c2 100644 --- a/docs/autolisp.md +++ b/docs/autolisp.md @@ -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