mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 04:19:59 -06:00
- #2584: a tilted LWPOLYLINE/HATCH run is flattened to at most a million vertices, then its arcs stay chords (a hostile file of arcs asked for gigabytes; austin62jake). The file-format-parity.md hunk is dropped: maintainers update the parity docs. - #2471: another program's proxy objects are in the DWG loss report as its proxy entities are (left out before 2013, or when their class can't be named); they went silently. - #1626: a drawing saved at a view keeps it in the desktop app and when it has xrefs: the app's first frame and the xref pass both fitted the extents over it. OPEN's result says `saved_view`; one `view::saved_view` replaces the copy of `usable_view` in files.rs. A misplaced doc comment goes back to its test. - #2028: a plot rotation (73) outside 0-3 is written back as 0, not as read. - #1026: the binary code-page retry drops the first pass's tags before it runs (the peak doubled near the tag limit); a float spelling of an integer group clamps to what the width holds unsigned too, so 65535.0 reads as 65535 does, not 32767. - #2658: the reference names Undo by the last command ("Undo Group of commands"); Undo matched nothing and went to the end of Edit. Ordering and its test share one rule. - #2612: its test's Insert gets `clip: None` (XCLIP landed after it was written).
305 lines
16 KiB
Rust
305 lines
16 KiB
Rust
//! Saving a drawing with entities of types CADCraft doesn't model (read from another program's
|
|
//! file) as DWG says which ones the file doesn't keep, instead of dropping them silently. DXF
|
|
//! writes them back as read: nothing to report.
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::cmd::file::{IoHooks, SaveAs, set_io, set_write_as};
|
|
use cadcraft_engine::doc::{Block, Common, Drawing, EntityKind, RawTag, Space, Unknown};
|
|
use serde_json::json;
|
|
|
|
fn unknown(kind: &str) -> EntityKind {
|
|
EntityKind::Unknown(Unknown { dxf_type: kind.into(), tags: vec![RawTag { code: 8, value: "0".into() }] })
|
|
}
|
|
|
|
#[test]
|
|
fn save_lists_the_entities_the_file_does_not_keep() {
|
|
set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write: |_, _| Ok(b"0\nEOF\n".to_vec()), plot: None });
|
|
let mut d = Drawing::new_imperial();
|
|
for k in ["MLINE", "3DSOLID", "MLINE"] {
|
|
d.add(&Space::Model, Common::default(), unknown(k)).unwrap();
|
|
}
|
|
let mut b = Block::new("Part");
|
|
let h = d.new_handle();
|
|
b.entities.push(cadcraft_engine::doc::Entity { handle: h, common: Common::default(), kind: unknown("REGION") });
|
|
d.blocks.insert("Part".into(), std::sync::Arc::new(b));
|
|
let mut s = Session::empty();
|
|
s.open_drawing(d, "foreign.dxf", None);
|
|
|
|
let path = std::env::temp_dir().join(format!("cadcraft-not-saved-{}.dwg", std::process::id()));
|
|
let r = s.execute("saveas", &json!({ "path": path.to_string_lossy() })).unwrap();
|
|
let _ = std::fs::remove_file(&path);
|
|
assert_eq!(r["notSaved"], json!({ "3DSOLID": 1, "MLINE": 2, "REGION": 1 }));
|
|
let msg = "Not saved in the DWG file (its writer can't write these object types yet; DXF keeps them, solids only when saved as 2010 or earlier): 1 3DSOLID, 2 MLINE, 1 REGION";
|
|
assert!(s.log.iter().any(|l| l == msg), "{:?}", s.log);
|
|
|
|
// DXF keeps them.
|
|
let dxf = std::env::temp_dir().join(format!("cadcraft-not-saved-{}.dxf", std::process::id()));
|
|
let n = s.log.len();
|
|
let r = s.execute("saveas", &json!({ "path": dxf.to_string_lossy() })).unwrap();
|
|
let _ = std::fs::remove_file(&dxf);
|
|
assert!(r.get("notSaved").is_none() && s.log.len() == n);
|
|
|
|
// Exporting an image isn't a save of the drawing: no report.
|
|
let png = std::env::temp_dir().join(format!("cadcraft-not-saved-{}.png", std::process::id()));
|
|
let r = s.execute("saveas", &json!({ "path": png.to_string_lossy() })).unwrap();
|
|
let _ = std::fs::remove_file(&png);
|
|
assert!(r.get("notSaved").is_none());
|
|
|
|
// A drawing the file keeps completely: nothing to report.
|
|
s.open_drawing(Drawing::new_imperial(), "plain.dxf", None);
|
|
let n = s.log.len();
|
|
let r = s.execute("document.bytes", &json!({})).unwrap();
|
|
assert!(r.get("notSaved").is_none() && s.log.len() == n);
|
|
}
|
|
|
|
#[test]
|
|
fn export_and_wblock_say_what_a_dwg_leaves_out() {
|
|
set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write: |_, _| Ok(b"0\nEOF\n".to_vec()), plot: None });
|
|
let mut d = Drawing::new_imperial();
|
|
let h = d.add(&Space::Model, Common::default(), unknown("ACME_WIDGET")).unwrap().hex();
|
|
let mut s = Session::empty();
|
|
s.open_drawing(d, "foreign.dxf", None);
|
|
let file = |ext: &str| std::env::temp_dir().join(format!("cadcraft-leaves-out-{}.{ext}", std::process::id())).to_string_lossy().into_owned();
|
|
let said = |l: &String| l.starts_with("Not saved in the DWG file") && l.ends_with(": 1 ACME_WIDGET");
|
|
|
|
let (dwg, dxf) = (file("dwg"), file("dxf"));
|
|
for (cmd, p) in [("export", json!({ "path": dwg })), ("wblock", json!({ "path": dwg, "handles": [h] }))] {
|
|
s.log.clear();
|
|
let r = s.execute(cmd, &p).unwrap();
|
|
assert_eq!(r["notSaved"], json!({ "ACME_WIDGET": 1 }), "{cmd}");
|
|
assert!(s.log.iter().any(said), "{cmd}: {:?}", s.log);
|
|
}
|
|
// -WBLOCK takes the objects out of the drawing, so last.
|
|
let r = s.execute("-wblock", &json!({ "path": dwg, "handles": [h] })).unwrap();
|
|
assert_eq!(r["notSaved"], json!({ "ACME_WIDGET": 1 }));
|
|
let _ = std::fs::remove_file(&dwg);
|
|
|
|
// DXF keeps them.
|
|
s.execute("undo", &json!({})).unwrap();
|
|
for (cmd, p) in [("export", json!({ "path": dxf })), ("wblock", json!({ "path": dxf, "handles": [h] }))] {
|
|
s.log.clear();
|
|
assert!(s.execute(cmd, &p).unwrap().get("notSaved").is_none(), "{cmd}");
|
|
assert!(!s.log.iter().any(said), "{cmd}: {:?}", s.log);
|
|
}
|
|
let _ = std::fs::remove_file(&dxf);
|
|
}
|
|
|
|
#[test]
|
|
fn templates_report_what_dwg_does_not_keep() {
|
|
use cadcraft_engine::cmd::file::drops_unknown;
|
|
assert!(drops_unknown("a.dwg") && drops_unknown("B.DWT"));
|
|
assert!(!drops_unknown("a.dxf"));
|
|
}
|
|
|
|
#[test]
|
|
fn solids_whose_geometry_the_data_store_keeps_are_not_reported_from_2013() {
|
|
use cadcraft_engine::cmd::file::{Version, not_saved_as};
|
|
use cadcraft_engine::doc::Handle;
|
|
use cadcraft_engine::doc::foreign::Foreign;
|
|
let mut d = Drawing::new_imperial();
|
|
let kept = d.add(&Space::Model, Common::default(), unknown("3DSOLID")).unwrap();
|
|
d.add(&Space::Model, Common::default(), unknown("3DSOLID")).unwrap();
|
|
d.add(&Space::Model, Common::default(), unknown("REGION")).unwrap();
|
|
let record = |h: Handle| {
|
|
vec![
|
|
RawTag { code: 0, value: "ACDSRECORD".into() },
|
|
RawTag { code: 320, value: h.hex() },
|
|
RawTag { code: 2, value: "ASM_Data".into() },
|
|
RawTag { code: 310, value: "00".into() },
|
|
]
|
|
};
|
|
d.foreign = std::sync::Arc::new(Foreign { data_store: record(kept), ..Foreign::default() });
|
|
// The solid with a data store record keeps its geometry from 2013 on; the others don't.
|
|
assert_eq!(not_saved_as(&d, Version::R2018), [("3DSOLID".to_string(), 1), ("REGION".to_string(), 1)].into_iter().collect());
|
|
// Before 2013 a file has no data store: every solid loses its geometry.
|
|
assert_eq!(not_saved_as(&d, Version::R2010), [("3DSOLID".to_string(), 2), ("REGION".to_string(), 1)].into_iter().collect());
|
|
}
|
|
|
|
/// Another program's proxy entity reaches a 2013 or later DWG with the class its group 91 names;
|
|
/// before 2013, or when the file has no such class, the DWG leaves it out and says so.
|
|
#[test]
|
|
fn proxy_entities_are_reported_only_before_2013() {
|
|
use cadcraft_engine::cmd::file::{Version, not_saved_as};
|
|
use cadcraft_engine::doc::foreign::Foreign;
|
|
let proxy = |class: i64| {
|
|
EntityKind::Unknown(Unknown {
|
|
dxf_type: "ACAD_PROXY_ENTITY".into(),
|
|
tags: vec![
|
|
RawTag { code: 8, value: "0".into() },
|
|
RawTag { code: 90, value: "498".into() },
|
|
RawTag { code: 91, value: class.to_string() },
|
|
],
|
|
})
|
|
};
|
|
let mut d = Drawing::new_imperial();
|
|
d.add(&Space::Model, Common::default(), proxy(500)).unwrap();
|
|
d.add(&Space::Model, Common::default(), unknown("MLINE")).unwrap();
|
|
d.foreign = std::sync::Arc::new(Foreign { class_order: vec!["MYAPP_THING".into()], ..Foreign::default() });
|
|
for v in [Version::R2013, Version::R2018] {
|
|
assert_eq!(not_saved_as(&d, v), [("MLINE".to_string(), 1)].into_iter().collect(), "{v:?}");
|
|
}
|
|
for v in [Version::R2004, Version::R2010] {
|
|
assert_eq!(not_saved_as(&d, v), [("ACAD_PROXY_ENTITY".to_string(), 1), ("MLINE".to_string(), 1)].into_iter().collect(), "{v:?}");
|
|
}
|
|
// A class number the file's CLASSES don't have, or none at all: the DWG can't name the class,
|
|
// leaves the proxy out, and says so from 2013 on too.
|
|
d.add(&Space::Model, Common::default(), proxy(501)).unwrap();
|
|
d.add(&Space::Model, Common::default(), unknown("ACAD_PROXY_ENTITY")).unwrap();
|
|
assert_eq!(not_saved_as(&d, Version::R2018), [("ACAD_PROXY_ENTITY".to_string(), 2), ("MLINE".to_string(), 1)].into_iter().collect());
|
|
}
|
|
|
|
/// Another program's proxy objects are left out of a DWG the same way as its proxy entities, and
|
|
/// said so: before 2013 always, from 2013 when the file names no class for them (#2471 review).
|
|
#[test]
|
|
fn proxy_objects_are_reported_like_proxy_entities() {
|
|
use cadcraft_engine::cmd::file::{Version, not_saved_as};
|
|
use cadcraft_engine::doc::Handle;
|
|
use cadcraft_engine::doc::foreign::{Foreign, ForeignObject};
|
|
let object = |h: u64, class: i64| ForeignObject {
|
|
handle: Handle(h),
|
|
kind: "ACAD_PROXY_OBJECT".into(),
|
|
tags: vec![
|
|
RawTag { code: 5, value: format!("{h:X}") },
|
|
RawTag { code: 90, value: "499".into() },
|
|
RawTag { code: 91, value: class.to_string() },
|
|
],
|
|
};
|
|
let mut d = Drawing::new_imperial();
|
|
d.foreign = std::sync::Arc::new(Foreign {
|
|
class_order: vec!["MYAPP_DATA".into()],
|
|
objects: vec![object(0x500, 500), object(0x501, 501)],
|
|
..Foreign::default()
|
|
});
|
|
let lost = |v| not_saved_as(&d, v).get("ACAD_PROXY_OBJECT").copied().unwrap_or(0);
|
|
assert_eq!(lost(Version::R2004), 2, "before 2013 every proxy object is left out");
|
|
assert_eq!(lost(Version::R2010), 2);
|
|
assert_eq!(lost(Version::R2018), 1, "from 2013 only the one whose class the file doesn't name");
|
|
}
|
|
|
|
/// A DXF of 2013 or later can't hold a solid's SAT-text geometry (it lives in the data store
|
|
/// there), so the save leaves it out and says so; 2010 and earlier keep it (#2334).
|
|
#[test]
|
|
fn saving_sat_text_solids_as_dxf_2013_reports_them() {
|
|
set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write: |_, _| Ok(b"0\nEOF\n".to_vec()), plot: None });
|
|
set_write_as(|_, _, _: &SaveAs| Ok(b"x".to_vec()));
|
|
let mut d = Drawing::new_imperial();
|
|
let sat = EntityKind::Unknown(Unknown {
|
|
dxf_type: "REGION".into(),
|
|
tags: vec![
|
|
RawTag { code: 8, value: "0".into() },
|
|
RawTag { code: 1, value: "400 1 1 0".into() },
|
|
RawTag { code: 3, value: "End-of-ACIS-data".into() },
|
|
],
|
|
});
|
|
d.add(&Space::Model, Common::default(), sat).unwrap();
|
|
let mut s = Session::empty();
|
|
s.open_drawing(d, "region.dxf", None);
|
|
|
|
let path = std::env::temp_dir().join(format!("cadcraft-sat-note-{}.dxf", std::process::id()));
|
|
let r = s.execute("saveas", &json!({ "path": path.to_string_lossy(), "version": 2018 })).unwrap();
|
|
assert_eq!(r["notSaved"], json!({ "REGION": 1 }));
|
|
assert!(s.log.iter().any(|l| l.contains("Not saved in the DXF file") && l.contains("1 REGION")), "{:?}", s.log);
|
|
|
|
// QSAVE takes no version: a `version` passed to it doesn't change what it writes (the
|
|
// drawing's own 2018), so the note still comes.
|
|
let r = s.execute("qsave", &json!({ "path": path.to_string_lossy(), "version": 2010 })).unwrap();
|
|
assert_eq!(r["notSaved"], json!({ "REGION": 1 }));
|
|
|
|
let n = s.log.len();
|
|
let r = s.execute("saveas", &json!({ "path": path.to_string_lossy(), "version": 2010 })).unwrap();
|
|
let _ = std::fs::remove_file(&path);
|
|
assert!(r.get("notSaved").is_none() && s.log.len() == n);
|
|
}
|
|
|
|
/// The interactive save asks with the same list, and EXPORT reports it, so overwriting a 2013+
|
|
/// DXF never drops solids on a command-line note alone (#2334).
|
|
#[test]
|
|
fn a_dxf_2013_loss_is_known_before_saving() {
|
|
use cadcraft_engine::cmd::file::loss_on_save;
|
|
set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write: |_, _| Ok(b"0\nEOF\n".to_vec()), plot: None });
|
|
let sat = |d: &mut Drawing| {
|
|
let tags = vec![RawTag { code: 8, value: "0".into() }, RawTag { code: 1, value: "400 1 1 0".into() }];
|
|
d.add(&Space::Model, Common::default(), EntityKind::Unknown(Unknown { dxf_type: "REGION".into(), tags })).unwrap();
|
|
};
|
|
let mut modern = Drawing::new_imperial();
|
|
modern.header.set_str("ACADVER", "AC1032");
|
|
sat(&mut modern);
|
|
let mut s = Session::empty();
|
|
s.open_drawing(modern, "region.dxf", None);
|
|
assert_eq!(loss_on_save(&s, "region.dxf"), [("REGION".to_string(), 1)].into_iter().collect());
|
|
|
|
let path = std::env::temp_dir().join(format!("cadcraft-sat-export-{}.dxf", std::process::id()));
|
|
let r = s.execute("export", &json!({ "path": path.to_string_lossy() })).unwrap();
|
|
let _ = std::fs::remove_file(&path);
|
|
assert_eq!(r["notSaved"], json!({ "REGION": 1 }));
|
|
assert!(s.log.iter().any(|l| l.contains("Not saved in the DXF file")), "{:?}", s.log);
|
|
|
|
let mut old = Drawing::new_imperial();
|
|
old.header.set_str("ACADVER", "AC1024");
|
|
sat(&mut old);
|
|
s.open_drawing(old, "region.dxf", None);
|
|
assert!(loss_on_save(&s, "region.dxf").is_empty(), "2010 keeps SAT text");
|
|
}
|
|
|
|
/// In the Block Editor the file still holds the host drawing: its solids and version decide the
|
|
/// note, not the edited block's (#2334).
|
|
#[test]
|
|
fn the_note_counts_the_host_drawing_while_a_block_is_edited() {
|
|
set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write: |_, _| Ok(b"0\nEOF\n".to_vec()), plot: None });
|
|
set_write_as(|_, _, _: &SaveAs| Ok(b"x".to_vec()));
|
|
let mut d = Drawing::new_imperial();
|
|
let sat = EntityKind::Unknown(Unknown {
|
|
dxf_type: "REGION".into(),
|
|
tags: vec![
|
|
RawTag { code: 8, value: "0".into() },
|
|
RawTag { code: 1, value: "400 1 1 0".into() },
|
|
RawTag { code: 3, value: "End-of-ACIS-data".into() },
|
|
],
|
|
});
|
|
d.add(&Space::Model, Common::default(), sat).unwrap();
|
|
d.blocks.insert("Door".into(), std::sync::Arc::new(Block::new("Door")));
|
|
let mut s = Session::empty();
|
|
s.open_drawing(d, "region.dxf", None);
|
|
s.execute("bedit", &json!({ "name": "Door" })).unwrap();
|
|
|
|
let path = std::env::temp_dir().join(format!("cadcraft-sat-edit-{}.dxf", std::process::id()));
|
|
let r = s.execute("saveas", &json!({ "path": path.to_string_lossy(), "version": 2018 })).unwrap();
|
|
let _ = std::fs::remove_file(&path);
|
|
assert_eq!(r["notSaved"], json!({ "REGION": 1 }));
|
|
}
|
|
|
|
/// A DWG before 2013 writes a solid's SAT text as it is, so a save there doesn't list a solid that
|
|
/// holds it; from 2013 the geometry must be in the data store and SAT text alone is lost, so the
|
|
/// save lists it. A solid with neither is always listed (#2334).
|
|
#[test]
|
|
fn a_dwg_keeps_sat_text_solids_only_before_2013() {
|
|
use cadcraft_engine::cmd::file::{Version, not_saved_as};
|
|
let tag = |code: i32, value: &str| RawTag { code, value: value.into() };
|
|
let solid = |tags: Vec<RawTag>| EntityKind::Unknown(Unknown { dxf_type: "3DSOLID".into(), tags });
|
|
let mut d = Drawing::new_imperial();
|
|
d.add(&Space::Model, Common::default(), solid(vec![tag(100, "AcDbModelerGeometry"), tag(1, "400 1 1 0")])).unwrap();
|
|
d.add(&Space::Model, Common::default(), solid(vec![tag(100, "AcDbModelerGeometry"), tag(70, "1")])).unwrap();
|
|
assert_eq!(not_saved_as(&d, Version::R2010), [("3DSOLID".to_string(), 1)].into_iter().collect(), "2010 keeps the SAT-text solid");
|
|
assert_eq!(not_saved_as(&d, Version::R2018), [("3DSOLID".to_string(), 2)].into_iter().collect(), "2018 loses it");
|
|
}
|
|
|
|
/// The DWG bridge writes OLE objects (`ole2frame_roundtrip` in the io tests), so saving one as
|
|
/// DWG reports nothing for them, at any version, while other unmodelled types are still listed.
|
|
/// The type is matched as stored: the bridge gets it as stored too, and acadrust reads only the
|
|
/// upper-case spelling into its OLE entity, so any other spelling is still reported.
|
|
#[test]
|
|
fn ole_objects_are_not_reported() {
|
|
use cadcraft_engine::cmd::file::{Version, not_saved_as};
|
|
let mut d = Drawing::new_imperial();
|
|
d.add(&Space::Model, Common::default(), unknown("OLE2FRAME")).unwrap();
|
|
d.add(&Space::Model, Common::default(), unknown("MLINE")).unwrap();
|
|
d.add(&Space::Model, Common::default(), unknown("ole2frame")).unwrap();
|
|
let mut b = Block::new("Logo");
|
|
let h = d.new_handle();
|
|
b.entities.push(cadcraft_engine::doc::Entity { handle: h, common: Common::default(), kind: unknown("OLE2FRAME") });
|
|
d.blocks.insert("Logo".into(), std::sync::Arc::new(b));
|
|
for v in [Version::R2000, Version::R2010, Version::R2018] {
|
|
assert_eq!(not_saved_as(&d, v), [("MLINE".to_string(), 1), ("ole2frame".to_string(), 1)].into_iter().collect(), "{v:?}");
|
|
}
|
|
}
|