diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs index d190277..8687193 100644 --- a/crates/dwg/src/lib.rs +++ b/crates/dwg/src/lib.rs @@ -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, h: &acadrust::document::HeaderVariables) -> Vec { + 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::().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`. diff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs index 6b7a354..1fd444b 100644 --- a/crates/io/src/dxf_read.rs +++ b/crates/io/src/dxf_read.rs @@ -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)); diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs index e6ee22c..35a6a79 100644 --- a/crates/io/src/dxf_write.rs +++ b/crates/io/src/dxf_write.rs @@ -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. diff --git a/crates/io/tests/dwg_header_vars.rs b/crates/io/tests/dwg_header_vars.rs new file mode 100644 index 0000000..178a5d2 --- /dev/null +++ b/crates/io/tests/dwg_header_vars.rs @@ -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); +}