craft-dxf: renumber the handles of records kept from another file (#997)

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

* craft-dxf: MULTILEADER and MLEADERSTYLE, read

model::mleader reads a MULTILEADER by level (the entity's groups, its
CONTEXT_DATA block, its leader roots and their leader lines, which reuse the
same codes) into MLeader, keeping the entity's and the context's copies of a
value apart, and MLEADERSTYLE into MLeaderStyle. Roots, lines, vertices and
attribute values are capped as CADCraft caps them. The writer leaves a
multileader out and counts it, as it does any record it has no groups for.

From CADCraft's cadcraft-io dxf_mleader.rs parse (5a6aa9b6).

* craft-dxf: ACAD_TABLE and TABLESTYLE, read

model::table reads an ACAD_TABLE's size, row heights, column widths and its
cells (text, merge span, merged flag, text height outside the value block,
alignment, text style, block) into Table, and a TABLESTYLE's margins,
suppressed rows and its data, header and title cell types with their six
borders into TableStyle. A size past 10,000 a side or 1,000,000 cells, or
with no rows or columns, keeps nothing, and only cells the file holds are
kept. The writer leaves a table out and counts it.

From CADCraft's cadcraft-io dxf_read/entities.rs acad_table and
dxf_table.rs read_table_style (5a6aa9b6).

* craft-dxf: renumber the handles of records kept from another file

model::write::remap_handles rewrites a run of records' own handles and every
pointer group (is_pointer_code: 320-369, 390-399, 480-481, 1005) through a
map, writing the null handle for anything the map drops, so records a caller
keeps from a file can go into raw_entities and raw_objects without colliding
with what the writer numbers.

From CADCraft's cadcraft-io dxf_raw.rs (5a6aa9b6), which renumbers what it
keeps this way.

---------

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 22:08:25 -05:00
1 parent 5f3d3d0326
commit b15cd61ab6
2 files changed
+83

No files matched your search

+37
View File
@@ -1600,3 +1600,40 @@ fn block_end(w: &mut W, h: u64, record: u64) {
w.s(8, "0");
w.sub("AcDbBlockEnd");
}
/// Group codes whose values are handles of other objects (DXF Reference, group code ranges):
/// soft and hard pointers and owners (320–369), more of them (390–399), 480 and 481, and the
/// extended data handle (1005).
pub fn is_pointer_code(code: i32) -> bool {
matches!(code, 320..=369 | 390..=399 | 480..=481 | 1005)
}
/// The tags of records kept from another file, for `raw_entities` or `raw_objects`, with every
/// handle renumbered by `map`: each record's own handle (group 5, or 105 of a DIMSTYLE) and
/// every pointer ([`is_pointer_code`]). A handle `map` gives nothing for is written as the null
/// handle `0`, so a kept record never points at an object the new file gives another meaning;
/// a record's own handle that `map` drops keeps its group with `0`. Groups inside an
/// application's own `102 {` ... `102 }` group are pointers too (reactors, extension
/// dictionaries) and are renumbered the same way. The null handle stays null, and text that is
/// no handle is left as it is.
///
/// From CADCraft's `cadcraft-io` `dxf_raw.rs` (5a6aa9b6), which renumbers what it keeps this way.
pub fn remap_handles(tags: &[Tag], map: impl Fn(u64) -> Option<u64>) -> Vec<Tag> {
let mut kind = "";
tags.iter()
.map(|t| {
if t.code == 0 {
kind = t.str_ref().map_or("", str::trim);
return t.clone();
}
let own = t.code == 5 || (t.code == 105 && kind == "DIMSTYLE");
if !own && !is_pointer_code(t.code) {
return t.clone();
}
match super::parse_handle(&t.str()) {
Some(0) | None => t.clone(),
Some(h) => Tag::s(t.code, format!("{:X}", map(h).unwrap_or(0))),
}
})
.collect()
}
+46
View File
@@ -650,3 +650,49 @@ fn one_entity_for_a_file_the_caller_writes() {
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)));
}