diff --git a/crates/io/src/dxf_read/common.rs b/crates/io/src/dxf_read/common.rs index e46f3c09d..1dbabf6e9 100644 --- a/crates/io/src/dxf_read/common.rs +++ b/crates/io/src/dxf_read/common.rs @@ -19,6 +19,15 @@ impl T<'_> { pub(super) fn p(&self, code: i32) -> Vec3 { Vec3::new(self.fd(code, 0.0), self.fd(code + 10, 0.0), self.fd(code + 20, 0.0)) } + /// A view's centre in the drawing. The DXF stores it in display coordinates (`center`), whose + /// origin is the view target (`target`), so the point in the middle of a plan view is their + /// sum: a file whose view centre is 0,0 keeps the location in the target (#2492). A sum that + /// overflows leaves the stored centre. + pub(super) fn view_center(&self, center: i32, target: i32) -> Vec2 { + let c = self.p(center).xy(); + let sum = c + self.p(target).xy(); + if sum.is_finite() { sum } else { c } + } pub(super) fn all_f(&self, code: i32) -> Vec { self.0.iter().filter(|t| t.code == code).map(Tag::f64).collect() } diff --git a/crates/io/src/dxf_read/entities.rs b/crates/io/src/dxf_read/entities.rs index 119d00bbd..980b1718c 100644 --- a/crates/io/src/dxf_read/entities.rs +++ b/crates/io/src/dxf_read/entities.rs @@ -246,7 +246,8 @@ pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_lef center: t.p(10), width: t.fd(40, 1.0), height: t.fd(41, 1.0), - view_center: Vec2::new(t.fd(12, 0.0), t.fd(22, 0.0)), + // 12/22 is in display coordinates, whose origin is the view target 17/27/37. + view_center: t.view_center(12, 17), view_height: t.fd(45, 1.0), id: t.i(69).unwrap_or(0) as u32, locked: t.i(90).unwrap_or(0) & 16384 != 0, diff --git a/crates/io/src/dxf_read/tables.rs b/crates/io/src/dxf_read/tables.rs index ee42b8a58..c914ea9f5 100644 --- a/crates/io/src/dxf_read/tables.rs +++ b/crates/io/src/dxf_read/tables.rs @@ -217,7 +217,8 @@ pub(super) fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) { if name.is_empty() || !(h.is_finite() && w.is_finite() && h > 0.0 && w > 0.0) || d.views.len() >= MAX_OBJECTS { continue; } - let center = t.p(10).xy(); + // The view centre 10/20 is in display coordinates, whose origin is the target 12/22/32. + let center = t.view_center(10, 12); if !(center.x.is_finite() && center.y.is_finite()) { continue; } @@ -316,7 +317,8 @@ const MAX_VPORT_TILES: usize = 64; /// are fractions of the drawing area; unusable tiles are skipped. fn vport_record(d: &mut Drawing, t: &T) { let name = t.s(2).map(|n| n.trim().to_string()).unwrap_or_default(); - let (min, max, center) = (t.p(10).xy(), t.p(11).xy(), t.p(12).xy()); + // The view centre 12/22 is in display coordinates, whose origin is the view target 17/27/37. + let (min, max, center) = (t.p(10).xy(), t.p(11).xy(), t.view_center(12, 17)); let height = t.fd(40, 0.0); let unit = |v: f64| v.is_finite() && (-1e-9..=1.0 + 1e-9).contains(&v); let ok = [min.x, min.y, max.x, max.y].into_iter().all(unit) diff --git a/crates/io/tests/it/main.rs b/crates/io/tests/it/main.rs index 79c4e4b3e..aa77795fa 100644 --- a/crates/io/tests/it/main.rs +++ b/crates/io/tests/it/main.rs @@ -73,5 +73,6 @@ mod text_left_justify; mod tolerance_roundtrip; mod user_lin_pat_roundtrip; mod view_tables_roundtrip; +mod view_target; mod viewport_clip_roundtrip; mod xref_roundtrip; diff --git a/crates/io/tests/it/view_target.rs b/crates/io/tests/it/view_target.rs new file mode 100644 index 000000000..c6a2eea27 --- /dev/null +++ b/crates/io/tests/it/view_target.rs @@ -0,0 +1,181 @@ +//! A view's centre is stored in display coordinates, whose origin is the view target (DXF +//! Reference: VIEWPORT 12/22 and 17/27/37, VPORT 12/22 and 17/27/37, VIEW 10/20 and 12/22/32). +//! ArchiCAD's AutoCAD 2018 export puts each layout viewport's location in the target with a +//! centre of 0,0, so reading the centre alone showed model point 0,0 (#2492). + +use cadcraft_doc::geom::Vec2; +use cadcraft_doc::{Drawing, EntityKind, Viewport}; +use cadcraft_io::{read_dxf, write_dxf}; + +/// An ASCII DXF of `tags`. +fn dxf(tags: &[(i32, &str)]) -> String { + tags.iter().map(|(c, v)| format!("{c}\n{v}\n")).collect() +} + +/// A layout viewport with view centre `center` (12/22) and view target `target` (17/27/37). +fn with_viewport(center: [&str; 2], target: [&str; 3]) -> Drawing { + looking_along(center, target, ["0", "0", "1"]) +} + +/// The same, looking along view direction `dir` (16/26/36). +fn looking_along(center: [&str; 2], target: [&str; 3], dir: [&str; 3]) -> Drawing { + let tags: Vec<(i32, &str)> = vec![ + (0, "SECTION"), + (2, "ENTITIES"), + (0, "VIEWPORT"), + (5, "40"), + (67, "1"), + (8, "0"), + (10, "148.5"), + (20, "105"), + (30, "0"), + (40, "250"), + (41, "180"), + (68, "2"), + (69, "2"), + (12, center[0]), + (22, center[1]), + (16, dir[0]), + (26, dir[1]), + (36, dir[2]), + (17, target[0]), + (27, target[1]), + (37, target[2]), + (45, "48680.2"), + (0, "ENDSEC"), + (0, "EOF"), + ]; + read_dxf(dxf(&tags).as_bytes()).expect("read") +} + +fn viewports(d: &Drawing) -> Vec { + d.layouts + .iter() + .flat_map(|l| l.entities.iter()) + .filter_map(|e| match &e.kind { + EntityKind::Viewport(v) if v.id > 1 => Some(v.clone()), + _ => None, + }) + .collect() +} + +fn view_center(d: &Drawing) -> Vec2 { + let vs = viewports(d); + assert_eq!(vs.len(), 1, "{vs:?}"); + vs[0].view_center +} + +/// `a` and `b` agree to within the rounding of adding two decimals. +fn close(a: Vec2, b: Vec2) -> bool { + (a - b).len() < 1e-6 +} + +#[test] +fn a_viewport_looks_at_its_target_plus_its_centre() { + // The issue's file: centre 0,0, the facade in the target. + let c = view_center(&with_viewport(["0", "0"], ["230748.7", "-867.4", "0"])); + assert_eq!(c, Vec2::new(230748.7, -867.4)); + // Both set: their sum. A target alone of 0 leaves the centre as it is. + assert_eq!(view_center(&with_viewport(["10", "5"], ["100", "50", "7"])), Vec2::new(110.0, 55.0)); + assert_eq!(view_center(&with_viewport(["10", "5"], ["0", "0", "0"])), Vec2::new(10.0, 5.0)); +} + +/// CADCraft draws every viewport as a plan view (the view direction, 16/26/36, isn't applied). A +/// viewport looking along another direction is centred on what it looks at, its target, as +/// AutoCAD's view of it is; its centre is in that view's own axes, so only the target is exact. +#[test] +fn a_viewport_looking_along_another_direction_centres_on_its_target() { + let c = view_center(&looking_along(["0", "0"], ["500", "-200", "30"], ["1", "1", "1"])); + assert_eq!(c, Vec2::new(500.0, -200.0)); +} + +/// Saved and reopened, the viewport still looks at the same place: CADCraft writes the whole +/// location as the centre with a target of 0. +#[test] +fn the_view_survives_a_save_and_reopen() { + let d = with_viewport(["3", "4"], ["84672.2", "25851.5", "0"]); + let back = read_dxf(write_dxf(&d).as_bytes()).expect("reopen"); + let c = view_center(&back); + assert!(close(c, Vec2::new(84675.2, 25855.5)), "{c:?}"); +} + +/// A target coordinate that is not a finite number reads as 0, as any DXF real does, and a sum +/// that overflows leaves the centre. +#[test] +fn a_hostile_target_leaves_the_centre() { + for target in [["1e999", "0", "0"], ["nan", "0", "0"], ["1e308", "0", "0"]] { + assert_eq!(view_center(&with_viewport(["1e308", "2"], target)), Vec2::new(1e308, 2.0), "{target:?}"); + } + assert_eq!(view_center(&with_viewport(["1", "2"], ["inf", "5", "0"])), Vec2::new(1.0, 7.0)); +} + +/// The model viewport tiles of a saved configuration (VPORT 12/22 and 17/27/37) and named views +/// (VIEW 10/20 and 12/22/32) are read the same way. +#[test] +fn vport_tiles_and_named_views_add_their_targets() { + let tags: &[(i32, &str)] = &[ + (0, "SECTION"), + (2, "TABLES"), + (0, "TABLE"), + (2, "VPORT"), + (0, "VPORT"), + (2, "TWO"), + (70, "0"), + (10, "0"), + (20, "0"), + (11, "0.5"), + (21, "1"), + (12, "1"), + (22, "2"), + (17, "100"), + (27, "200"), + (37, "0"), + (40, "50"), + (41, "1"), + (0, "VPORT"), + (2, "TWO"), + (70, "0"), + (10, "0.5"), + (20, "0"), + (11, "1"), + (21, "1"), + (12, "0"), + (22, "0"), + (17, "-40"), + (27, "30"), + (37, "0"), + (40, "50"), + (41, "1"), + (0, "ENDTAB"), + (0, "TABLE"), + (2, "VIEW"), + (0, "VIEW"), + (2, "Facade"), + (70, "0"), + (40, "20"), + (41, "30"), + (10, "1"), + (20, "2"), + (11, "0"), + (21, "0"), + (31, "1"), + (12, "367097.3"), + (22, "-441.8"), + (32, "0"), + (0, "ENDTAB"), + (0, "ENDSEC"), + (0, "EOF"), + ]; + let d = read_dxf(dxf(tags).as_bytes()).expect("read"); + let two = d.vports.iter().find(|c| c.name.eq_ignore_ascii_case("TWO")).expect("configuration TWO"); + let centers: Vec = two.tiles.iter().map(|t| t.center).collect(); + assert_eq!(centers, [Vec2::new(101.0, 202.0), Vec2::new(-40.0, 30.0)]); + let facade = d.views.iter().find(|v| v.name == "Facade").expect("view Facade"); + assert!(close(facade.center, Vec2::new(367098.3, -439.8)), "{:?}", facade.center); + // And again after a save and reopen. + let back = read_dxf(write_dxf(&d).as_bytes()).expect("reopen"); + let two = back.vports.iter().find(|c| c.name.eq_ignore_ascii_case("TWO")).expect("configuration TWO"); + assert_eq!(two.tiles.iter().map(|t| t.center).collect::>(), centers); + let again = back.views.iter().find(|v| v.name == "Facade").map(|v| v.center).expect("view Facade"); + assert!(close(again, Vec2::new(367098.3, -439.8)), "{again:?}"); +}