craft-dxf: model reader fixes from open CADCraft PRs; MLINE (#981)

* 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).

* craft-dxf: an attribute's vertical alignment is group 74, not the field length in 73

Group 73 of an ATTRIB or ATTDEF is its field length. A writer that emits it
turned every attribute into another vertical alignment, since 73 was read
first and 74 only in its absence.

Ported from storytold/cadcraft#1591.

* README: keep main's crate list after the merge

* craft-dxf: read entity groups written after the entity's own subclass marker

Some writers leave the AcDbEntity subclass without a layer and put its
groups right after the entity's own marker. Every entity has a layer, so
its absence marks that layout, and the run of AcDbEntity codes that starts
after the marker is read too. The run ends at the first other code, so the
entity's own groups that reuse a code (an MTEXT's 48) are not taken for it.

Ported from storytold/cadcraft#2877.

* craft-dxf: an attribute's justification is read from its text groups, not its multiline header

A multiline ATTRIB or ATTDEF writes a header in its AcDbAttribute subclass
whose 72 and 11 are not the justification and alignment point. A
left-justified attribute writes no 72 of its own, so the header's was read
and the attribute came out centred. The text is now read from the groups up
to the attribute's subclass and, after it, from all but 71, 72, 73 and 11.

Ported from storytold/cadcraft#2313.

* craft-dxf: MLINE, read and written

EntityKind::MLine holds the style name and handle, scale, justification,
flags, start point and each vertex with its direction, miter and the 41 and
42 parameter lists of each element. Reading the 41s and 42s into the list
their 74 or 75 started keeps them apart.

From CADCraft's cadcraft-io dxf_mline.rs read_entity and entity_tags
(5a6aa9b6).

---------

Co-authored-by: Oliver Hagmann <268387159+ohagmann@users.noreply.github.com>
Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Francesco BV <francesco.esposito@bvitalia.net>
This commit is contained in:
authored and GitHub committed 2026-11-11 21:47:41 -05:00
1 parent 2eddd3073e
commit 83c654d2a9
6 files changed
+257 -12

No files matched your search

+2 -1
View File
@@ -27,7 +27,8 @@ pub mod write;
pub use document::{Block, Dictionary, Document, EntityRecord, Header, Layout, Record, entity_records, records};
pub use entity::{
Attrib, Common, Dimension, Entity, EntityKind, Helix, Image, Insert, Leader, LwPolyline, LwVertex, Polyline, Spline, Text, Vertex, Viewport,
Attrib, Common, Dimension, Entity, EntityKind, Helix, Image, Insert, Leader, LwPolyline, LwVertex, MLine, MLineVertex, Polyline, Spline, Text,
Vertex, Viewport,
};
pub use hatch::{Gradient, Hatch, HatchEdge, HatchLoop, HatchPath, PatternLine};
pub use tables::{AppId, BlockRecord, DashElement, DimStyle, Layer, Linetype, TextStyle, Ucs, View, Vport, dim_var_code, dim_var_name};
+131 -6
View File
@@ -6,6 +6,8 @@
//! 3DFACE, MTEXT, LEADER and DIMENSION definition points are world coordinates. Angles are in
//! degrees, except an ELLIPSE's parameters, which the format stores in radians.
use std::borrow::Cow;
use super::document::{EntityRecord, Record};
use super::hatch::Hatch;
use super::text::{MText, decode_line, split_embedded};
@@ -39,21 +41,40 @@ pub struct Common {
pub plot_style: Option<u64>,
}
/// The AcDbEntity group codes a writer can put after the entity's own subclass marker instead
/// (DXF Reference, Common Group Codes for Entities): paper space, layout, layer, linetype,
/// material, colour, lineweight, linetype scale, visibility, true colour, colour name,
/// transparency, plot style and shadow mode.
const ENTITY_CODES: [i32; 14] = [67, 410, 8, 6, 347, 62, 370, 48, 60, 420, 430, 440, 390, 284];
/// The groups of the AcDbEntity subclass: the record up to the entity's own subclass marker (all
/// of it in files without markers, and in records with no AcDbEntity marker, whose layer and
/// colour may follow the entity's marker), or up to an embedded object (101). Later groups reuse
/// the codes: an MTEXT's column width is 48. Markers inside an application-defined group do not
/// count.
fn entity_groups(tags: &[Tag]) -> &[Tag] {
///
/// Some writers leave the AcDbEntity subclass without a layer and put its groups right after the
/// entity's own marker. Every entity has a layer, so its absence marks that layout, and the run
/// of [`ENTITY_CODES`] that starts after the marker is read too. The run ends at the first other
/// code, so the entity's own groups that reuse a code are not taken for it. Only such a record
/// pays for the copy.
fn entity_groups(tags: &[Tag]) -> Cow<'_, [Tag]> {
let is_entity = |t: &Tag| t.str_ref().is_some_and(|s| s == "AcDbEntity");
let marked = outside_app(tags).any(|(_, t)| t.code == 100 && is_entity(t));
let end = outside_app(tags).find(|(_, t)| (marked && t.code == 100 && !is_entity(t)) || t.code == 101).map_or(tags.len(), |(i, _)| i);
tags.get(..end).unwrap_or(tags)
let groups = tags.get(..end).unwrap_or(tags);
let after_own_marker = marked && tags.get(end).is_some_and(|t| t.code == 100);
if !after_own_marker || outside_app(groups).any(|(_, t)| t.code == 8) {
return Cow::Borrowed(groups);
}
let run = tags.get(end + 1..).unwrap_or_default().iter().take_while(|t| ENTITY_CODES.contains(&t.code));
Cow::Owned(groups.iter().chain(run).cloned().collect())
}
impl Common {
pub fn read(tags: &[Tag]) -> Common {
let (all, e) = (Groups(tags), Groups(entity_groups(tags)));
let own = entity_groups(tags);
let (all, e) = (Groups(tags), Groups(&own));
Common {
handle: e.handle(5),
owner: outside_app(tags).find(|(_, t)| t.code == 330).and_then(|(_, t)| parse_handle(&t.str())),
@@ -201,6 +222,42 @@ pub struct Helix {
pub constrain: i64,
}
/// A vertex of an MLINE.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct MLineVertex {
/// Groups 11/21/31.
pub location: Point,
/// Groups 12/22/32: the direction of the segment that starts here.
pub direction: Point,
/// Groups 13/23/33: the miter direction.
pub miter: Point,
/// Per element of the style: its 74 count of 41 parameters (the distance from the vertex
/// along the miter, then the pairs that start and end its breaks).
pub element_params: Vec<Vec<f64>>,
/// Per element: its 75 count of 42 area fill parameters.
pub fill_params: Vec<Vec<f64>>,
}
/// An MLINE: parallel lines drawn by an MLINESTYLE.
#[derive(Clone, Debug, PartialEq)]
pub struct MLine {
/// Group 2: the style's name.
pub style: String,
/// Group 340: the MLINESTYLE.
pub style_handle: Option<u64>,
/// Group 40.
pub scale: f64,
/// Group 70: 0 top, 1 zero (centred), 2 bottom.
pub justification: i64,
/// Group 71: 1 has vertices, 2 closed, 4 start caps suppressed, 8 end caps suppressed.
pub flags: i64,
/// Group 73: the number of elements of the style, as the entity states it.
pub elements: i64,
/// Groups 10/20/30.
pub start: Point,
pub vertices: Vec<MLineVertex>,
}
/// TEXT, and the text of an ATTRIB or ATTDEF.
#[derive(Clone, Debug, PartialEq)]
pub struct Text {
@@ -223,12 +280,22 @@ pub struct Text {
pub generation: i64,
/// Group 72: 0 left, 1 centre, 2 right, 3 aligned, 4 middle, 5 fit.
pub halign: i64,
/// Group 73 (74 in an ATTRIB or ATTDEF): 0 baseline, 1 bottom, 2 middle, 3 top.
/// Group 73 (74 in an ATTRIB or ATTDEF, whose 73 is the field length): 0 baseline, 1 bottom,
/// 2 middle, 3 top.
pub valign: i64,
}
impl Text {
/// The typed form of TEXT groups.
pub fn read(g: &Groups<'_>) -> Text {
Self::read_as(g, false)
}
/// A TEXT, or (with `attribute`) the text of an ATTRIB or ATTDEF. The vertical alignment is
/// group 73 for TEXT and group 74 for attributes, whose group 73 is the field length: a
/// writer that emits the field length would otherwise turn every attribute into baseline
/// text.
pub fn read_as(g: &Groups<'_>, attribute: bool) -> Text {
Text {
insert: g.point(10),
align: g.point_opt(11),
@@ -240,7 +307,7 @@ impl Text {
style: g.name(7).unwrap_or_else(|| "Standard".into()),
generation: g.i_or(71, 0),
halign: g.i_or(72, 0),
valign: g.i(73).or_else(|| g.i(74)).unwrap_or(0),
valign: g.i_or(if attribute { 74 } else { 73 }, 0),
}
}
}
@@ -270,9 +337,17 @@ impl Attrib {
let sub = own.iter().position(|t| t.code == 100 && t.str_ref().is_some_and(|s| s.trim().starts_with("AcDbAttribute"))).unwrap_or(0);
let rest = own.get(sub..).unwrap_or_default();
let lock_position = rest.iter().position(|t| t.code == 2).is_some_and(|at| rest.iter().skip(at + 1).any(|t| t.code == 280 && t.i64() != 0));
// The text is described by the groups up to the attribute's own subclass, and from there
// by all but the field length (73) and a multiline attribute's header (71, 72 and the
// point 11): there 72 isn't the justification nor 11 the alignment point, and a
// left-justified attribute writes no 72 of its own, so the multiline one was read as
// centred.
let at = own.iter().position(|t| t.code == 100 && t.str_ref().is_some_and(|s| s.trim().starts_with("AcDbAttribute"))).unwrap_or(own.len());
let (head, tail) = own.split_at(at);
let text_groups: Vec<Tag> = head.iter().chain(tail.iter().filter(|t| !matches!(t.code, 71 | 72 | 73 | 11 | 21 | 31))).cloned().collect();
Attrib {
tag: g.s(2).unwrap_or_default(),
text: Text::read(&g),
text: Text::read_as(&Groups(&text_groups), true),
prompt: if kind == "ATTDEF" { decode_line(&g.s(3).unwrap_or_default()) } else { String::new() },
flags: g.i_or(70, 0),
lock_position,
@@ -454,6 +529,7 @@ pub enum EntityKind {
Polyline(Polyline),
Spline(Spline),
Helix(Helix),
MLine(MLine),
Ray {
base: Point,
direction: Point,
@@ -549,6 +625,50 @@ fn polyline(g: &Groups<'_>, followers: &[Record<'_>]) -> Polyline {
}
}
fn mline(tags: &[Tag], g: &Groups<'_>) -> MLine {
let mut vertices: Vec<MLineVertex> = Vec::new();
// Which list the 41s or 42s go to: the last one a 74 or 75 started.
let mut fill = false;
for t in tags {
if t.code == 11 {
vertices.push(MLineVertex { location: [t.f64(), 0.0, 0.0], ..MLineVertex::default() });
continue;
}
let Some(v) = vertices.last_mut() else { continue };
let axis = match t.code / 10 {
1 => 0,
2 => 1,
_ => 2,
};
match t.code {
21 | 31 => v.location[axis] = t.f64(),
12 | 22 | 32 => v.direction[axis] = t.f64(),
13 | 23 | 33 => v.miter[axis] = t.f64(),
74 => {
v.element_params.push(Vec::new());
fill = false;
}
75 => {
v.fill_params.push(Vec::new());
fill = true;
}
41 if !fill => v.element_params.last_mut().into_iter().for_each(|l| l.push(t.f64())),
42 if fill => v.fill_params.last_mut().into_iter().for_each(|l| l.push(t.f64())),
_ => {}
}
}
MLine {
style: g.s(2).unwrap_or_default(),
style_handle: g.handle(340).filter(|h| *h != 0),
scale: g.f_or(40, 1.0),
justification: g.i_or(70, 0),
flags: g.i_or(71, 1),
elements: g.i_or(73, 0),
start: g.point(10),
vertices,
}
}
fn spline(g: &Groups<'_>) -> Spline {
Spline {
degree: g.i_or(71, 3),
@@ -672,6 +792,11 @@ impl Entity {
constrain: h.i_or(280, 2),
})
}
"MLINE" => {
let own = g.subclass("AcDbMline");
let own = if own.0.is_empty() { g } else { own };
EntityKind::MLine(mline(own.0, &own))
}
"RAY" => EntityKind::Ray { base: line(&g).0, direction: line(&g).1 },
"XLINE" => EntityKind::XLine { base: line(&g).0, direction: line(&g).1 },
"TEXT" => EntityKind::Text(Text::read(&g)),
+32
View File
@@ -809,6 +809,37 @@ impl W {
self.i(290, i64::from(h.ccw));
self.i(280, h.constrain);
}
EntityKind::MLine(m) => {
let n = |v: usize| i64::try_from(v).unwrap_or(0);
self.sub("AcDbMline");
self.s(2, m.style.as_str());
self.h(340, m.style_handle.unwrap_or(0));
self.f(40, m.scale);
self.i(70, m.justification);
self.i(71, m.flags);
self.i(72, n(m.vertices.len()));
self.i(73, m.elements);
self.p(10, m.start);
if c.extrusion != [0.0, 0.0, 1.0] {
self.p(210, c.extrusion);
}
for v in &m.vertices {
self.p(11, v.location);
self.p(12, v.direction);
self.p(13, v.miter);
for (k, params) in v.element_params.iter().enumerate() {
self.i(74, n(params.len()));
for x in params {
self.f(41, *x);
}
let fill = v.fill_params.get(k).map_or(&[][..], Vec::as_slice);
self.i(75, n(fill.len()));
for x in fill {
self.f(42, *x);
}
}
}
}
EntityKind::Ray { base, direction } | EntityKind::XLine { base, direction } => {
self.sub(if matches!(kind, EntityKind::Ray { .. }) { "AcDbRay" } else { "AcDbXline" });
self.p(10, *base);
@@ -884,6 +915,7 @@ fn kind_name(kind: &EntityKind) -> Option<&'static str> {
EntityKind::Polyline(_) => "POLYLINE",
EntityKind::Spline(_) => "SPLINE",
EntityKind::Helix(_) => "HELIX",
EntityKind::MLine(_) => "MLINE",
EntityKind::Ray { .. } => "RAY",
EntityKind::XLine { .. } => "XLINE",
EntityKind::Text(_) => "TEXT",
+43
View File
@@ -391,3 +391,46 @@ fn a_spline_edge_without_fit_data_leaves_the_loops_source_count() {
matches!(&h.loops[0].path, HatchPath::Edges(e) if matches!(&e[0], HatchEdge::Spline { fit, control, .. } if fit.is_empty() && control.len() == 2))
);
}
#[test]
fn an_attributes_vertical_alignment_is_group_74_not_the_field_length() {
// 73 is the field length of an ATTRIB or ATTDEF; a writer that emits it must not turn a
// middle-aligned attribute into another alignment.
let EntityKind::AttDef(a) = one("0|ATTDEF|10|1.0|1|x|2|TAG|70|0|73|8|74|2").kind else { panic!("an attribute definition") };
assert_eq!(a.text.valign, 2);
let EntityKind::AttDef(a) = one("0|ATTDEF|10|1.0|1|x|2|TAG|70|0|73|3").kind else { panic!("an attribute definition") };
assert_eq!(a.text.valign, 0, "a field length alone is no alignment");
let EntityKind::Text(t) = one("0|TEXT|10|1.0|1|x|73|3|74|1").kind else { panic!("text") };
assert_eq!(t.valign, 3, "TEXT has its alignment in 73");
}
#[test]
fn entity_groups_written_after_the_entitys_own_marker() {
// An AcDbEntity subclass without a layer: the run of entity codes after the LINE's own marker
// is the entity's.
let c = one("0|LINE|5|2A|100|AcDbEntity|100|AcDbLine|8|Walls|62|1|370|50|10|1.0|20|2.0|48|9.0").common;
assert_eq!((c.layer.as_str(), c.color, c.lineweight), ("Walls", Color::Index(1), Lineweight::Hundredths(50)));
assert_eq!(c.ltscale, 1.0, "the run ends at the entity's own first group");
// With a layer in AcDbEntity, nothing after the entity's marker is taken.
let c = one("0|MTEXT|100|AcDbEntity|8|A|100|AcDbMText|62|3|48|7.0").common;
assert_eq!((c.layer.as_str(), c.color, c.ltscale), ("A", Color::ByLayer, 1.0));
}
#[test]
fn an_attributes_justification_is_not_its_multiline_header() {
// A left-justified multiline attribute writes no 72 of its own; the 72 and 11 of its
// AcDbAttribute subclass are the multiline header.
let EntityKind::AttDef(a) = one(
"0|ATTDEF|100|AcDbText|10|1.0|20|2.0|40|2.5|1|x|100|AcDbAttributeDefinition|280|0|3|p|2|TAG|70|0|74|0|280|0|71|4|72|1|11|9.0|21|9.0|31|0.0|101|Embedded Object|10|1.0|1|x",
)
.kind
else {
panic!("an attribute definition")
};
assert_eq!((a.text.halign, a.text.valign, a.text.align, a.text.generation), (0, 0, None, 0));
// The text's own justification still counts.
let EntityKind::AttDef(a) = one("0|ATTDEF|100|AcDbText|10|1.0|1|x|72|2|11|5.0|100|AcDbAttributeDefinition|2|TAG|74|3").kind else {
panic!("an attribute definition")
};
assert_eq!((a.text.halign, a.text.valign, a.text.align), (2, 3, Some([5.0, 0.0, 0.0])));
}
+19
View File
@@ -133,3 +133,22 @@ fn helix() {
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));
}
#[test]
fn mline() {
let t = tags("0|SECTION|2|ENTITIES|0|MLINE|100|AcDbEntity|8|0|100|AcDbMline|2|WALL|340|41|40|2.0|70|1|71|3|72|2|73|2|10|0.0|20|0.0|30|0.0|210|0.0|220|0.0|230|1.0
11|0.0|21|0.0|31|0.0|12|1.0|22|0.0|32|0.0|13|0.0|23|1.0|33|0.0|74|3|41|0.5|41|1.0|41|2.0|75|0|74|1|41|-0.5|75|1|42|7.0
11|4.0|21|0.0|31|0.0|12|1.0|22|0.0|32|0.0|13|0.0|23|1.0|33|0.0|74|1|41|0.5|75|0|74|1|41|-0.5|75|0|0|ENDSEC|0|EOF");
let d = Document::read(&t);
let EntityKind::MLine(m) = Entity::read(&d.entities[0]).kind else { panic!("a multiline") };
assert_eq!(
(m.style.as_str(), m.style_handle, m.scale, m.justification, m.flags, m.elements, m.start),
("WALL", Some(0x41), 2.0, 1, 3, 2, [0.0; 3])
);
assert_eq!(m.vertices.len(), 2);
let v = &m.vertices[0];
assert_eq!((v.location, v.direction, v.miter), ([0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]));
assert_eq!(v.element_params, [vec![0.5, 1.0, 2.0], vec![-0.5]]);
assert_eq!(v.fill_params, [vec![], vec![7.0]], "42s go to the list their 75 started");
assert_eq!(m.vertices[1].location, [4.0, 0.0, 0.0]);
}
+30 -5
View File
@@ -6,8 +6,8 @@ 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, PatternLine, Polyline, Spline, Text, TextStyle, Transparency, Vertex,
Viewport, records,
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};
@@ -257,6 +257,31 @@ fn sample(full: bool) -> Vec<Entity> {
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"))),
@@ -394,8 +419,8 @@ fn owners_are_the_space_the_entity_is_in() {
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[18].kind else { panic!("an insert") };
assert_eq!(i.attribs[0].0.owner, back[18].common.handle, "an attribute is owned by its insert");
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");
}
@@ -613,7 +638,7 @@ fn the_file_around_the_content() {
#[test]
fn one_entity_for_a_file_the_caller_writes() {
let insert = sample(true).remove(18);
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);