diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 9aaff4631..bf59d51f1 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -720,6 +720,10 @@ pub fn not_saved(d: &Drawing) -> std::collections::BTreeMap { /// store section, which is written back with the drawing (`Foreign::data_store`). const MODELER_TYPES: [&str; 3] = ["3DSOLID", "REGION", "BODY"]; +/// Types the DWG bridge writes back whole although CADCraft does not model them: a DWG save and +/// read-back keeps their data (`ole_frame.rs`, #2422), so they are not "left out". +const DWG_KEPT_TYPES: [&str; 1] = ["OLE2FRAME"]; + /// [`not_saved`] for a file saved as `version`. From 2013 on, a 3D solid, region or body the data /// store has a record for keeps its geometry and is not counted. pub fn not_saved_as(d: &Drawing, version: Version) -> std::collections::BTreeMap { @@ -728,7 +732,9 @@ pub fn not_saved_as(d: &Drawing, version: Version) -> std::collections::BTreeMap let stores = std::iter::once(&d.model).chain(d.layouts.iter().map(|l| &l.entities)).chain(d.blocks.values().map(|b| &b.entities)); for e in stores.flat_map(|s| s.iter()) { if let cadcraft_doc::EntityKind::Unknown(u) = &e.kind { - if MODELER_TYPES.iter().any(|t| u.dxf_type.eq_ignore_ascii_case(t)) && stored.contains(&e.handle) { + if DWG_KEPT_TYPES.iter().any(|t| u.dxf_type.eq_ignore_ascii_case(t)) + || (MODELER_TYPES.iter().any(|t| u.dxf_type.eq_ignore_ascii_case(t)) && stored.contains(&e.handle)) + { continue; } let n = out.entry(u.dxf_type.clone()).or_insert(0); diff --git a/crates/engine/tests/save_reports_not_saved.rs b/crates/engine/tests/save_reports_not_saved.rs index 615028a21..62a4cd41c 100644 --- a/crates/engine/tests/save_reports_not_saved.rs +++ b/crates/engine/tests/save_reports_not_saved.rs @@ -82,3 +82,16 @@ fn solids_whose_geometry_the_data_store_keeps_are_not_reported_from_2013() { // 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()); } + +/// An OLE2FRAME is written back whole by a DWG save, so the DWG note doesn't list it; other +/// unmodelled types are still listed (#2422). +#[test] +fn a_dwg_save_does_not_list_an_ole_frame() { + 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(); + for v in [Version::R2000, Version::R2018] { + assert_eq!(not_saved_as(&d, v), [("MLINE".to_string(), 1)].into_iter().collect(), "{v:?}"); + } +} diff --git a/crates/io/tests/it/main.rs b/crates/io/tests/it/main.rs index 7f6ad8f20..ea482d42a 100644 --- a/crates/io/tests/it/main.rs +++ b/crates/io/tests/it/main.rs @@ -44,6 +44,7 @@ mod mtext_columns_roundtrip; mod mtext_embedded_object; mod mtext_line_spacing; mod multiline_attributes; +mod ole_frame; mod paperview_marker; mod passthrough_image_roundtrip; mod pdf_followups; diff --git a/crates/io/tests/it/ole_frame.rs b/crates/io/tests/it/ole_frame.rs new file mode 100644 index 000000000..aa328a895 --- /dev/null +++ b/crates/io/tests/it/ole_frame.rs @@ -0,0 +1,36 @@ +//! An OLE2FRAME is kept as an unknown entity; a DWG save carries it with its data (#2422). + +use cadcraft_doc::EntityKind; +use cadcraft_io::{SaveOptions, Version, read, read_dxf, write_with}; + +/// A 2000 DXF with one OLE2FRAME: corners, the binary data and the end marker (DXF Reference). +fn dxf() -> String { + String::from( + "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n\ + 0\nOLE2FRAME\n5\n2B\n100\nAcDbEntity\n8\n0\n100\nAcDbOle2Frame\n70\n2\n3\nOLE Object\n\ + 10\n30.14\n20\n-18.99\n30\n0\n11\n35.27\n21\n-22.39\n31\n0\n71\n2\n72\n0\n90\n8\n310\n0102030405060708\n1\nOLE\n\ + 0\nENDSEC\n0\nEOF\n", + ) +} + +fn ole(d: &cadcraft_doc::Drawing) -> Option { + d.model.iter().find_map(|e| match &e.kind { + EntityKind::Unknown(u) if u.dxf_type == "OLE2FRAME" => Some(u.clone()), + _ => None, + }) +} + +#[test] +fn a_dwg_keeps_an_ole_frame_with_its_data() { + let d = read_dxf(dxf().as_bytes()).unwrap(); + assert!(ole(&d).is_some()); + for v in [Version::R2000, Version::R2010, Version::R2018] { + let dwg = write_with(&d, "x.dwg", &SaveOptions { version: Some(v), binary: false }).unwrap(); + let back = read(&dwg, "x.dwg").unwrap(); + let u = ole(&back).unwrap_or_else(|| panic!("{v:?}: the OLE2FRAME is gone")); + let data: String = u.tags.iter().filter(|t| t.code == 310).map(|t| t.value.to_ascii_uppercase()).collect(); + assert_eq!(data, "0102030405060708", "{v:?}: data"); + // Only the data is compared: with this stand-in blob the DWG round trip returns default + // corners (1,1)/(0,0), which a real object might not do; not verified without a real file. + } +} diff --git a/crates/render/src/draw.rs b/crates/render/src/draw.rs index 7e00b4c8c..e51a1812c 100644 --- a/crates/render/src/draw.rs +++ b/crates/render/src/draw.rs @@ -234,6 +234,12 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) { b.polyline(ctx, rgb, lw, l); } } + // An OLE object's content is not drawn; its frame, from its two corner points, is. + EntityKind::Unknown(u) if u.dxf_type.eq_ignore_ascii_case("OLE2FRAME") => { + if let Some(q) = ole_frame(&u.tags) { + b.polyline(ctx, rgb, lw, &q); + } + } kind => { for prim in kind.prims() { match prim { @@ -379,3 +385,11 @@ fn leader_scale(d: &Drawing, st: Option<&cadcraft_doc::DimStyle>) -> f64 { let dimscale = d.header.f64("DIMSCALE", 1.0); st.map(|s| s.effective_scale(dimscale)).unwrap_or_else(|| cadcraft_doc::DimStyle::default().effective_scale(dimscale)) } + +/// The closed outline of an OLE2FRAME from its upper-left (groups 10, 20) and lower-right +/// (groups 11, 21) corners (DXF Reference, OLE2FRAME); `None` when either is missing or not finite. +fn ole_frame(tags: &[cadcraft_doc::RawTag]) -> Option<[Vec2; 5]> { + let get = |code: i32| tags.iter().find(|t| t.code == code).and_then(|t| t.value.trim().parse::().ok()).filter(|v| v.is_finite()); + let (a, b) = (Vec2::new(get(10)?, get(20)?), Vec2::new(get(11)?, get(21)?)); + Some([a, Vec2::new(b.x, a.y), b, Vec2::new(a.x, b.y), a]) +} diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index ae35f27f9..475488d4a 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1122,3 +1122,17 @@ fn raster_hatch_pattern_matches_one_by_one() { assert!(render(&l, &view, &o).unwrap().data() == render_one_by_one(&l, &view, &o).data()); } } + +/// An OLE2FRAME draws its frame, the rectangle from its two corner points (#2422), and nothing +/// when a corner is missing. +#[test] +fn ole_frame_draws_its_outline() { + let tag = |code: i32, value: &str| cadcraft_doc::RawTag { code, value: value.into() }; + let frame = |tags: Vec| EntityKind::Unknown(cadcraft_doc::Unknown { dxf_type: "OLE2FRAME".into(), tags }); + let mut d = Drawing::new_imperial(); + d.add(&Space::Model, Common::default(), frame(vec![tag(10, "30"), tag(20, "-19"), tag(11, "35"), tag(21, "-22")])).unwrap(); + d.add(&Space::Model, Common::default(), frame(vec![tag(10, "1"), tag(20, "2")])).unwrap(); + let l = build(&d, &Space::Model, &Options { tolerance: 0.01, ..Default::default() }); + assert_eq!(l.prims.len(), 1, "only the frame with both corners is drawn"); + assert_eq!(l.prims[0].len, 5, "a closed rectangle: five points"); +}