DWG and DXF keep ANGBASE/ANGDIR, fillet/chamfer, elevation and the snap grid (fixes #349) (#350)

acadrust's DXF writer leaves ANGBASE, ANGDIR, FILLETRAD, CHAMFERA/B,
ELEVATION and THICKNESS out of the HEADER although its DWG reader fills
them in; the DWG bridge now adds them to the DXF it hands our reader.
SNAPBASE/SNAPANG are written to and read from the *ACTIVE VPORT record
(groups 13/23 and 50), where R13+ files keep them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein 2026-10-10 23:32:07 +02:00 committed by GitHub
parent 7bed82838d
commit 937dca6590
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 116 additions and 2 deletions

View File

@ -79,7 +79,43 @@ mod native {
));
}
written.map_err(|e| format!("DWG→DXF: {e}"))?;
Ok(out.buf)
Ok(add_header_vars(out.buf, &doc.header))
}
/// Header variables the DWG reader fills in but acadrust's DXF writer (0.6.3) leaves out of
/// 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()),
("$ANGDIR", 70, h.angle_direction.to_string()),
("$FILLETRAD", 40, h.fillet_radius.to_string()),
("$CHAMFERA", 40, h.chamfer_distance_a.to_string()),
("$CHAMFERB", 40, h.chamfer_distance_b.to_string()),
("$ELEVATION", 40, h.elevation.to_string()),
("$THICKNESS", 40, h.thickness.to_string()),
]
}
/// Add the [`unwritten_header_vars`] to the HEADER section of acadrust's DXF text, before its
/// ENDSEC; a variable the writer already wrote is left alone. Text of another layout than
/// acadrust's (` 0\r\nSECTION\r\n 2\r\nHEADER\r\n` … ` 0\r\nENDSEC\r\n`) is returned unchanged.
fn add_header_vars(mut dxf: Vec<u8>, h: &acadrust::document::HeaderVariables) -> Vec<u8> {
const START: &[u8] = b" 0\r\nSECTION\r\n 2\r\nHEADER\r\n";
const END: &[u8] = b"\r\n 0\r\nENDSEC\r\n";
if !dxf.starts_with(START) {
return dxf;
}
let Some(end) = dxf.windows(END.len()).position(|w| w == END).map(|p| p + 2) else { return dxf };
let section = dxf.get(..end).unwrap_or_default();
let mut add = String::new();
for (name, code, value) in unwritten_header_vars(h) {
let written = section.windows(name.len() + 2).any(|w| w.starts_with(name.as_bytes()) && w.ends_with(b"\r\n"));
if !written && value.parse::<f64>().is_ok_and(f64::is_finite) {
add.push_str(&format!(" 9\r\n{name}\r\n{code:>3}\r\n{value}\r\n"));
}
}
dxf.splice(end..end, add.into_bytes());
dxf
}
/// Collects the DXF text and fails the write once it would grow past `limit`.

View File

@ -1076,6 +1076,7 @@ fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
_ => {}
}
}
let mut active_vport = false;
for (kind, tg) in recs {
let t = T(&tg);
match kind.as_str() {
@ -1145,6 +1146,19 @@ fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
None => d.text_styles.push(st),
}
}
// The first *ACTIVE viewport holds the snap grid's origin and rotation (degrees).
"VPORT" if !active_vport && t.s(2).is_some_and(|n| n.trim().eq_ignore_ascii_case("*ACTIVE")) => {
active_vport = true;
if let (Some(x), Some(y)) = (t.f(13), t.f(23))
&& x.is_finite()
&& y.is_finite()
{
d.header.set("SNAPBASE", HVal::Point(Vec3::new(x, y, 0.0)));
}
if let Some(a) = t.f(50).filter(|a| a.is_finite()) {
d.header.set_f64("SNAPANG", a);
}
}
"VIEW" => {
let name = t.s(2).unwrap_or_default();
let (h, w) = (t.fd(40, 0.0), t.fd(41, 0.0));

View File

@ -1367,7 +1367,8 @@ pub fn write(d: &Drawing) -> String {
w.p2(10, Vec2::ZERO);
w.p2(11, Vec2::new(1.0, 1.0));
w.p2(12, if ext.is_empty() { Vec2::new(6.0, 4.5) } else { ext.center() });
w.p2(13, Vec2::ZERO);
// Snap grid origin and rotation (SNAPBASE, SNAPANG in degrees): R13+ files keep them here.
w.p2(13, d.header.point("SNAPBASE").map(|p| p.xy()).filter(|p| p.is_finite()).unwrap_or(Vec2::ZERO));
w.p2(14, Vec2::new(0.5, 0.5));
w.p2(15, Vec2::new(0.5, 0.5));
w.p(16, Vec3::Z);
@ -1375,6 +1376,7 @@ pub fn write(d: &Drawing) -> String {
w.f(40, if ext.is_empty() { 9.0 } else { (ext.height() * 1.1).max(1e-6) });
w.f(41, 1.6);
w.f(42, 50.0);
w.f(50, d.header.f64("SNAPANG", 0.0));
w.s(0, "ENDTAB");
// LTYPE. Embedded text and shapes point at STYLE records (340), which get their handles
// in the STYLE table below: (tag position, style name, is a shape) are filled in there.

View File

@ -0,0 +1,62 @@
//! Unit, angle and drafting settings in the drawing header survive saving as DXF or DWG and
//! reopening. The DWG bridge converts with acadrust, whose DXF writer leaves some header
//! variables out; the snap grid's origin and rotation live in the `*ACTIVE` VPORT record.
#![cfg(not(target_arch = "wasm32"))]
use cadcraft_doc::{Drawing, HVal};
use cadcraft_geom::Vec3;
fn drawing() -> Drawing {
let mut d = Drawing::new_metric();
let h = &mut d.header;
h.set_i64("AUNITS", 3);
h.set_i64("AUPREC", 5);
h.set_i64("LUNITS", 4);
h.set_i64("LUPREC", 6);
h.set_f64("ANGBASE", 0.5);
h.set_i64("ANGDIR", 1);
h.set_i64("INSUNITS", 6);
h.set_i64("MEASUREMENT", 0);
h.set_f64("LTSCALE", 2.5);
h.set_f64("DIMSCALE", 4.0);
h.set_i64("PDMODE", 34);
h.set_f64("PDSIZE", 1.5);
h.set_f64("FILLETRAD", 2.0);
h.set_f64("CHAMFERA", 1.25);
h.set_f64("CHAMFERB", 0.75);
h.set_f64("ELEVATION", 3.0);
h.set_f64("THICKNESS", 0.25);
h.set("SNAPBASE", HVal::Point(Vec3::new(3.0, 4.0, 0.0)));
h.set_f64("SNAPANG", 30.0);
d
}
fn check(name: &str, r: &Drawing) {
let d = drawing();
let ints = ["AUNITS", "AUPREC", "LUNITS", "LUPREC", "ANGDIR", "INSUNITS", "MEASUREMENT", "PDMODE"];
for v in ints {
assert_eq!(r.header.i64(v, -99), d.header.i64(v, -99), "{name}: ${v}");
}
let reals = ["ANGBASE", "LTSCALE", "DIMSCALE", "PDSIZE", "FILLETRAD", "CHAMFERA", "CHAMFERB", "ELEVATION", "THICKNESS", "SNAPANG"];
for v in reals {
let (got, want) = (r.header.f64(v, -99.0), d.header.f64(v, -99.0));
assert!((got - want).abs() < 1e-9, "{name}: ${v} read back as {got}, not {want}");
}
let base = r.header.point("SNAPBASE").unwrap_or(Vec3::new(-99.0, -99.0, 0.0));
assert!((base.x - 3.0).abs() < 1e-9 && (base.y - 4.0).abs() < 1e-9, "{name}: $SNAPBASE read back as {base:?}");
}
#[test]
fn header_settings_round_trip_through_dxf() {
let d = drawing();
let r = cadcraft_io::read_dxf(cadcraft_io::write_dxf(&d).as_bytes()).unwrap();
check("DXF", &r);
}
#[test]
fn header_settings_round_trip_through_dwg() {
let d = drawing();
let dwg = cadcraft_io::write(&d, "x.dwg").unwrap();
let r = cadcraft_io::read(&dwg, "x.dwg").unwrap();
check("DWG", &r);
}