mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 00:42:46 -06:00
* DXF: link dimension sources to their associations (#2221) Source objects lacked the persistent reactor back to their DIMASSOC. Register validated source and intersection references once through the existing reactor list, keeping a self-link from duplicating the dimension's own reactor. Synthetic regressions cover missing links, repeated references and self-links. * Parity checklist: regenerated for dimension reactor gate * Parity checklist: regenerated after merging main * DXF: a dimension source keeps its other reactors next to the DIMASSOC (review on #2589) The DIMASSOC reactor made the writer emit an ACAD_REACTORS group, so the raw passthrough skipped the source's retained foreign reactors. They are now merged, deduplicated, into that one group. Test: a line reactor-linked to a kept object keeps both links after an associative dimension is added. * DXF: an unmodelled DIMASSOC source gets the DIMASSOC reactor (review on #2589) Unknown entities are written by the raw passthrough, which never saw the planned reactors. The map is now passed to dxf_raw::finish and merged into the entity's reactor group. Test: an unknown entity with a dimension snapped to it names the DIMASSOC. * Docs: leave the file-format parity document to maintainers (review on #2589) * DXF: pass the unknown-entity reactors as one argument (review on #2589) * DXF: collapse the extension dictionary condition (review on #2589) * Test: pin the reactor merge for dimension sources (review on #2589) * DXF: a kept extension dictionary follows the reactors group (review on #2589) When a written record already had its reactors group, the kept extension dictionary was inserted right after the handle, ahead of it; the DXF reference puts the reactors first. The reactor tests now find each group by name, so either order can no longer pass. * docs: leave the generated parity checklist to maintainers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
f0b46e34dc
commit
ccbc4fa854
+45
-15
@@ -846,7 +846,7 @@ fn unknown_handles(d: &Drawing) -> HashSet<String> {
|
||||
}
|
||||
|
||||
/// Put the drawing's foreign data back into a written file's tags (see the module comment).
|
||||
pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing) {
|
||||
pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing, reactors: &HashMap<Handle, Vec<String>>) {
|
||||
let f = d.foreign.as_ref();
|
||||
let unknown = unknown_handles(d);
|
||||
let xd: XdataOf = all_entities(d).filter(|e| !e.common.xdata.is_empty()).map(|e| (e.handle, e.common.xdata.as_slice())).collect();
|
||||
@@ -993,8 +993,10 @@ pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing) {
|
||||
let (Some(zero), Some(ub)) = (t.first(), t.get(1..)) else { continue };
|
||||
let ur = Rec { sec: sec.clone(), kind: zero.str(), start: 0, end: 0 };
|
||||
let x = head(ub).handle.and_then(|h| handle(&h)).and_then(|h| entity_extra(f, &xd, h));
|
||||
// Reactors the writer planned for this source (a DIMASSOC, an associative hatch).
|
||||
let extra = head(ub).handle.and_then(|h| handle(&h)).and_then(|h| reactors.get(&h)).map_or(&[][..], Vec::as_slice);
|
||||
out.push(zero.clone());
|
||||
record(&map, &ur, ub, x.as_deref(), true, false, &mut out);
|
||||
record(&map, &ur, ub, x.as_deref(), Some(extra), false, &mut out);
|
||||
}
|
||||
out.extend(tags.get(r.start).cloned());
|
||||
let x = extra_for(f, &xd, r, b, &block);
|
||||
@@ -1002,7 +1004,7 @@ pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing) {
|
||||
out.extend(b.iter().cloned());
|
||||
continue;
|
||||
}
|
||||
record(&map, r, b, x.as_deref(), false, Some(r.start) == root, &mut out);
|
||||
record(&map, r, b, x.as_deref(), None, Some(r.start) == root, &mut out);
|
||||
}
|
||||
add_appids(&mut out);
|
||||
let top = max_handle(&out).saturating_add(1);
|
||||
@@ -1076,10 +1078,23 @@ fn plan_unknown(d: &Drawing, recs: &[Rec], tags: &[Tag], roles: &HashMap<Foreign
|
||||
out
|
||||
}
|
||||
|
||||
/// The reactor handles a written record gains: the kept foreign ones (remapped) unless the record
|
||||
/// is an unknown entity that already carries its own, then `extra` (handles of the written file,
|
||||
/// such as a DIMASSOC), without repeating any the record already names. They go into the one
|
||||
/// `{ACAD_REACTORS` group the record has, or a new one, so no reactor is lost to a second group.
|
||||
fn reactors_to_add(map: &Map, h: &Head, x: Option<&ForeignExtra>, unknown: Option<&[String]>) -> Vec<String> {
|
||||
let extra = unknown.unwrap_or_default();
|
||||
let mut have: HashSet<String> = h.reactors.iter().cloned().collect();
|
||||
let kept = x.filter(|_| !(unknown.is_some() && h.has_reactors)).into_iter().flat_map(|x| x.reactors.iter()).filter_map(|r| map.get(&r.hex()));
|
||||
kept.chain(extra.iter().cloned()).filter(|r| r != "0" && have.insert(norm(r))).collect()
|
||||
}
|
||||
|
||||
/// A written record with its kept extras, remapped references (unknown entities) and kept
|
||||
/// dictionary entries (the named object dictionary).
|
||||
fn record(map: &Map, r: &Rec, b: &[Tag], x: Option<&ForeignExtra>, unknown: bool, root: bool, out: &mut Vec<Tag>) {
|
||||
fn record(map: &Map, r: &Rec, b: &[Tag], x: Option<&ForeignExtra>, unknown: Option<&[String]>, root: bool, out: &mut Vec<Tag>) {
|
||||
let h = head(b);
|
||||
let adds = reactors_to_add(map, &h, x, unknown);
|
||||
let mut in_reactors = false;
|
||||
let entity = matches!(r.sec.as_str(), "ENTITIES" | "BLOCKS") && r.kind != "BLOCK";
|
||||
let xdata_at = b.iter().position(|t| t.code == 1001).unwrap_or(b.len());
|
||||
let own_apps: HashSet<String> = b.iter().filter(|t| t.code == 1001).map(|t| up(&t.str())).collect();
|
||||
@@ -1096,6 +1111,12 @@ fn record(map: &Map, r: &Rec, b: &[Tag], x: Option<&ForeignExtra>, unknown: bool
|
||||
let rgb = b.iter().take(xdata_at).find(|t| t.code == 420).map(Tag::str);
|
||||
let has_book = b.iter().take(xdata_at).any(|t| t.code == 430);
|
||||
let handle_at = b.iter().position(|t| matches!(t.code, 5 | 105));
|
||||
// A kept extension dictionary follows the reactors group, as the DXF reference orders them.
|
||||
let reactors_end = b.iter().position(|t| t.code == 102 && t.str().trim() == "{ACAD_REACTORS").and_then(|s| {
|
||||
let rest = b.get(s + 1..)?;
|
||||
rest.iter().position(|t| t.code == 102 && t.str().trim() == "}").map(|e| s + 1 + e)
|
||||
});
|
||||
let xdict_at = if h.has_reactors { reactors_end.or(handle_at) } else { handle_at };
|
||||
let mut seen_owner = false;
|
||||
for (i, t) in b.iter().enumerate() {
|
||||
if i == props_at
|
||||
@@ -1114,8 +1135,18 @@ fn record(map: &Map, r: &Rec, b: &[Tag], x: Option<&ForeignExtra>, unknown: bool
|
||||
{
|
||||
xdata(map, x, &own_apps, out);
|
||||
}
|
||||
if t.code == 102 {
|
||||
match t.str().trim() {
|
||||
"{ACAD_REACTORS" => in_reactors = true,
|
||||
"}" if in_reactors => {
|
||||
in_reactors = false;
|
||||
out.extend(adds.iter().map(|a| Tag::s(330, a.clone())));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut t = t.clone();
|
||||
if unknown && i < xdata_at && is_pointer(t.code) && t.code != 102 {
|
||||
if unknown.is_some() && i < xdata_at && is_pointer(t.code) && t.code != 102 {
|
||||
if t.code == 330 && !seen_owner {
|
||||
seen_owner = true;
|
||||
} else {
|
||||
@@ -1123,18 +1154,17 @@ fn record(map: &Map, r: &Rec, b: &[Tag], x: Option<&ForeignExtra>, unknown: bool
|
||||
}
|
||||
}
|
||||
out.push(t);
|
||||
if Some(i) == handle_at
|
||||
if Some(i) == handle_at && !h.has_reactors && !adds.is_empty() {
|
||||
out.push(Tag::s(102, "{ACAD_REACTORS"));
|
||||
out.extend(adds.iter().map(|a| Tag::s(330, a.clone())));
|
||||
out.push(Tag::s(102, "}"));
|
||||
}
|
||||
if Some(i) == xdict_at
|
||||
&& let Some(x) = x
|
||||
&& h.xdict.is_none()
|
||||
&& let Some(xd) = x.xdict
|
||||
{
|
||||
if !h.has_reactors {
|
||||
let rs: Vec<String> = x.reactors.iter().map(|r| r.hex()).collect();
|
||||
map.handle_group(out, "ACAD_REACTORS", 330, &rs);
|
||||
}
|
||||
if h.xdict.is_none()
|
||||
&& let Some(xd) = x.xdict
|
||||
{
|
||||
map.handle_group(out, "ACAD_XDICTIONARY", 360, &[xd.hex()]);
|
||||
}
|
||||
map.handle_group(out, "ACAD_XDICTIONARY", 360, &[xd.hex()]);
|
||||
}
|
||||
}
|
||||
if let Some(x) = x {
|
||||
|
||||
@@ -43,8 +43,8 @@ struct W {
|
||||
pstyles: HashMap<String, String>,
|
||||
/// Upper-case layer name → its LAYER table record handle (group 331/341 of viewports).
|
||||
layers: HashMap<String, String>,
|
||||
/// Persistent reactors of boundary objects: each source object of an associative hatch →
|
||||
/// the hatches (groups `{ACAD_REACTORS 330 …}`), from [`hatch_reactors`].
|
||||
/// Persistent reactors of source geometry: the associative hatches and DIMASSOC objects
|
||||
/// that follow it (groups `{ACAD_REACTORS 330 …}`).
|
||||
reactors: HashMap<Handle, Vec<String>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -874,7 +874,20 @@ fn plan_dimension_associations(d: &Drawing, all_spaces: &[&EntityStore], w: &mut
|
||||
if let EntityKind::Dimension(dm) = &e.kind {
|
||||
let (flags, refs) = std_assoc_refs(d, dm);
|
||||
if !refs.is_empty() {
|
||||
cx.assoc.insert(e.handle, AssocObj { xdict: w.h(), handle: w.h(), flags, refs });
|
||||
let assoc = AssocObj { xdict: w.h(), handle: w.h(), flags, refs };
|
||||
// Source geometry must name the DIMASSOC as well, or other readers do not
|
||||
// follow later edits. Both objects of an intersection need the reactor.
|
||||
for source in assoc.refs.iter().flat_map(|r| std::iter::once(r.main).chain(r.other)) {
|
||||
// common_x already gives the dimension its own DIMASSOC reactor.
|
||||
if source == e.handle {
|
||||
continue;
|
||||
}
|
||||
let reactors = w.reactors.entry(source).or_default();
|
||||
if !reactors.contains(&assoc.handle) {
|
||||
reactors.push(assoc.handle.clone());
|
||||
}
|
||||
}
|
||||
cx.assoc.insert(e.handle, assoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1117,7 +1130,7 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version, frozen_34
|
||||
if let Some(t) = w.t.get_mut(seed_pos + 1) {
|
||||
*t = Tag::s(5, seed);
|
||||
}
|
||||
crate::dxf_raw::finish(&mut w.t, d);
|
||||
crate::dxf_raw::finish(&mut w.t, d, &w.reactors);
|
||||
crate::dxf_raw::data_store(&mut w.t, d, target >= Version::R2013);
|
||||
add_class_instance_counts(&mut w.t, target);
|
||||
mark_xrefs(&mut w.t, d);
|
||||
|
||||
@@ -288,3 +288,50 @@ fn standard_dimassoc_associates_in_dxf_and_dwg() {
|
||||
let back = read(&dwg, "case.dwg").unwrap();
|
||||
assert_standard_assoc(&c, &back, "DWG");
|
||||
}
|
||||
|
||||
/// Every source object names each DIMASSOC that follows it once, before its owner group.
|
||||
#[test]
|
||||
fn dimension_sources_carry_their_persistent_reactors() {
|
||||
let c = case();
|
||||
let text = write_dxf(&c.d);
|
||||
let all = records(&text);
|
||||
for dimension in [c.hd, c.hr, c.ha, c.hl] {
|
||||
let assoc = dimassoc(&text, dimension).expect("DIMASSOC");
|
||||
let reactor = string(&assoc, 5).expect("DIMASSOC handle");
|
||||
for reference in point_refs(&assoc) {
|
||||
// Both objects of an intersection need the link back. Repeated snaps to one
|
||||
// object must not write the same DIMASSOC reactor twice.
|
||||
for source in [string(&reference, 331), string(&reference, 332)].into_iter().flatten() {
|
||||
let geometry = all.iter().find(|r| string(r, 5) == Some(source)).expect("referenced geometry");
|
||||
let start = geometry.iter().position(|(code, value)| *code == 102 && value == "{ACAD_REACTORS").expect("source reactor group");
|
||||
let end =
|
||||
start + 1 + geometry.iter().skip(start + 1).position(|(code, value)| *code == 102 && value == "}").expect("reactor group end");
|
||||
assert_eq!(geometry[start + 1..end].iter().filter(|(code, value)| *code == 330 && value == reactor).count(), 1);
|
||||
assert_eq!(start, 2, "reactors immediately follow the entity handle");
|
||||
assert_eq!(geometry.get(end + 1).map(|(code, _)| *code), Some(330), "entity owner follows reactors");
|
||||
assert_eq!(geometry.iter().filter(|(code, value)| *code == 102 && value == "{ACAD_REACTORS").count(), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Malformed self-links must not duplicate the reactor the dimension already carries.
|
||||
#[test]
|
||||
fn a_self_referential_dimension_writes_its_reactor_once() {
|
||||
let mut c = case();
|
||||
assert!(c.d.model.modify(c.hl, |e| {
|
||||
let EntityKind::Dimension(dm) = &mut e.kind else { panic!("dimension") };
|
||||
for reference in &mut dm.assoc {
|
||||
reference.handle = c.hl;
|
||||
}
|
||||
}));
|
||||
let text = write_dxf(&c.d);
|
||||
let assoc = dimassoc(&text, c.hl).expect("DIMASSOC");
|
||||
let reactor = string(&assoc, 5).expect("DIMASSOC handle");
|
||||
let all = records(&text);
|
||||
let handle = c.hl.hex();
|
||||
let dimension = all.iter().find(|r| string(r, 5) == Some(&handle)).expect("dimension record");
|
||||
let start = dimension.iter().position(|(code, value)| *code == 102 && value == "{ACAD_REACTORS").expect("reactor group");
|
||||
let end = start + 1 + dimension.iter().skip(start + 1).position(|(code, value)| *code == 102 && value == "}").expect("reactor group end");
|
||||
assert_eq!(dimension[start + 1..end].iter().filter(|(code, value)| *code == 330 && value == reactor).count(), 1);
|
||||
}
|
||||
|
||||
@@ -1010,3 +1010,182 @@ fn dwg_keeps_extension_dictionaries_xrecords_and_entity_xdata() {
|
||||
assert_eq!(d.block("DOOR").map(|b| b.description.as_str()), Some("A door leaf"));
|
||||
assert!(d.ucss.iter().any(|u| u.name == "TILTED"));
|
||||
}
|
||||
|
||||
/// A linear dimension whose two extension origins are snapped to `a` and `b`, so the saved file
|
||||
/// carries a DIMASSOC naming both objects.
|
||||
fn add_associated_dimension(d: &mut Drawing, a: cadcraft_doc::Handle, b: cadcraft_doc::Handle) {
|
||||
use cadcraft_doc::{AssocSnap, Common, DimAssoc, DimKind, Dimension, Space};
|
||||
use cadcraft_geom::Vec3;
|
||||
let at = |point: &str, handle| DimAssoc { point: point.into(), handle, snap: AssocSnap::Mid };
|
||||
let dm = Dimension {
|
||||
kind: DimKind::Linear { rotation: 0.0 },
|
||||
defpt: Vec3::new(5.0, -3.0, 0.0),
|
||||
text_mid: Vec3::ZERO,
|
||||
p13: Vec3::ZERO,
|
||||
p14: Vec3::new(10.0, 0.0, 0.0),
|
||||
p15: Vec3::ZERO,
|
||||
p16: Vec3::ZERO,
|
||||
text: String::new(),
|
||||
style: "Standard".into(),
|
||||
measurement: 0.0,
|
||||
text_rotation: 0.0,
|
||||
user_text_pos: false,
|
||||
block: None,
|
||||
overrides: Default::default(),
|
||||
assoc: vec![at("p13", a), at("p14", b)],
|
||||
marks: Default::default(),
|
||||
};
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Dimension(dm)).expect("add dimension");
|
||||
}
|
||||
|
||||
/// The handle of the one DIMASSOC object of a written file.
|
||||
fn dimassoc_handle(o: &Out) -> String {
|
||||
let assocs = o.of("DIMASSOC");
|
||||
assert_eq!(assocs.len(), 1, "one DIMASSOC");
|
||||
h(assocs.first().copied().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// A source object that already has a reactor to a kept foreign object (the fixture's line 2A
|
||||
/// names ACME_OBJECT 42) keeps it when a DIMASSOC reactor is added: one group with both (review on #2589).
|
||||
#[test]
|
||||
fn a_dimension_source_keeps_its_foreign_reactors_next_to_the_dimassoc() {
|
||||
let mut d = read_dxf(fixture().as_bytes()).expect("read fixture");
|
||||
add_associated_dimension(&mut d, cadcraft_doc::Handle(0x2A), cadcraft_doc::Handle(0x2A));
|
||||
let o = Out::new(&write_dxf(&d));
|
||||
let line = o.of("LINE").into_iter().find(|t| h(t).eq_ignore_ascii_case("2A")).expect("line 2A");
|
||||
assert_eq!(line.iter().filter(|t| t.code == 102 && t.str() == "{ACAD_REACTORS").count(), 1, "one reactor group");
|
||||
let reactors = group(line, "ACAD_REACTORS");
|
||||
assert!(reactors.contains(&dimassoc_handle(&o)), "DIMASSOC reactor: {reactors:?}");
|
||||
assert!(reactors.iter().any(|r| o.handle(r).is_some_and(|(k, _)| k == "ACME_OBJECT")), "foreign reactor kept: {reactors:?}");
|
||||
assert!(o.dangling().is_empty(), "{:?}", o.dangling());
|
||||
}
|
||||
/// An unmodelled entity a DIMASSOC refers to gets the reactor too, next to what it had (review on #2589).
|
||||
#[test]
|
||||
fn an_unknown_dimension_source_carries_the_dimassoc_reactor() {
|
||||
let mut d = read_dxf(fixture().as_bytes()).expect("read fixture");
|
||||
add_associated_dimension(&mut d, cadcraft_doc::Handle(0x30), cadcraft_doc::Handle(0x2A));
|
||||
let o = Out::new(&write_dxf(&d));
|
||||
let widget = entity_by_text(&o, "ACME_WIDGET", "widget-in-model");
|
||||
let reactors = group(widget, "ACAD_REACTORS");
|
||||
assert!(reactors.contains(&dimassoc_handle(&o)), "DIMASSOC reactor: {reactors:?}");
|
||||
assert_eq!(widget.iter().filter(|t| t.code == 102 && t.str() == "{ACAD_REACTORS").count(), 1);
|
||||
assert_eq!(owner(widget), h(o.named("BLOCK_RECORD", "*Model_Space")), "owner unchanged");
|
||||
assert!(o.dangling().is_empty(), "{:?}", o.dangling());
|
||||
}
|
||||
|
||||
/// The handles of the `{ACAD_REACTORS` group of the record of handle `at`, after checking the
|
||||
/// written text holds exactly one such group, placed as the DXF reference orders a record:
|
||||
/// handle (5), then the reactors, then the owner (330), then the first subclass marker (100) and
|
||||
/// any xdata (1001). Each handle appears once and names a DIMASSOC when `assoc_only` is set.
|
||||
fn one_reactor_group(o: &Out, at: &str, assoc_only: bool) -> Vec<String> {
|
||||
let (_, rec) = o.handle(at).expect("source record");
|
||||
let codes: Vec<(i32, String)> = rec.iter().map(|t| (t.code, t.str())).collect();
|
||||
let starts: Vec<usize> = (0..codes.len()).filter(|&i| codes[i].0 == 102 && codes[i].1 == "{ACAD_REACTORS").collect();
|
||||
assert_eq!(starts.len(), 1, "exactly one reactor group in {codes:?}");
|
||||
let start = starts[0];
|
||||
let end = start + codes[start..].iter().position(|c| c.0 == 102 && c.1 == "}").expect("group end");
|
||||
let handles: Vec<String> = codes[start + 1..end].iter().map(|c| c.1.clone()).collect();
|
||||
assert!(codes[start + 1..end].iter().all(|c| c.0 == 330), "only 330 groups inside: {codes:?}");
|
||||
let unique: std::collections::HashSet<String> = handles.iter().map(|r| r.to_ascii_uppercase()).collect();
|
||||
assert_eq!(unique.len(), handles.len(), "each reactor once: {handles:?}");
|
||||
let pos = |code: i32| codes.iter().position(|c| c.0 == code);
|
||||
assert_eq!(pos(5), Some(0), "handle first");
|
||||
let owner_at = end + 1 + codes[end + 1..].iter().position(|c| c.0 == 330).expect("owner");
|
||||
assert!(owner_at > end, "owner after the reactors: {codes:?}");
|
||||
assert!(end < pos(100).expect("subclass marker"), "reactors before the first subclass marker");
|
||||
if let Some(x) = pos(1001) {
|
||||
assert!(end < x, "reactors before the xdata");
|
||||
}
|
||||
if assoc_only {
|
||||
for r in &handles {
|
||||
assert_eq!(o.handle(r).map(|(k, _)| k), Some("DIMASSOC"), "{r} resolves to a DIMASSOC");
|
||||
}
|
||||
}
|
||||
handles
|
||||
}
|
||||
|
||||
/// A record that already has a reactors group (line 2A names ACME_OBJECT 42) comes out with one
|
||||
/// group holding the foreign handle and the DIMASSOC, each once, in the same order after a
|
||||
/// second save and with the extension dictionary group still between it and the owner.
|
||||
/// Pinning test for the merge (review on #2589).
|
||||
#[test]
|
||||
fn a_record_with_a_reactor_group_gets_one_group_with_every_handle_once() {
|
||||
let mut d = read_dxf(fixture().as_bytes()).expect("read fixture");
|
||||
add_associated_dimension(&mut d, cadcraft_doc::Handle(0x2A), cadcraft_doc::Handle(0x2A));
|
||||
let text = write_dxf(&d);
|
||||
let o = Out::new(&text);
|
||||
let reactors = one_reactor_group(&o, "2A", false);
|
||||
assert_eq!(reactors.len(), 2, "foreign + DIMASSOC: {reactors:?}");
|
||||
let assoc = dimassoc_handle(&o);
|
||||
assert_eq!(reactors.iter().filter(|r| r.eq_ignore_ascii_case(&assoc)).count(), 1);
|
||||
assert!(reactors.iter().any(|r| o.handle(r).is_some_and(|(k, _)| k == "ACME_OBJECT")));
|
||||
let (_, line) = o.handle("2A").expect("line");
|
||||
let order: Vec<i32> = line.iter().map(|t| t.code).collect();
|
||||
let start = |name: &str| line.iter().position(|t| t.code == 102 && t.str() == format!("{{{name}"));
|
||||
let (rx, xd) = (start("ACAD_REACTORS"), start("ACAD_XDICTIONARY"));
|
||||
assert!(rx.is_some() && xd.is_some() && rx < xd, "reactors group before the extension dictionary group: {order:?}");
|
||||
assert_eq!(group(line, "ACAD_XDICTIONARY").len(), 1);
|
||||
// Our reader accepts it, the association survives, and a second save keeps the same group.
|
||||
let back = read_dxf(text.as_bytes()).expect("reopen");
|
||||
let again = Out::new(&write_dxf(&back));
|
||||
assert_eq!(again.of("DIMASSOC").len(), 1);
|
||||
let second = one_reactor_group(&again, "2A", false);
|
||||
assert_eq!(second.len(), 2, "{second:?}");
|
||||
assert!(second.iter().any(|r| again.handle(r).is_some_and(|(k, _)| k == "DIMASSOC")));
|
||||
assert!(again.dangling().is_empty(), "{:?}", again.dangling());
|
||||
}
|
||||
|
||||
/// A record with no reactors group (line 2A with its group removed, keeping the extension
|
||||
/// dictionary and the xdata) gets a new one in the preamble, between the handle and the owner,
|
||||
/// so before the first subclass marker and the xdata. Pinning test (review on #2589).
|
||||
#[test]
|
||||
fn a_record_without_a_reactor_group_gets_a_new_one_in_the_preamble() {
|
||||
let bare = fixture().replacen("5\n2A\n102\n{ACAD_REACTORS\n330\n42\n102\n}\n", "5\n2A\n", 1);
|
||||
assert_ne!(bare, fixture(), "the fixture line lost its reactors");
|
||||
let mut d = read_dxf(bare.as_bytes()).expect("read fixture");
|
||||
add_associated_dimension(&mut d, cadcraft_doc::Handle(0x2A), cadcraft_doc::Handle(0x2A));
|
||||
let text = write_dxf(&d);
|
||||
let o = Out::new(&text);
|
||||
let (_, line) = o.handle("2A").expect("line");
|
||||
assert!(line.iter().any(|t| t.code == 1001), "the record keeps its xdata");
|
||||
let xdict_at = line.iter().position(|t| t.code == 102 && t.str() == "{ACAD_XDICTIONARY").expect("xdict group");
|
||||
let reactors = one_reactor_group(&o, "2A", true);
|
||||
assert_eq!(reactors, vec![dimassoc_handle(&o)]);
|
||||
// The preamble is the handle, then the reactors group and the extension dictionary group in
|
||||
// the DXF reference's order, then the owner: nothing else in between.
|
||||
let reactors_at = line.iter().position(|t| t.code == 102 && t.str() == "{ACAD_REACTORS").expect("reactor group");
|
||||
let owner_at = line.iter().position(|t| t.code == 330 && t.str() != reactors[0]).expect("owner");
|
||||
assert!(reactors_at < xdict_at && xdict_at < owner_at, "reactors {reactors_at}, xdict {xdict_at}, owner {owner_at}");
|
||||
assert!(line.iter().take(owner_at).all(|t| matches!(t.code, 5 | 102 | 330 | 360)), "only handle and groups before the owner");
|
||||
// Reading it back keeps the association and the DIMASSOC still resolves.
|
||||
let back = read_dxf(text.as_bytes()).expect("reopen");
|
||||
let again = Out::new(&write_dxf(&back));
|
||||
one_reactor_group(&again, "2A", true);
|
||||
assert!(again.dangling().is_empty(), "{:?}", again.dangling());
|
||||
}
|
||||
|
||||
/// A new line used by two dimensions (so by two DIMASSOC objects) lists both, once each, in one
|
||||
/// group that sits between its handle and its owner. Pinning test (review on #2589).
|
||||
#[test]
|
||||
fn a_source_used_by_two_associations_lists_both_dimassocs_once() {
|
||||
use cadcraft_doc::{Common, Line, Space};
|
||||
use cadcraft_geom::Vec3;
|
||||
let mut d = Drawing::new_metric();
|
||||
let line = d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(10.0, 0.0, 0.0) })).expect("add line");
|
||||
add_associated_dimension(&mut d, line, line);
|
||||
add_associated_dimension(&mut d, line, line);
|
||||
let text = write_dxf(&d);
|
||||
let o = Out::new(&text);
|
||||
let assocs: Vec<String> = o.of("DIMASSOC").into_iter().map(h).collect();
|
||||
assert_eq!(assocs.len(), 2, "one DIMASSOC per dimension");
|
||||
let reactors = one_reactor_group(&o, &line.hex(), true);
|
||||
let mut got: Vec<String> = reactors.iter().map(|r| r.to_ascii_uppercase()).collect();
|
||||
let mut want: Vec<String> = assocs.iter().map(|r| r.to_ascii_uppercase()).collect();
|
||||
got.sort();
|
||||
want.sort();
|
||||
assert_eq!(got, want, "both associations, once each");
|
||||
let back = read_dxf(text.as_bytes()).expect("reopen");
|
||||
let again = Out::new(&write_dxf(&back));
|
||||
assert_eq!(one_reactor_group(&again, &line.hex(), true).len(), 2);
|
||||
assert!(again.dangling().is_empty(), "{:?}", again.dangling());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user