diff --git a/crates/dxf/src/model/write.rs b/crates/dxf/src/model/write.rs index 8a33731d2..42ee1d155 100644 --- a/crates/dxf/src/model/write.rs +++ b/crates/dxf/src/model/write.rs @@ -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) -> Vec { + 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() +} diff --git a/crates/dxf/tests/model_write.rs b/crates/dxf/tests/model_write.rs index 567e3528a..1f1df5fcb 100644 --- a/crates/dxf/tests/model_write.rs +++ b/crates/dxf/tests/model_write.rs @@ -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))); +}