mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 00:42:46 -06:00
* fix(io): a viewport looks at its view target plus its centre (#2492) A DXF stores a view's centre 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). The readers took the centre alone, so a file that keeps the location in the target with a centre of 0,0 (as ArchiCAD's AutoCAD 2018 export does) showed model point 0,0 in every layout viewport, on screen and when plotted. The VIEWPORT entity, VPORT tiles and named views now read the target plus the centre. A sum that overflows keeps the stored centre. The writers already store the whole location as the centre with a target of 0, so a save and reopen keeps the view, through DXF and DWG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(io): a viewport looking along another direction centres on its target Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
austin62jake
parent
3146b47656
commit
a277346c2f
@@ -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<f64> {
|
||||
self.0.iter().filter(|t| t.code == code).map(Tag::f64).collect()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Viewport> {
|
||||
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<Vec2> = 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::<Vec<_>>(), 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:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user