craft-dxf: model::write, a drawing as tags (R12 to 2018) (#947)

* craft-dxf: model, the drawing above the tags (header, tables, blocks, layouts)

Document::read indexes a tag list into header variables, table records,
blocks with their entities, the ENTITIES records (a POLYLINE with its
vertices, an INSERT with its attributes) and the objects, each a slice of the
tag list. The typed table records, LAYOUT and DICTIONARY are made on request.
Also the colour index palette, colour, lineweight and transparency values,
extended data lookup and the object coordinate system.

Merged from CADCraft's cadcraft-io dxf_read (5a6aa9b6) and VectorCraft's
vectorcraft-cad import (566f7208). Neither app's document types appear.

* craft-dxf: model entities (geometry, text, MTEXT columns, hatches, dimensions)

Entity::read types an entity record: the properties every entity has, and
LINE, POINT, CIRCLE, ARC, ELLIPSE, LWPOLYLINE, POLYLINE with its vertices
(2D, 3D, mesh, polyface), SPLINE, RAY, XLINE, TEXT, MTEXT with columns,
ATTDEF, INSERT with its attributes (multiline ones included), DIMENSION,
LEADER, SOLID, TRACE, 3DFACE, HATCH (polyline and edge loops, pattern lines,
seeds, gradient), VIEWPORT, IMAGE, WIPEOUT and TOLERANCE. Other types stay
readable from the record's tags. model::text has the caret encoding of
strings, both ways.

Merged from CADCraft's cadcraft-io dxf_read (5a6aa9b6) and VectorCraft's
vectorcraft-cad import (566f7208).

* craft-dxf: a hatch spline edge without fit data leaves the loop's source count

Before R2010 a spline edge has no 97 of its own, so the 97 after it counts the
loop's source entities. It was read as the edge's fit point count and the
sources were lost.

* craft-dxf: model::write, a drawing as tags (R12 to 2018)

Drawing::tags writes the header, classes, the nine symbol tables, blocks,
entities and (R2000 and later) the dictionaries, plot style placeholder and
layouts around the layers, linetypes, styles, dimension styles, blocks and
entities it is given, in the model's own types. entity_tags writes one entity
for a caller with its own file. Handles given with the content are kept;
the rest are numbered after them.

The file skeleton is VectorCraft's vectorcraft-cad writer.rs (566f7208); the
MTEXT column groups are CADCraft's dxf_read/mtext_dxf.rs column_tags
(5a6aa9b6).

* README: keep main's crate list after the merge
This commit is contained in:
Dylan M. Taylor
2026-11-11 20:03:30 -05:00
committed by GitHub
parent b15ff4486f
commit 491fb0095f
4 changed files with 2161 additions and 1 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
- [`craft-dxf`](crates/dxf/src/lib.rs): DXF tag reader and writer, from CADCraft: ASCII and
binary DXF as (group code, value) tags, split into sections and entities, with pre-2007 code
pages and version detection, and written back; and `model`, the drawing above the tags (header
variables, tables, blocks, layouts, entities), merged from CADCraft's and VectorCraft's readers. Bounded
variables, tables, blocks, layouts, entities) and its writer (R12 to 2018), merged from CADCraft's and VectorCraft's readers. Bounded
on hostile input. Mapping the model onto a document stays in each app.
- [`craft-ecproj-format`](crates/ecproj-format/README.md): the envelope of EffectCraft `.ecproj`
project files: `savedBy`, the schema version and its check, and the file object with the
+1
View File
@@ -22,6 +22,7 @@ mod entity;
mod hatch;
mod tables;
pub mod text;
pub mod write;
pub use document::{Block, Dictionary, Document, EntityRecord, Header, Layout, Record, entity_records, records};
pub use entity::{
File diff suppressed because it is too large Load Diff
+606
View File
@@ -0,0 +1,606 @@
//! 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, Image, Insert,
Layer, Leader, Linetype, Lineweight, LwPolyline, LwVertex, 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::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[17].kind else { panic!("an insert") };
assert_eq!(i.attribs[0].0.owner, back[17].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()
};
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]));
// 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(17);
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());
}