Files
craft-libs/crates/dxf/tests/model_objects.rs
T
Dylan M. Taylor b85549a5b4 craft-dxf: model objects (page setup, GROUP, IMAGEDEF, SORTENTSTABLE, SPATIAL_FILTER, DIMASSOC, SCALE, MLINESTYLE), HELIX, dimension overrides (#955)
* 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).

* craft-dxf: model objects (page setup, groups, image definitions, draw order, clip filters, DIMASSOC, scales, multiline styles), HELIX and dimension overrides

model::objects types the OBJECTS records both readers and CADCraft's passes
use: the AcDbPlotSettings groups of a LAYOUT, GROUP, IMAGEDEF, SORTENTSTABLE,
SPATIAL_FILTER, DIMASSOC, SCALE and MLINESTYLE, and dim_overrides reads the
DSTYLE list of a dimension's ACAD extended data. Document gains objects_of and
dictionary_names (the name a dictionary lists an object under). HELIX is an
entity kind, read and written.

From CADCraft's cadcraft-io dxf_read/objects.rs, document.rs, dimassoc.rs,
entities.rs, dxf_mline.rs, dxf_clip.rs and dxf_image.rs (5a6aa9b6).

* README: keep main's crate list after the merge
2026-11-11 20:50:47 -05:00

136 lines
7.9 KiB
Rust

//! The typed OBJECTS records and dimension style overrides.
use craft_dxf::model::objects::{DimAssoc, Group, ImageDef, MLineStyle, PlotSettings, Scale, SortEntsTable, SpatialFilter, dim_overrides};
use craft_dxf::model::{Color, Document, Entity, EntityKind};
use craft_dxf::{Tag, parse};
fn tags(text: &str) -> Vec<Tag> {
let lines: Vec<&str> = text.split(['\n', '|']).map(str::trim).filter(|l| !l.is_empty()).map(|l| if l == "~" { "" } else { l }).collect();
parse((lines.join("\n") + "\n").as_bytes()).expect("the test drawing parses")
}
const OBJECTS: &str = "
0|SECTION|2|OBJECTS
0|DICTIONARY|5|C|3|ACAD_GROUP|350|D|3|ACAD_MLINESTYLE|350|17
0|DICTIONARY|5|D|3|Doors|350|40
0|DICTIONARY|5|17|3|Wall|350|41
0|GROUP|5|40|330|D|100|AcDbGroup|300|All doors|70|0|71|0|340|A1|340|A2
0|MLINESTYLE|5|41|100|AcDbMlineStyle|2|WALL|70|273|3|A wall|62|5|51|45.0|52|90.0|71|2|49|0.5|62|1|6|DASHED|49|-0.5|62|256|6|BYLAYER
0|IMAGEDEF|5|42|100|AcDbRasterImageDef|90|0|1|plans/site.png|10|640.0|20|480.0|11|0.01|21|0.01|280|1|281|5
0|SORTENTSTABLE|5|43|330|44|100|AcDbSortentsTable|330|1F|331|A1|5|B|331|A2|5|A|331|zz|5|9|5|8
0|SPATIAL_FILTER|5|45|100|AcDbFilter|100|AcDbSpatialFilter|70|2|10|0.0|20|0.0|10|4.0|20|3.0|210|0.0|220|0.0|230|1.0|11|1.0|21|2.0|31|3.0|71|1|72|1|40|7.5|73|0
40|1.0|40|0.0|40|0.0|40|-5.0|40|0.0|40|1.0|40|0.0|40|-6.0|40|0.0|40|0.0|40|1.0|40|0.0
40|2.0|40|0.0|40|0.0|40|0.0|40|0.0|40|2.0|40|0.0|40|0.0|40|0.0|40|0.0|40|2.0|40|0.0
0|DIMASSOC|5|46|330|77|100|AcDbDimAssoc|330|D1|90|3|70|1|71|0
1|AcDbOsnapPointRef|72|1|331|A1|73|2|91|4|40|0.0|10|1.0|20|2.0|30|0.0
1|AcDbOsnapPointRef|72|6|331|A2|73|0|91|0|40|0.5|10|3.0|20|4.0|30|0.0|332|A3
0|SCALE|5|47|100|AcDbScale|70|0|300|1:50|140|1.0|141|50.0|290|0
0|SCALE|5|48|100|AcDbScale|300|broken|140|1.0
0|LAYOUT|5|49|100|AcDbPlotSettings|1|Final|2|printer.pc3|4|ISO_A3_(420.00_x_297.00_MM)|6|~|40|7.5|41|20.0|42|7.5|43|20.0|44|420.0|45|297.0|46|1.0|47|2.0|48|0.0|49|0.0|140|100.0|141|50.0|142|1.0|143|50.0|70|688|72|1|73|1|74|4|7|monochrome.ctb|75|0|147|0.02|148|3.0|149|4.0|100|AcDbLayout|1|Sheet|70|1|71|1|10|9.0
0|ENDSEC|0|EOF
";
#[test]
fn objects_typed() {
let t = tags(OBJECTS);
let d = Document::read(&t);
let names = d.dictionary_names();
assert_eq!((names.get(&0x40).copied(), names.get(&0x41).copied(), names.get(&0xD).copied()), (Some("Doors"), Some("Wall"), Some("ACAD_GROUP")));
let one = |kind: &'static str| *d.objects_of(kind).next().expect("the record");
let g = Group::read(&one("GROUP"));
assert_eq!(
(g.handle, g.description.as_str(), g.unnamed, g.selectable, g.members.as_slice()),
(Some(0x40), "All doors", false, false, [0xA1, 0xA2].as_slice())
);
let m = MLineStyle::read(&one("MLINESTYLE")).expect("a style");
assert_eq!(
(m.name.as_str(), m.description.as_str(), m.flags, m.fill, m.start_angle, m.end_angle),
("WALL", "A wall", 273, Color::Index(5), 45.0, 90.0)
);
assert_eq!(m.elements.len(), 2);
assert_eq!((m.elements[0].offset, m.elements[0].color, m.elements[0].linetype.as_str()), (0.5, Color::Index(1), "DASHED"));
assert_eq!((m.elements[1].offset, m.elements[1].color), (-0.5, Color::ByLayer));
let i = ImageDef::read(&one("IMAGEDEF"));
assert_eq!(
(i.handle, i.file.as_str(), i.size, i.pixel_size, i.loaded, i.units),
(Some(0x42), "plans/site.png", [640.0, 480.0], [0.01, 0.01], true, 5)
);
let s = SortEntsTable::read(&one("SORTENTSTABLE")).expect("a table");
// The block is the 330 after the marker, not the owning dictionary; a pair needs both handles.
assert_eq!((s.block_record, s.entries.as_slice()), (Some(0x1F), [(0xA1, 0xB), (0xA2, 0xA)].as_slice()));
let f = SpatialFilter::read(&one("SPATIAL_FILTER"));
assert_eq!(
(f.boundary.as_slice(), f.normal, f.origin, f.enabled, f.inverted),
([[0.0, 0.0], [4.0, 3.0]].as_slice(), [0.0, 0.0, 1.0], [1.0, 2.0, 3.0], true, false)
);
// The front clip distance is the 40 after its switch, not the first matrix value.
assert_eq!((f.front, f.back), (Some(7.5), None));
assert_eq!(f.inverse_insert.map(|m| (m[0], m[3], m[7])), Some((1.0, -5.0, -6.0)));
assert_eq!(f.to_boundary.map(|m| (m[0], m[5], m[10])), Some((2.0, 2.0, 2.0)));
let a = DimAssoc::read(&one("DIMASSOC"));
assert_eq!((a.handle, a.dimension, a.flags, a.trans_space, a.refs.len()), (Some(0x46), Some(0xD1), 3, true, 2));
assert_eq!((a.refs[0].snap, a.refs[0].entity, a.refs[0].sub_type, a.refs[0].sub_index, a.refs[0].point), (1, Some(0xA1), 2, 4, [1.0, 2.0, 0.0]));
assert_eq!((a.refs[1].snap, a.refs[1].other, a.refs[1].near), (6, Some(0xA3), 0.5));
let scales: Vec<Scale> = d.objects_of("SCALE").filter_map(Scale::read).collect();
assert_eq!(scales.len(), 1, "a scale without both numbers is none");
assert_eq!((scales[0].name.as_str(), scales[0].paper, scales[0].drawing, scales[0].unit), ("1:50", 1.0, 50.0, false));
let p = PlotSettings::read(&one("LAYOUT"));
// The plot settings' own 1, 70 and 10, not the layout's after them.
assert_eq!((p.name.as_str(), p.device.as_str(), p.paper.as_str(), p.view.as_str()), ("Final", "printer.pc3", "ISO_A3_(420.00_x_297.00_MM)", ""));
assert_eq!((p.margins, p.paper_size, p.origin, p.window), ([7.5, 20.0, 7.5, 20.0], [420.0, 297.0], [1.0, 2.0], [0.0, 0.0, 100.0, 50.0]));
assert_eq!((p.scale, p.flags, p.paper_units, p.rotation, p.plot_type), ([1.0, 50.0], 688, 1, 1, 4));
assert_eq!((p.style_sheet.as_str(), p.standard_scale, p.scale_factor, p.image_origin), ("monochrome.ctb", 0, 0.02, [3.0, 4.0]));
}
#[test]
fn damaged_objects_read_as_what_they_hold() {
let t = tags(
"0|SECTION|2|OBJECTS|0|GROUP|0|MLINESTYLE|0|IMAGEDEF|0|SORTENTSTABLE|0|SPATIAL_FILTER|72|1|0|DIMASSOC|1|x|0|SCALE|0|LAYOUT|0|DICTIONARY|3|a|350|zz|0|ENDSEC|0|EOF",
);
let d = Document::read(&t);
let one = |kind: &'static str| *d.objects_of(kind).next().expect("the record");
assert!(Group::read(&one("GROUP")).members.is_empty());
assert!(MLineStyle::read(&one("MLINESTYLE")).is_none());
assert_eq!(ImageDef::read(&one("IMAGEDEF")).file, "");
assert!(SortEntsTable::read(&one("SORTENTSTABLE")).is_none());
let f = SpatialFilter::read(&one("SPATIAL_FILTER"));
assert_eq!((f.front, f.inverse_insert, f.to_boundary, f.boundary.len()), (None, None, None, 0));
assert_eq!(DimAssoc::read(&one("DIMASSOC")).refs.len(), 1);
assert!(Scale::read(&one("SCALE")).is_none());
assert_eq!(PlotSettings::read(&one("LAYOUT")).plot_type, 5);
assert!(d.dictionary_names().is_empty());
}
#[test]
fn dimension_style_overrides() {
let t = tags(
"0|DIMENSION|70|1|1001|OTHER|1000|DSTYLE|1002|{|1070|41|1040|9.0|1002|}|1001|ACAD|1000|DSTYLE|1002|{|1070|41|1040|2.5|1070|9999|1070|1|1040|7.0|1070|340|1005|2F|1070|147|1002|}|1000|after",
);
let o = dim_overrides(&t[1..]);
// The ACAD list only; a code that names no variable is passed over with its value, and a
// stray group realigns on the next 1070.
assert_eq!(o.iter().map(|(n, v)| (*n, v.str())).collect::<Vec<_>>(), [("DIMASZ", "2.5".to_string()), ("DIMTXSTY", "2F".to_string())]);
assert!(dim_overrides(&tags("0|DIMENSION|70|1")).is_empty());
}
#[test]
fn helix() {
let t = tags(
"0|SECTION|2|ENTITIES|0|HELIX|100|AcDbEntity|8|0|100|AcDbSpline|70|0|71|3|40|0.0|40|1.0|10|1.0|20|0.0|30|0.0|10|0.0|20|1.0|30|4.0|100|AcDbHelix|90|29|91|63|10|0.5|20|0.5|30|0.0|11|1.5|21|0.5|31|0.0|12|0.0|22|0.0|32|1.0|40|1.0|41|3.0|42|2.0|290|0|280|1|0|ENDSEC|0|EOF",
);
let d = Document::read(&t);
let EntityKind::Helix(h) = Entity::read(&d.entities[0]).kind else { panic!("a helix") };
// The spline's 10s and 40s and the helix's own are kept apart by their subclasses.
assert_eq!((h.spline.control.len(), h.spline.knots.len(), h.spline.degree), (2, 2, 3));
assert_eq!((h.axis_base, h.start, h.axis), ([0.5, 0.5, 0.0], [1.5, 0.5, 0.0], [0.0, 0.0, 1.0]));
assert_eq!((h.radius, h.turns, h.turn_height, h.ccw, h.constrain), (1.0, 3.0, 2.0, false, 1));
}