Viewport twist: normalise with norm_angle on read and write; test the untwisted case on a real viewport

From CodeRabbit's review: a tiny negative angle no longer reads as a full turn, a huge finite
twist from JSON or MCP no longer writes an infinite group 51, and the "no group 51" check now
looks at a viewport that exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRnJvmr59vS3DS5p2E8FhG
This commit is contained in:
ATARYS-LAB
2026-11-03 18:32:40 +01:00
co-authored by Claude Opus 5.5
parent f7494840dc
commit c8e1671eb4
3 changed files with 20 additions and 7 deletions
+1 -1
View File
@@ -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);
+3 -2
View File
@@ -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.
+16 -4
View File
@@ -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::<f64>().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<String> = 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();