From 3fa7275e4fd64bf7eb4eef9ae4420a04579145bf Mon Sep 17 00:00:00 2001 From: Coast Date: Mon, 19 Oct 2026 00:31:12 -0600 Subject: [PATCH] DXF: $ANGBASE is in degrees (#1234) AutoCAD writes $ANGBASE in degrees (a drawing whose angles run clockwise from north has 90.0); CADCraft read and wrote it as radians, so such a drawing's angle base came in as 90 radians (about 116.6 degrees), turning typed angles, polar tracking and angles shown in Properties. The reader now converts to radians (the variable's unit), the writer back, and the DWG reader's hand-off of the value in DXF form gives degrees. Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- crates/dwg/src/lib.rs | 3 ++- crates/engine/src/sysvars.rs | 2 +- crates/engine/src/units.rs | 2 +- crates/io/src/dxf_read.rs | 3 +++ crates/io/src/dxf_write.rs | 2 ++ crates/io/tests/angle_header.rs | 19 +++++++++++++++++-- 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs index 9fe25dc3e..7c7da02a6 100644 --- a/crates/dwg/src/lib.rs +++ b/crates/dwg/src/lib.rs @@ -330,7 +330,8 @@ mod native { /// the HEADER section: (name, group code, value). fn unwritten_header_vars(h: &acadrust::document::HeaderVariables) -> [(&'static str, i32, String); 7] { [ - ("$ANGBASE", 50, h.angle_base.to_string()), + // DWG holds radians; `$ANGBASE` is in degrees. + ("$ANGBASE", 50, h.angle_base.to_degrees().to_string()), ("$ANGDIR", 70, h.angle_direction.to_string()), ("$FILLETRAD", 40, h.fillet_radius.to_string()), ("$CHAMFERA", 40, h.chamfer_distance_a.to_string()), diff --git a/crates/engine/src/sysvars.rs b/crates/engine/src/sysvars.rs index 0dd7503bd..6cb6b9e92 100644 --- a/crates/engine/src/sysvars.rs +++ b/crates/engine/src/sysvars.rs @@ -130,7 +130,7 @@ pub fn get(s: &Session, name: &str) -> Option { "MAXARRAY" => json!(st.maxarray), "ZOOMFACTOR" => json!(st.zoomfactor), "LASTPOINT" => json!([s.last_point.x, s.last_point.y, 0.0]), - // Held in radians (as `$ANGBASE` in DXF); reported in degrees, like POLARANG. + // Held in radians (`$ANGBASE` in DXF is in degrees); reported in degrees, like POLARANG. "ANGBASE" => json!(s.doc().ok()?.header.f64("ANGBASE", 0.0).to_degrees()), "CONSTRAINTINFER" => json!(i32::from(s.doc().ok()?.parametric.settings.infer)), // Process-wide font substitution (profile settings, not saved in the drawing). diff --git a/crates/engine/src/units.rs b/crates/engine/src/units.rs index dd15111c3..44af14f9e 100644 --- a/crates/engine/src/units.rs +++ b/crates/engine/src/units.rs @@ -157,7 +157,7 @@ fn parse_mixed(s: &str) -> Option { pub struct AngleSettings { /// AUNITS: 0 decimal degrees, 1 degrees/minutes/seconds, 2 grads, 3 radians, 4 surveyor's. pub aunits: i64, - /// ANGBASE: the direction of angle 0, in radians counterclockwise from +X (as `$ANGBASE` in DXF). + /// ANGBASE: the direction of angle 0, in radians counterclockwise from +X (`$ANGBASE` in DXF is in degrees). pub angbase: f64, /// ANGDIR 1: positive angles turn clockwise. pub clockwise: bool, diff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs index c043842eb..92d803e17 100644 --- a/crates/io/src/dxf_read.rs +++ b/crates/io/src/dxf_read.rs @@ -1341,6 +1341,9 @@ fn header(tags: &[Tag], d: &mut Drawing) { }, // $DIMJOGANG is in radians; the variable (like the style) holds degrees. HVal::Real(r) if name == "DIMJOGANG" => HVal::Real((r.to_degrees() * 1e9).round() / 1e9), + // $ANGBASE is in degrees (a drawing measured from north has 90.0); the variable + // holds radians. + HVal::Real(r) if name == "ANGBASE" => HVal::Real(r.to_radians()), hv => hv, }; d.header.set(&name, hv); diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs index 3fdf32580..521654e3d 100644 --- a/crates/io/src/dxf_write.rs +++ b/crates/io/src/dxf_write.rs @@ -358,6 +358,8 @@ fn header_vars(w: &mut W, d: &Drawing) { (-2 | -3, _) => {} (c @ (40 | 50), v) => { if let Some(f) = v.as_f64() { + // ANGBASE is held in radians; `$ANGBASE` is in degrees. + let f = if *name == "ANGBASE" { f.to_degrees() } else { f }; w.s(9, format!("${name}")); w.f(c, f); } diff --git a/crates/io/tests/angle_header.rs b/crates/io/tests/angle_header.rs index 1b39f8203..e9b29da3c 100644 --- a/crates/io/tests/angle_header.rs +++ b/crates/io/tests/angle_header.rs @@ -1,5 +1,6 @@ -//! The angle settings round-trip through DXF: `$ANGBASE` (group 50) in radians, `$ANGDIR` and -//! `$AUNITS` (group 70) as integers. +//! The angle settings round-trip through DXF: `$ANGBASE` (group 50) in degrees, as AutoCAD writes +//! it (a drawing measured from north has 90.0), held in radians; `$ANGDIR` and `$AUNITS` +//! (group 70) as integers. use cadcraft_doc::{Drawing, HVal}; @@ -10,9 +11,23 @@ fn angbase_angdir_aunits_round_trip_through_dxf() { d.header.set_i64("ANGDIR", 1); d.header.set_i64("AUNITS", 3); let text = cadcraft_io::write_dxf(&d); + // Written in degrees. + let lines: Vec<&str> = text.lines().map(str::trim).collect(); + let at = lines.iter().position(|l| *l == "$ANGBASE").expect("$ANGBASE is written"); + let written: f64 = lines.get(at + 2).and_then(|v| v.parse().ok()).expect("a number"); + assert!((written - 90.0).abs() < 1e-9, "$ANGBASE written as {written}"); let r = cadcraft_io::read_dxf(text.as_bytes()).unwrap(); let base = r.header.f64("ANGBASE", 0.0); assert!((base - std::f64::consts::FRAC_PI_2).abs() < 1e-12, "$ANGBASE read back as {base}"); assert_eq!(r.header.get("ANGDIR"), Some(&HVal::Int(1))); assert_eq!(r.header.get("AUNITS"), Some(&HVal::Int(3))); } + +#[test] +fn angbase_in_degrees_is_read_as_radians() { + // What AutoCAD writes for a drawing whose angles run clockwise from north. + let text = "0\nSECTION\n2\nHEADER\n9\n$ANGBASE\n50\n90.0\n9\n$ANGDIR\n70\n1\n0\nENDSEC\n0\nEOF\n"; + let d = cadcraft_io::read_dxf(text.as_bytes()).unwrap(); + let base = d.header.f64("ANGBASE", 0.0); + assert!((base - std::f64::consts::FRAC_PI_2).abs() < 1e-12, "$ANGBASE 90.0 read as {base} radians"); +}