Files
craft-libs/crates/dxf/tests/model_write.rs
T

701 lines
28 KiB
Rust

//! Writing the model: what `Drawing::tags` writes reads back as what was given.
use std::collections::HashSet;
use craft_dxf::model::text::{MText, MTextColumns};
use craft_dxf::model::write::{BlockDef, Drawing, Format, entity_tags, fixed};
use craft_dxf::model::{
Attrib, Color, Common, DashElement, Dimension, Document, Entity, EntityKind, Gradient, Hatch, HatchEdge, HatchLoop, HatchPath, Helix, Image,
Insert, Layer, Leader, Linetype, Lineweight, LwPolyline, LwVertex, MLine, MLineVertex, PatternLine, Polyline, Spline, Text, TextStyle,
Transparency, Vertex, Viewport, records,
};
use craft_dxf::{Tag, parse, write_ascii_as};
fn common(layer: &str) -> Common {
Common {
handle: None,
owner: None,
layer: layer.into(),
linetype: None,
color: Color::ByLayer,
lineweight: Lineweight::ByLayer,
ltscale: 1.0,
visible: true,
paper_space: false,
transparency: Transparency::ByLayer,
thickness: 0.0,
extrusion: [0.0, 0.0, 1.0],
plot_style: None,
}
}
fn text(value: &str) -> Text {
Text {
insert: [1.0, 2.0, 0.0],
align: Some([3.0, 2.0, 0.0]),
height: 2.5,
value: value.into(),
rotation: 30.0,
width_factor: 0.8,
oblique: 15.0,
style: "Notes".into(),
generation: 2,
halign: 1,
valign: 2,
}
}
fn mtext(columns: Option<MTextColumns>) -> MText {
MText {
insert: [1.0, 2.0, 0.0],
height: 2.5,
width: 40.0,
attach: 5,
drawing_direction: 1,
direction: None,
rotation: 0.0,
style: "Notes".into(),
contents: format!("tab\there\nnext ^ line {}", "long ".repeat(120)),
line_spacing: 1.2,
line_spacing_exact: true,
background: Some((1, Color::Index(4), 1.25)),
columns,
}
}
fn hatch(fit: bool) -> Hatch {
let spline = HatchEdge::Spline {
degree: 1,
rational: true,
periodic: false,
knots: vec![0.0, 0.0, 1.0, 1.0],
control: vec![[0.0, 3.0], [0.0, 0.0]],
weights: vec![1.0, 2.0],
fit: if fit { vec![[0.0, 3.0], [0.0, 0.0]] } else { Vec::new() },
start_tangent: fit.then_some([0.0, -1.0]),
end_tangent: None,
};
let edges = vec![
HatchEdge::Line { start: [0.0, 0.0], end: [4.0, 0.0] },
HatchEdge::Arc { center: [4.0, 1.0], radius: 1.0, start_angle: 270.0, end_angle: 360.0, ccw: true },
HatchEdge::Ellipse { center: [2.0, 2.0], major: [2.0, 0.0], ratio: 0.5, start_angle: 0.0, end_angle: 180.0, ccw: false },
spline,
];
Hatch {
pattern: "ANSI31".into(),
solid: false,
associative: true,
style: 1,
pattern_type: 1,
angle: 45.0,
scale: 2.0,
double: false,
elevation: 2.5,
loops: vec![
HatchLoop { flags: 1, path: HatchPath::Edges(edges), sources: vec![0x91, 0x92] },
HatchLoop {
flags: 2,
path: HatchPath::Polyline { closed: true, vertices: vec![[1.0, 1.0, 0.5], [2.0, 1.0, 0.0], [2.0, 2.0, 0.0]] },
sources: vec![],
},
],
pattern_lines: vec![
PatternLine { angle: 45.0, base: [0.0, 0.0], offset: [-1.4, 1.4], dashes: vec![] },
PatternLine { angle: 135.0, base: [1.0, 2.0], offset: [1.0, 1.0], dashes: vec![3.0, -1.5] },
],
seeds: vec![[1.0, 1.0]],
gradient: None,
}
}
/// One of every kind the writer has, with values that are not the defaults. `full` adds what
/// only the newest versions hold.
fn sample(full: bool) -> Vec<Entity> {
let e = |kind: EntityKind| Entity { common: common("Walls"), kind };
let p = |x: f64| [x, x + 1.0, x + 2.0];
let polyline =
|flags: i64, vertices: Vec<Vertex>| Polyline { flags, elevation: 0.0, start_width: 0.0, end_width: 0.0, m: 0, n: 0, smooth: 0, vertices };
let vertex = |x: f64, flags: i64, face: [i64; 4]| Vertex { location: [x, 1.0, 0.0], bulge: 0.0, start_width: 0.0, end_width: 0.0, flags, face };
let attrib = Attrib {
tag: "A".into(),
text: text("one"),
prompt: String::new(),
flags: 1,
lock_position: true,
multiline: full.then(|| MText { background: None, ..mtext(None) }),
};
let attdef = Attrib { prompt: "Enter\ta".into(), flags: 12, multiline: None, ..attrib.clone() };
let dim = |flags: i64| Dimension {
flags,
block: Some("*D1".into()),
style: "ISO-25".into(),
defpt: p(1.0),
text_mid: p(2.0),
insertion: [0.0; 3],
p13: [0.0; 3],
p14: [0.0; 3],
p15: [0.0; 3],
p16: [0.0; 3],
text: "<> ^ m".into(),
measurement: Some(12.5),
rotation: 0.0,
oblique: 0.0,
text_rotation: 45.0,
horizontal: 10.0,
leader_length: 0.0,
};
let image = Image {
insert: p(1.0),
u: [0.5, 0.0, 0.0],
v: [0.0, 0.5, 0.0],
size: [640.0, 480.0],
image_def: Some(0x77),
display: 7,
clipping: true,
brightness: 60,
contrast: 40,
fade: 10,
clip_type: 2,
clip: vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]],
clip_inverted: full,
};
let mut styled = e(EntityKind::Line { start: p(1.0), end: p(4.0) });
styled.common = Common {
handle: Some(0x500),
linetype: Some("DASHED".into()),
color: Color::Rgb([255, 127, 0]),
lineweight: Lineweight::Hundredths(50),
ltscale: 2.5,
visible: false,
paper_space: true,
transparency: Transparency::Alpha(128),
thickness: 1.5,
extrusion: [0.0, 0.0, -1.0],
plot_style: Some(fixed::PLOT_NORMAL),
..common("Walls")
};
let columns = MTextColumns {
dynamic: true,
count: 3,
width: 30.0,
gutter: 5.0,
height: 40.0,
auto_height: false,
heights: vec![10.0, 40.0, 20.0],
flow_reversed: true,
};
let fixed_columns =
MTextColumns { dynamic: false, count: 2, width: 30.0, gutter: 5.0, height: 80.0, auto_height: false, heights: vec![], flow_reversed: false };
let gradient =
Gradient { name: "LINEAR".into(), angle: 0.5, shift: 1.0, single: false, tint: 0.25, colors: vec![Color::Rgb([0, 0, 255]), Color::Index(2)] };
vec![
styled,
e(EntityKind::Point { location: p(1.0), angle: 30.0 }),
e(EntityKind::Circle { center: p(1.0), radius: 3.0 }),
e(EntityKind::Arc { center: p(1.0), radius: 3.0, start_angle: 10.0, end_angle: 200.0 }),
e(EntityKind::Ellipse { center: p(1.0), major: [2.0, 0.0, 0.0], ratio: 0.5, start: 0.25, end: 3.0 }),
e(EntityKind::LwPolyline(LwPolyline {
vertices: vec![
LwVertex { x: 0.0, y: 0.0, bulge: 1.0, start_width: 0.0, end_width: 0.0 },
LwVertex { x: 4.0, y: 0.0, bulge: 0.0, start_width: 0.1, end_width: 0.2 },
LwVertex { x: 4.0, y: 3.0, ..LwVertex::default() },
],
closed: true,
plinegen: true,
const_width: 0.5,
elevation: 2.0,
})),
e(EntityKind::Polyline(Polyline {
start_width: 0.5,
end_width: 0.25,
smooth: 6,
..polyline(1 | 4, vec![Vertex { bulge: 0.5, start_width: 0.1, end_width: 0.2, ..vertex(0.0, 16, [0; 4]) }, vertex(1.0, 0, [0; 4])])
})),
e(EntityKind::Polyline(polyline(8, vec![vertex(0.0, 32, [0; 4]), vertex(1.0, 32, [0; 4])]))),
e(EntityKind::Polyline(Polyline {
m: 3,
n: 1,
..polyline(
64,
vec![
vertex(0.0, 192, [0; 4]),
vertex(1.0, 192, [0; 4]),
vertex(2.0, 192, [0; 4]),
Vertex { location: [0.0; 3], ..vertex(0.0, 128, [1, 2, -3, 0]) },
],
)
})),
e(EntityKind::Spline(Spline {
degree: 2,
flags: 8,
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
control: vec![p(0.0), p(1.0), p(2.0)],
weights: vec![1.0, 2.0, 1.0],
fit: vec![p(0.0), p(2.0)],
start_tangent: Some([1.0, 0.0, 0.0]),
end_tangent: Some([0.0, 1.0, 0.0]),
fit_tolerance: 0.01,
})),
e(EntityKind::Helix(Helix {
spline: Spline {
degree: 3,
flags: 0,
knots: vec![0.0, 1.0],
control: vec![p(0.0), p(1.0)],
weights: vec![],
fit: vec![],
start_tangent: None,
end_tangent: None,
fit_tolerance: 0.0,
},
axis_base: p(1.0),
start: p(2.0),
axis: [0.0, 0.0, 1.0],
radius: 2.0,
turns: 3.0,
turn_height: 1.5,
ccw: false,
constrain: 1,
})),
e(EntityKind::MLine(MLine {
style: "WALL".into(),
style_handle: Some(0x41),
scale: 2.0,
justification: 1,
flags: 1 | 4,
elements: 2,
start: p(0.0),
vertices: vec![
MLineVertex {
location: p(0.0),
direction: [1.0, 0.0, 0.0],
miter: [0.0, 1.0, 0.0],
element_params: vec![vec![0.5, 1.0, 2.0], vec![-0.5]],
fill_params: vec![vec![], vec![]],
},
MLineVertex {
location: p(4.0),
direction: [1.0, 0.0, 0.0],
miter: [0.0, 1.0, 0.0],
element_params: vec![vec![0.5], vec![-0.5]],
fill_params: vec![vec![], vec![]],
},
],
})),
e(EntityKind::Ray { base: p(1.0), direction: [0.0, 1.0, 0.0] }),
e(EntityKind::XLine { base: p(1.0), direction: [1.0, 0.0, 0.0] }),
e(EntityKind::Text(text("10^2\tm"))),
e(EntityKind::MText(mtext(None))),
e(EntityKind::MText(MText { direction: Some([0.0, 1.0, 0.0]), background: None, ..mtext(Some(columns)) })),
e(EntityKind::MText(MText { background: None, ..mtext(Some(fixed_columns)) })),
e(EntityKind::AttDef(attdef)),
e(EntityKind::Insert(Insert {
block: "DOOR".into(),
insert: p(1.0),
scale: [2.0, 1.0, -1.0],
rotation: 45.0,
columns: 3,
rows: 2,
column_spacing: 5.0,
row_spacing: 6.0,
attribs: vec![(Common { color: Color::Index(2), handle: Some(0x501), ..common("Tags") }, attrib)],
})),
e(EntityKind::Insert(Insert {
block: "DOOR".into(),
insert: p(1.0),
scale: [1.0; 3],
rotation: 0.0,
columns: 1,
rows: 1,
column_spacing: 0.0,
row_spacing: 0.0,
attribs: vec![],
})),
e(EntityKind::Dimension(Dimension { p13: p(3.0), p14: p(4.0), rotation: 30.0, oblique: 60.0, ..dim(128) })),
e(EntityKind::Dimension(Dimension { p13: p(3.0), p14: p(4.0), ..dim(1) })),
e(EntityKind::Dimension(Dimension { p13: p(3.0), p14: p(4.0), p15: p(5.0), p16: p(6.0), ..dim(2) })),
e(EntityKind::Dimension(Dimension { p15: p(5.0), leader_length: 2.0, ..dim(3) })),
e(EntityKind::Dimension(Dimension { p15: p(5.0), leader_length: 2.0, ..dim(4) })),
e(EntityKind::Dimension(Dimension { p13: p(3.0), p14: p(4.0), p15: p(5.0), ..dim(5) })),
e(EntityKind::Dimension(Dimension { p13: p(3.0), p14: p(4.0), ..dim(6 | 64) })),
e(EntityKind::Leader(Leader {
vertices: vec![p(1.0), [4.0, 5.0, 0.0], p(6.0)],
arrow: false,
spline: true,
style: "Arrows".into(),
annotation_type: 0,
annotation: Some(0x4F),
})),
e(EntityKind::Solid { corners: [p(0.0), p(1.0), p(2.0), p(3.0)] }),
e(EntityKind::Trace { corners: [p(0.0), p(1.0), p(2.0), p(3.0)] }),
e(EntityKind::Face3d { corners: [p(0.0), p(1.0), p(2.0), p(3.0)], hidden_edges: 5 }),
e(EntityKind::Hatch(Hatch { gradient: None, ..hatch(full) })),
e(EntityKind::Hatch(Hatch {
pattern: "SOLID".into(),
solid: true,
angle: 0.0,
scale: 1.0,
pattern_lines: vec![],
gradient: Some(gradient),
..hatch(full)
})),
e(EntityKind::Viewport(Viewport {
center: p(5.0),
width: 8.0,
height: 6.0,
id: 2,
status: 16384,
view_center: [1.0, 2.0],
view_direction: [0.0, 0.0, 1.0],
view_target: p(9.0),
view_height: 30.0,
twist: 90.0,
clip: Some(0xA1),
frozen_layers: vec![0xB1, 0xB2],
})),
e(EntityKind::Image(image.clone())),
e(EntityKind::Wipeout(Image { image_def: None, ..image })),
e(EntityKind::Tolerance { insert: p(1.0), direction: [0.0, 1.0, 0.0], style: "Tol".into(), text: "{\\Fgdt;j}%%v0.1".into() }),
]
}
/// The file as text and back: what a reader of the written file sees.
fn through_text(tags: &[Tag], format: Format) -> Vec<Tag> {
parse(write_ascii_as(tags, format.utf8()).as_bytes()).expect("the written file parses")
}
fn read_back(tags: &[Tag]) -> Vec<Entity> {
Document::read(tags).entities.iter().map(Entity::read).collect()
}
/// What the writer decides: the handle of an entity that had none, and every owner.
fn settled(mut e: Entity, given: &Entity) -> Entity {
if given.common.handle.is_none() {
assert!(e.common.handle.is_some(), "every entity gets a handle");
e.common.handle = None;
}
e.common.owner = None;
if let (EntityKind::Insert(i), EntityKind::Insert(g)) = (&mut e.kind, &given.kind) {
for ((c, _), (gc, _)) in i.attribs.iter_mut().zip(&g.attribs) {
c.owner = None;
c.handle = gc.handle.and(c.handle);
}
}
e
}
#[test]
fn every_entity_reads_back_as_it_was_given() {
for (format, full) in [(Format::R2010, false), (Format::R2018, true)] {
let given = sample(full);
let (tags, skipped) = Drawing { format, entities: given.clone(), ..Drawing::default() }.tags();
assert_eq!(skipped, 0);
let back = read_back(&through_text(&tags, format));
assert_eq!(back.len(), given.len());
for (b, g) in back.into_iter().zip(&given) {
let mut g = g.clone();
// An R2010 file has no fit data in a hatch's spline edge, and keeps a multiline
// attribute as its one line.
if !full && let EntityKind::Hatch(h) = &mut g.kind {
assert!(matches!(&h.loops[0].path, HatchPath::Edges(e) if matches!(&e[3], HatchEdge::Spline { fit, .. } if fit.is_empty())));
h.clone_from(&Hatch {
gradient: h.gradient.clone(),
pattern: h.pattern.clone(),
solid: h.solid,
angle: h.angle,
scale: h.scale,
pattern_lines: h.pattern_lines.clone(),
..hatch(false)
});
}
assert_eq!(settled(b, &g), g, "{format:?}");
}
}
}
#[test]
fn owners_are_the_space_the_entity_is_in() {
let (tags, _) = Drawing { entities: sample(true), ..Drawing::default() }.tags();
let back = read_back(&tags);
assert_eq!(back[0].common.owner, Some(fixed::PAPER_RECORD), "a paper space entity");
assert_eq!(back[1].common.owner, Some(fixed::MODEL_RECORD));
let EntityKind::Insert(i) = &back[19].kind else { panic!("an insert") };
assert_eq!(i.attribs[0].0.owner, back[19].common.handle, "an attribute is owned by its insert");
assert_eq!((back[0].common.handle, i.attribs[0].0.handle), (Some(0x500), Some(0x501)), "given handles are kept");
}
#[test]
fn what_a_version_does_not_have_is_left_out() {
let given = sample(false);
// R12: no handles, owners or subclass markers, and no lightweight polyline.
let (tags, _) = Drawing { format: Format::R12, entities: given.clone(), ..Drawing::default() }.tags();
assert!(!tags.iter().any(|t| matches!(t.code, 5 | 100 | 105 | 370 | 420 | 440)), "R12 has none of these groups");
let d = Document::read(&tags);
assert_eq!(d.header.acadver().as_deref(), Some("AC1009"));
assert!(d.objects.is_empty() && d.classes.is_empty() && d.layouts().is_empty());
let back = read_back(&tags);
let EntityKind::Polyline(p) = &back[5].kind else { panic!("the lightweight polyline as a POLYLINE") };
assert!(p.closed() && p.flags & 128 != 0);
assert_eq!((p.vertices.len(), p.vertices[0].bulge, p.vertices[1].end_width, p.start_width), (3, 1.0, 0.2, 0.5));
assert_eq!(p.vertices[2].location, [4.0, 3.0, 2.0], "the elevation is each vertex's z");
// The colour's nearest index stands in for the true colour.
assert_eq!(back[0].common.color, Color::Index(30));
assert_eq!((back[0].common.lineweight, back[0].common.transparency), (Lineweight::ByLayer, Transparency::ByLayer));
// R13 has handles but still no LWPOLYLINE; R14 has it. R2000 has lineweights, R2004 colour.
let kinds = |format| {
let (tags, _) = Drawing { format, entities: given.clone(), ..Drawing::default() }.tags();
read_back(&tags)
};
assert!(matches!(kinds(Format::R13)[5].kind, EntityKind::Polyline(_)));
assert!(matches!(kinds(Format::R14)[5].kind, EntityKind::LwPolyline(_)));
assert_eq!(kinds(Format::R14)[0].common.lineweight, Lineweight::ByLayer);
let r2000 = &kinds(Format::R2000)[0].common;
assert_eq!((r2000.lineweight, r2000.color, r2000.transparency), (Lineweight::Hundredths(50), Color::Index(30), Transparency::ByLayer));
let r2004 = &kinds(Format::R2004)[0].common;
assert_eq!((r2004.color, r2004.transparency), (Color::Rgb([255, 127, 0]), Transparency::Alpha(128)));
for f in Format::ALL {
let (tags, _) = Drawing { format: f, ..Drawing::default() }.tags();
assert_eq!(Document::read(&through_text(&tags, f)).header.acadver().as_deref(), Some(f.acadver()));
}
}
fn tables() -> (Vec<Layer>, Vec<Linetype>, Vec<TextStyle>) {
let layer = Layer {
handle: Some(0x200),
name: "Walls".into(),
color: Color::Rgb([255, 0, 255]),
on: false,
frozen: true,
vp_freeze_new: true,
locked: true,
plot: false,
linetype: "GAS".into(),
lineweight: Lineweight::Hundredths(35),
plot_style: Some(fixed::PLOT_NORMAL),
transparency: Transparency::Alpha(127),
description: Some("outer walls".into()),
};
let plain = Layer {
handle: None,
name: "Plain".into(),
color: Color::Index(3),
on: true,
frozen: false,
vp_freeze_new: false,
locked: false,
plot: true,
linetype: "Continuous".into(),
lineweight: Lineweight::Default,
transparency: Transparency::ByLayer,
description: None,
..layer.clone()
};
let text_element = DashElement {
length: -0.2,
absolute: true,
text: Some("GAS".into()),
shape: None,
style: Some(0x201),
scale: 0.1,
rotation: 15.0,
offset: [-0.1, -0.05],
};
let dash = |length: f64| DashElement { length, scale: 1.0, ..DashElement::default() };
let gas = Linetype {
handle: Some(0x202),
name: "GAS".into(),
description: "Gas line".into(),
length: 0.95,
pattern: vec![dash(0.5), text_element, dash(-0.25)],
};
let style = TextStyle {
handle: Some(0x201),
name: "Notes".into(),
font_file: "arial.ttf".into(),
big_font: String::new(),
typeface: Some("Arial".into()),
height: 2.5,
width_factor: 0.8,
oblique: 15.0,
shape_file: false,
vertical: true,
backwards: true,
upside_down: true,
annotative: false,
};
(vec![layer, plain], vec![gas], vec![style])
}
#[test]
fn tables_read_back_as_they_were_given() {
let (layers, linetypes, styles) = tables();
let source = parse(b"0\nDIMSTYLE\n105\n203\n2\nISO-25\n70\n0\n41\n2.5\n278\n44\n69\n2\n70\n5\n").expect("parses");
let dim_styles = Document { tables: records(&source), ..Document::default() }.dim_styles();
let drawing = Drawing {
layers: layers.clone(),
linetypes: linetypes.clone(),
styles: styles.clone(),
dim_styles,
insunits: 4,
metric: false,
..Drawing::default()
};
let (tags, _) = drawing.tags();
let tags = through_text(&tags, Format::R2018);
let d = Document::read(&tags);
let back = d.layers();
assert_eq!(back[0].name, "0");
assert_eq!(back[1], layers[0]);
assert!(back[2].handle.is_some_and(|h| h >= fixed::FIRST_FREE));
assert_eq!(Layer { handle: None, ..back[2].clone() }, Layer { plot_style: Some(fixed::PLOT_NORMAL), ..layers[1].clone() });
let lt = d.linetypes();
assert_eq!(lt.iter().map(|l| l.name.as_str()).collect::<Vec<_>>(), ["ByBlock", "ByLayer", "Continuous", "GAS"]);
assert_eq!(lt[3], linetypes[0]);
let st = d.text_styles();
assert_eq!((st[0].name.as_str(), st[0].font_file.as_str()), ("Standard", "txt"));
assert_eq!(st[1], styles[0]);
let ds = d.dim_styles();
assert_eq!(ds.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["Standard", "ISO-25"]);
assert_eq!(ds[1].handle, Some(0x203));
assert_eq!(
ds[1].vars.iter().map(|(n, t)| (*n, t.str())).collect::<Vec<_>>(),
[("DIMASZ", "2.5".into()), ("DIMDSEP", "44".into()), ("DIMTFILL", "2".into()), ("DIMTFILLCLR", "5".to_string())]
);
assert_eq!((d.header.insunits(), d.header.measurement()), (4, Some(0)));
assert_eq!(d.app_ids()[0].name, "ACAD");
assert_eq!(d.vports()[0].name, "*Active");
}
#[test]
fn the_file_around_the_content() {
let line = |layer: &str| Entity { common: common(layer), kind: EntityKind::Line { start: [0.0; 3], end: [1.0; 3] } };
let door = BlockDef {
name: "DOOR".into(),
base: [1.0, 2.0, 3.0],
description: "A door".into(),
entities: vec![line("0"), Entity { common: common("0"), kind: EntityKind::Other }],
};
let raw = vec![Tag::s(0, "MULTILEADER"), Tag::s(5, "900"), Tag::s(8, "0")];
let drawing = Drawing {
blocks: vec![door],
// Handles in the reserved range, and one given twice, are replaced.
entities: vec![
Entity { common: Common { handle: Some(0xA), ..common("0") }, ..line("0") },
Entity { common: Common { handle: Some(0x400), ..common("0") }, ..line("0") },
Entity { common: Common { handle: Some(0x400), ..common("0") }, ..line("0") },
],
raw_entities: raw,
extents: [0.0, 0.0, 100.0, 50.0],
header: vec![
("$LTSCALE".into(), vec![Tag::f(40, 2.0)]),
("$INSUNITS".into(), vec![Tag::i(70, 6)]),
("$ACADVER".into(), vec![Tag::s(1, "nonsense")]),
("$HANDSEED".into(), vec![Tag::s(5, "1")]),
],
root_entries: vec![("ACAD_IMAGE_DICT".into(), 0x901)],
raw_objects: vec![Tag::s(0, "DICTIONARY"), Tag::s(5, "901"), Tag::s(330, "A")],
raw_classes: vec![Tag::s(0, "CLASS"), Tag::s(1, "IMAGEDEF")],
..Drawing::default()
}
.with_linear_units(2);
let (tags, skipped) = drawing.tags();
assert_eq!(skipped, 1, "the entity with no groups to write");
assert_eq!(tags.last().map(|t| (t.code, t.str())), Some((0, "EOF".to_string())));
let d = Document::read(&tags);
assert_eq!(d.sections, 6);
assert!(d.skipped.is_empty());
// Every handle once, and the seed above them all.
let handles: Vec<u64> = tags.iter().filter(|t| matches!(t.code, 5 | 105)).filter_map(|t| u64::from_str_radix(&t.str(), 16).ok()).collect();
let unique: HashSet<u64> = handles.iter().copied().collect();
let seed = d.header.handseed().expect("a handle seed");
assert_eq!(unique.len(), handles.len());
assert!(handles.iter().filter(|h| **h != seed).all(|h| *h < seed));
assert_eq!(d.entities.iter().filter(|e| e.record().handle() == Some(0x400)).count(), 1);
assert!(d.entities.iter().all(|e| e.record().handle() != Some(0xA)));
assert_eq!(d.entities.len(), 4, "the raw record is written after the entities");
assert_eq!(d.entities[3].kind, "MULTILEADER");
// The header: the writer's version and seed, the caller's variables over the defaults.
assert_eq!((d.header.acadver().as_deref(), d.header.ltscale(), d.header.insunits()), (Some("AC1032"), 2.0, 6));
assert_eq!(d.header.vars.iter().filter(|(n, _)| *n == "$INSUNITS" || *n == "$ACADVER" || *n == "$HANDSEED").count(), 3);
assert_eq!(d.header.point("$EXTMAX"), Some([100.0, 50.0, 0.0]));
assert_eq!(d.header.vars.iter().find(|(name, _)| *name == "$LUNITS").map(|(_, tags)| tags[0].i64()), Some(2));
// Blocks, their records and the layouts that name them.
let names = d.block_names();
let block = d.block("DOOR").expect("the block");
assert_eq!((block.base(), block.description().as_deref(), block.entities.len()), ([1.0, 2.0, 3.0], Some("A door"), 1));
assert_eq!(block.block_record().and_then(|h| names.get(&h).copied()), Some("DOOR"));
assert_eq!(Entity::read(&block.entities[0]).common.owner, block.block_record());
let mut layouts = d.layouts();
layouts.sort_by_key(|l| l.tab_order);
let of = |i: usize| (layouts[i].name.as_str(), layouts[i].block_record.and_then(|h| names.get(&h).copied()));
assert_eq!((of(0), of(1)), (("Model", Some("*Model_Space")), ("Layout1", Some("*Paper_Space"))));
let root = d.root_dictionary().expect("the root dictionary");
assert_eq!((root.get("ACAD_LAYOUT"), root.get("ACAD_IMAGE_DICT")), (Some(fixed::LAYOUTS), Some(0x901)));
assert!(d.objects.iter().any(|r| r.handle() == Some(0x901)));
assert!(d.classes.iter().any(|r| r.groups().s(1).as_deref() == Some("IMAGEDEF")));
// The file opens at its extents.
let vp = &d.vports()[0];
assert_eq!((vp.center, vp.height > 50.0), (Some([50.0, 25.0]), true));
}
#[test]
fn one_entity_for_a_file_the_caller_writes() {
let insert = sample(true).remove(19);
let mut next = 0x10;
let tags =
entity_tags(&Entity { common: Common { handle: Some(0x5), ..insert.common.clone() }, ..insert.clone() }, Format::R2018, 0x1F, &mut next);
let (recs, strays) = craft_dxf::model::entity_records(&records(&tags));
assert_eq!((recs.len(), strays), (1, 0));
let back = Entity::read(&recs[0]);
assert_eq!((back.common.handle, back.common.owner), (Some(0x5), Some(0x1F)), "its own handle, whatever its number");
assert_eq!(recs[0].seqend.map(|s| s.handle()), Some(Some(0x10)));
assert_eq!(next, 0x11, "the next free handle is past the ones used");
assert!(entity_tags(&Entity { common: common("0"), kind: EntityKind::Other }, Format::R2018, 0, &mut next).is_empty());
}
#[test]
fn records_kept_from_another_file_are_renumbered() {
use craft_dxf::model::write::{is_pointer_code, remap_handles};
let kept = parse(
b"0\nMULTILEADER\n5\n80\n102\n{ACAD_REACTORS\n330\n81\n102\n}\n330\n1F\n100\nAcDbEntity\n8\n0\n340\n0\n344\nZZ\n1001\nAPP\n1005\n82\n0\nDIMSTYLE\n105\n90\n2\nX\n340\n81\n",
)
.expect("parses");
let map = |h: u64| match h {
0x80 => Some(0x500),
0x81 => Some(0x501),
0x90 => Some(0x502),
_ => None,
};
let out = remap_handles(&kept, map);
let groups: Vec<(i32, String)> = out.iter().map(|t| (t.code, t.str())).collect();
let want: Vec<(i32, String)> = [
(0, "MULTILEADER"),
(5, "500"),
(102, "{ACAD_REACTORS"),
(330, "501"),
(102, "}"),
// An owner the new file has no counterpart for is the null handle.
(330, "0"),
(100, "AcDbEntity"),
(8, "0"),
(340, "0"),
// Text that is no handle is left as it is.
(344, "ZZ"),
(1001, "APP"),
(1005, "0"),
(0, "DIMSTYLE"),
(105, "502"),
(2, "X"),
(340, "501"),
]
.iter()
.map(|(c, v)| (*c, v.to_string()))
.collect();
assert_eq!(groups, want);
assert!(is_pointer_code(330) && is_pointer_code(1005) && is_pointer_code(390) && !is_pointer_code(5) && !is_pointer_code(370));
// The renumbered records go through the writer as they are.
let (tags, skipped) = Drawing { raw_entities: out[..12].to_vec(), ..Drawing::default() }.tags();
assert_eq!(skipped, 0);
assert!(Document::read(&tags).entities.iter().any(|e| e.kind == "MULTILEADER" && e.record().handle() == Some(0x500)));
}