Files
cadcraft/crates/io/tests/tables_objects_roundtrip.rs
2026-10-11 19:53:33 -04:00

260 lines
8.8 KiB
Rust

//! DXF save → reopen keeps table and object-section data: complex linetype elements,
//! multileader styles, layout page setups, named views, UCSs and groups.
use cadcraft_doc::geom::{Vec2, Vec3};
use cadcraft_doc::{Common, DashElement, Drawing, EntityKind, Group, Line, Linetype, MLeaderStyle, NamedView, Space, TextStyle, Ucs, library};
use cadcraft_io::{read_dxf, write_dxf};
fn round_trip(d: &Drawing) -> Drawing {
read_dxf(write_dxf(d).as_bytes()).expect("reopen")
}
#[test]
fn linetype_text_and_shape_elements_round_trip() {
let mut d = Drawing::new_imperial();
let fence = library::standard_linetypes().into_iter().find(|l| l.name == "FENCELINE1").expect("FENCELINE1");
d.linetypes.push(fence.clone());
let gas = Linetype {
name: "GAS".into(),
description: "Gas line ----GAS----".into(),
pattern: vec![
DashElement::dash(0.5),
DashElement {
text: Some("GAS".into()),
style: Some("Standard".into()),
scale: 0.1,
rotation: 0.25,
offset: Vec2::new(-0.1, -0.05),
..DashElement::dash(-0.2)
},
],
};
// A shape from a shape file (not a text style), with an absolute rotation.
let shape = Linetype {
name: "BATTING".into(),
description: String::new(),
pattern: vec![
DashElement::dash(0.0001),
DashElement {
shape: Some(132),
style: Some("ltypeshp.shx".into()),
scale: 0.2,
rotation: 1.5,
absolute: true,
offset: Vec2::new(0.0, 0.1),
..DashElement::dash(-0.3)
},
],
};
d.linetypes.extend([gas.clone(), shape.clone()]);
let dxf = write_dxf(&d);
// The shape file is written as an unnamed shape style (STYLE flag 1).
assert!(dxf.contains("ltypeshp.shx"), "no shape style record");
let r = read_dxf(dxf.as_bytes()).expect("reopen");
for lt in [&fence, &gas, &shape] {
assert_eq!(r.linetype(&lt.name), Some(lt), "{}", lt.name);
}
// The shape style isn't a text style.
assert!(r.text_styles.iter().all(|s| !s.name.is_empty()));
}
#[test]
fn multileader_styles_and_current_style_round_trip() {
let mut d = Drawing::new_imperial();
d.text_styles.push(TextStyle { name: "Slanted".into(), oblique: 0.2, ..TextStyle::default() });
let callout = MLeaderStyle {
name: "Callout".into(),
arrow_size: 2.0,
text_height: 3.0,
landing_gap: 1.0,
dogleg: 5.0,
text_style: "Slanted".into(),
annotative: false,
};
d.mleader_styles.push(callout);
d.header.set_str("CMLEADERSTYLE", "Callout");
let text = write_dxf(&d);
let tags = cadcraft_dxf::parse(text.as_bytes()).expect("parse written DXF");
let header = cadcraft_dxf::sections(&tags).into_iter().find(|s| s.name == "HEADER").expect("HEADER");
assert!(header.tags.iter().all(|t| t.code != 9 || t.str() != "$CMLEADERSTYLE"));
let r = read_dxf(text.as_bytes()).expect("reopen");
assert_eq!(r.mleader_styles, d.mleader_styles);
assert_eq!(r.header.str("CMLEADERSTYLE", ""), "Callout");
}
#[test]
fn layout_page_setup_and_paper_view_round_trip() {
let mut d = Drawing::new_imperial();
let first = d.layouts.get_mut(0).expect("Layout1");
first.page.scale = 0.5;
first.page.plot_style_table = "monochrome.ctb".into();
first.page.center = true;
first.page.plot_area = "extents".into();
first.view = Some((Vec2::new(5.0, 4.0), 12.0));
let second = d.layouts.get_mut(1).expect("Layout2");
second.page.scale_to_fit = true;
second.page.scale = 0.25;
second.page.lineweights = false;
second.page.plot_area = "window".into();
second.page.device = "DWG To PDF.pc3".into();
let r = round_trip(&d);
for (a, b) in d.layouts.iter().zip(&r.layouts) {
assert_eq!(a.name, b.name);
assert_eq!(a.page, b.page, "{}", a.name);
assert_eq!(a.view, b.view, "{}", a.name);
}
assert_eq!(r.layouts.len(), d.layouts.len());
}
#[test]
fn named_views_ucss_and_groups_round_trip() {
let mut d = Drawing::new_imperial();
let line = d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(10.0, 0.0, 0.0) })).expect("line");
d.views.push(NamedView {
name: "Front".into(),
center: Vec2::new(5.0, 5.0),
height: 20.0,
width: 30.0,
layer_state: Some("Plan".into()),
..Default::default()
});
d.ucss.push(Ucs { name: "Tilted".into(), origin: Vec3::new(1.0, 2.0, 0.0), x_axis: Vec3::new(0.6, 0.8, 0.0), y_axis: Vec3::new(-0.8, 0.6, 0.0) });
d.groups.push(Group { name: "G1".into(), description: "grp".into(), selectable: false, members: vec![line] });
// A member that no longer exists is not written.
let gone = d.new_handle();
d.groups.push(Group { name: "*A1".into(), description: String::new(), selectable: true, members: vec![line, gone] });
let r = round_trip(&d);
assert_eq!(r.views, d.views);
assert_eq!(r.ucss, d.ucss);
assert_eq!(r.groups.len(), 2);
assert_eq!(r.groups.first(), d.groups.first());
let anon = r.groups.get(1).expect("*A1");
assert_eq!((anon.name.as_str(), anon.members.as_slice()), ("*A1", [line].as_slice()));
}
/// A minimal file in the shape other programs write: an unnamed shape style, a VIEW, a UCS and a
/// GROUP in ACAD_GROUP. Hostile values (NaN, out-of-range shape numbers, missing groups) are
/// skipped without panicking.
#[test]
fn foreign_and_hostile_tables_objects() {
let tags: &[(i32, &str)] = &[
(0, "SECTION"),
(2, "TABLES"),
(0, "TABLE"),
(2, "STYLE"),
(0, "STYLE"),
(5, "11"),
(2, ""),
(70, "1"),
(3, "shapes.shx"),
(0, "ENDTAB"),
(0, "TABLE"),
(2, "LTYPE"),
(0, "LTYPE"),
(5, "12"),
(2, "ODD"),
(73, "3"),
(49, "0.5"),
(74, "4"),
(75, "70000"),
(340, "11"),
(46, "nan"),
(49, "-0.25"),
(74, "2"),
(340, "FFFF"),
(9, "X"),
(45, "1e400"),
(0, "ENDTAB"),
(0, "TABLE"),
(2, "VIEW"),
(0, "VIEW"),
(2, "Top"),
(40, "10"),
(41, "16"),
(10, "3"),
(20, "4"),
(0, "VIEW"),
(2, "Bad"),
(40, "-1"),
(41, "nan"),
(0, "ENDTAB"),
(0, "TABLE"),
(2, "UCS"),
(0, "UCS"),
(2, "Side"),
(10, "1"),
(20, "2"),
(30, "3"),
(11, "0"),
(21, "1"),
(31, "0"),
(12, "0"),
(22, "0"),
(32, "1"),
(0, "ENDTAB"),
(0, "ENDSEC"),
(0, "SECTION"),
(2, "ENTITIES"),
(0, "LINE"),
(5, "2A"),
(8, "0"),
(10, "0"),
(20, "0"),
(11, "1"),
(21, "1"),
(0, "ENDSEC"),
(0, "SECTION"),
(2, "OBJECTS"),
(0, "DICTIONARY"),
(5, "C"),
(3, "ACAD_GROUP"),
(350, "D"),
(3, "ACAD_MLEADERSTYLE"),
(350, "E"),
(0, "DICTIONARY"),
(5, "D"),
(3, "Doors"),
(350, "20"),
(0, "GROUP"),
(5, "20"),
(330, "D"),
(300, "the doors"),
(70, "0"),
(71, "1"),
(340, "2A"),
(340, "zz"),
(340, "99"),
(0, "DICTIONARY"),
(5, "E"),
(3, "Odd"),
(350, "21"),
(0, "MLEADERSTYLE"),
(5, "21"),
(42, "-2"),
(43, "nan"),
(45, "-3"),
(342, "404"),
(0, "ENDSEC"),
(0, "EOF"),
];
let dxf: String = tags.iter().map(|(c, v)| format!("{c}\n{v}\n")).collect();
let d = read_dxf(dxf.as_bytes()).expect("read");
let odd = d.linetype("ODD").expect("ODD");
let first = odd.pattern.first().expect("shape element");
assert_eq!((first.shape, first.style.as_deref()), (Some(0), Some("shapes.shx")));
assert!(first.scale.is_finite());
let second = odd.pattern.get(1).expect("text element");
assert_eq!((second.text.as_deref(), second.style.as_deref()), (Some("X"), None));
assert_eq!(d.views.iter().map(|v| v.name.as_str()).collect::<Vec<_>>(), ["Top"]);
assert_eq!(d.ucss.first().map(|u| u.origin), Some(Vec3::new(1.0, 2.0, 3.0)));
let g = d.groups.first().expect("group");
assert_eq!((g.name.as_str(), g.description.as_str(), g.members.len()), ("Doors", "the doors", 1));
let odd_style = d.mleader_styles.iter().find(|s| s.name == "Odd").expect("mleader style");
// Unusable values fall back to the defaults (NaN reads as 0).
assert_eq!(odd_style, &MLeaderStyle { name: "Odd".into(), dogleg: 0.0, ..MLeaderStyle::default() });
// Saving it again keeps everything readable.
let again = round_trip(&d);
assert_eq!(again.groups, d.groups);
assert_eq!(again.views, d.views);
}