diff --git a/crates/io/src/dxf_read/entities.rs b/crates/io/src/dxf_read/entities.rs index b442e2434..1930368f9 100644 --- a/crates/io/src/dxf_read/entities.rs +++ b/crates/io/src/dxf_read/entities.rs @@ -257,7 +257,7 @@ pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_lef // Status flags: 65536 = non-rectangular clipping (by the 340 entity), 131072 = off. clip: t.s(340).and_then(|h| Handle::parse_hex(&h)).filter(|h| h.0 != 0 && t.i(90).unwrap_or(0) & 65536 != 0), on: t.i(90).unwrap_or(0) & 131072 == 0, - twist: t.f(51).filter(|a| a.is_finite()).map_or(0.0, |a| a.rem_euclid(360.0).to_radians()), + twist: t.f(51).map_or(0.0, |a| cadcraft_geom::norm_angle(a.to_radians())), }), "WIPEOUT" => { let o = t.p(10); diff --git a/crates/io/src/dxf_write/entity.rs b/crates/io/src/dxf_write/entity.rs index 1b86741ef..452535794 100644 --- a/crates/io/src/dxf_write/entity.rs +++ b/crates/io/src/dxf_write/entity.rs @@ -686,8 +686,9 @@ fn write_viewport_entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: w.i(69, i64::from(v.id)); w.p2(12, v.view_center); w.f(45, v.view_height); - if v.twist.is_finite() && v.twist != 0.0 { - w.f(51, v.twist.to_degrees()); + let twist = cadcraft_geom::norm_angle(v.twist); + if twist != 0.0 { + w.f(51, twist.to_degrees()); } // Status flags: 16384 = display locked, 32768 = always set, 65536 = clipped by the // 340 entity, 131072 = off. diff --git a/crates/io/tests/it/dwg_roundtrip_losses.rs b/crates/io/tests/it/dwg_roundtrip_losses.rs index 0cf3d4553..d0577ef91 100644 --- a/crates/io/tests/it/dwg_roundtrip_losses.rs +++ b/crates/io/tests/it/dwg_roundtrip_losses.rs @@ -218,12 +218,24 @@ fn viewport_twist_survives_dxf_and_dwg() { assert!((twist_of(&cadcraft_io::read(&dxf, "x.dxf").unwrap()) - twist).abs() < 1e-9); assert!((twist_of(&through_dwg(&d)) - twist).abs() < 1e-9); - // An untwisted viewport writes no group 51. - let plain = records(&String::from_utf8_lossy(&cadcraft_io::write(&Drawing::new_metric(), "x.dxf").unwrap()), "VIEWPORT"); - assert!(plain.iter().all(|r| group(r, "51").is_none())); + // An untwisted viewport writes no group 51; a hostile twist (from JSON or MCP) writes a finite + // angle within one turn, or none. + for (value, want) in [(0.0, None), (f64::NAN, None), (f64::INFINITY, None), (1e300, Some(()))] { + let mut d = Drawing::new_metric(); + let EntityKind::Viewport(mut vp) = viewport(2) else { unreachable!() }; + vp.twist = value; + let h = d.add(&space, Common::default(), EntityKind::Viewport(vp)).unwrap(); + let text = String::from_utf8_lossy(&cadcraft_io::write(&d, "x.dxf").unwrap()).to_string(); + let rec = records(&text, "VIEWPORT").into_iter().find(|r| group(r, "5") == Some(h.hex().as_str())).unwrap(); + let written = group(&rec, "51").map(|v| v.parse::().unwrap()); + assert_eq!(written.map(|_| ()), want, "{value}: {rec:?}"); + assert!(written.is_none_or(|a| a.is_finite() && (0.0..360.0).contains(&a)), "{value}: {written:?}"); + } // Hostile angles: not a number reads as 0, a huge one is brought into one turn. - for (value, want) in [("nan", Some(0.0)), ("1e400", Some(0.0)), ("-330", Some(30f64.to_radians())), ("7200000000000000000030", None)] { + for (value, want) in + [("nan", Some(0.0)), ("1e400", Some(0.0)), ("-1e-20", Some(0.0)), ("-330", Some(30f64.to_radians())), ("7200000000000000000030", None)] + { let mut lines: Vec = text.lines().map(str::to_string).collect(); let at = lines.windows(2).position(|w| w[0].trim() == "51" && Some(w[1].trim()) == group(&rec, "51")).unwrap(); lines[at + 1] = value.to_string();