mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 12:00:50 -06:00
fix(dwg): give proxies their own class in the DWG
A DXF proxy object or entity names its class by number: 500 plus the class's
place in the DXF's CLASSES (DXF Reference, ACAD_PROXY_OBJECT, group 91).
acadrust's DXF reader puts its default classes first and numbers the DXF's
after them, and its DWG writer keeps the proxy's DXF number, so in the DWG
the number named another class. A 2018 drawing with an MEP plugin's piping
proxies wrote them as LARGE_RADIAL_DIMENSION and SECTIONOBJECT, and the ODA
File Converter refused the whole file ("AcDbProxyObject can't be cast to
AcDbBlockTableRecord").
The DWG bridge now reads the DXF's class names in CLASSES order and gives
each proxy the number acadrust writes for its class. Before 2013 (AC1027)
acadrust writes only the classes it knows such a DWG needs, and a proxy's is
not one of them, so the proxy would name a missing class ("No ClassId") or,
by its old number, another one; such a DWG now leaves the proxies out, and
the save report lists the proxy entities as not kept only for those
versions (from 2013 the DWG keeps them).
Tests: #1812's proxy drawing saved as 2018, 2013, 2010 and 2004 DWG keeps
its proxies' classes from 2013 and leaves them out before (fails before
this change: the classes came out as MESH and SCALE); unit test for the
CLASSES order; the not-saved report counts proxy entities only before 2013.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
33019677a8
commit
bf137e2c44
@@ -589,6 +589,103 @@ mod native {
|
||||
}
|
||||
}
|
||||
|
||||
/// The DXF names of an ASCII DXF's CLASS records, in CLASSES order: a proxy's class number
|
||||
/// (group 91) is 500 plus its class's place here (DXF Reference, ACAD_PROXY_OBJECT). Empty for
|
||||
/// a binary DXF or one without a CLASSES section.
|
||||
pub(super) fn dxf_class_names(dxf: &[u8]) -> Vec<String> {
|
||||
let mut names = Vec::new();
|
||||
if dxf.starts_with(b"AutoCAD Binary DXF") {
|
||||
return names;
|
||||
}
|
||||
let mut lines = dxf.split(|b| *b == b'\n').map(|l| l.trim_ascii());
|
||||
let (mut in_classes, mut in_class) = (false, false);
|
||||
while let (Some(code), Some(value)) = (lines.next(), lines.next()) {
|
||||
match (code, value) {
|
||||
(b"0", b"ENDSEC") if in_classes => break,
|
||||
(b"2", b"CLASSES") => in_classes = true,
|
||||
(b"0", v) if in_classes => in_class = v == b"CLASS",
|
||||
(b"1", v) if in_class => {
|
||||
names.push(String::from_utf8_lossy(v).into_owned());
|
||||
in_class = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
/// Proxy objects' and entities' class numbers as the DWG numbers its classes. A DXF proxy
|
||||
/// names its class by its place in the DXF's CLASSES (`classes`, from 500), but acadrust's
|
||||
/// reader puts its own default classes first and numbers the DXF's after them, so the DXF
|
||||
/// number named another class in the DWG: a 2018 drawing's piping proxies became
|
||||
/// LARGE_RADIAL_DIMENSION and SECTIONOBJECT, and the ODA File Converter refused the file. A
|
||||
/// number that names no class of the DXF is left as it is.
|
||||
///
|
||||
/// Before 2013 (AC1027) acadrust writes only the classes it knows a pre-2013 DWG needs, and a
|
||||
/// proxy's class is not one of them: the proxy would name a missing class (the ODA refuses
|
||||
/// the file: "No ClassId") or, by its old number, some other class. Such a DWG leaves the
|
||||
/// proxies out instead, as it does other programs' data it can't write (`not_saved` reports
|
||||
/// the entities). Returns how many proxies were left out.
|
||||
fn proxy_classes(doc: &mut acadrust::CadDocument, classes: &[String]) -> usize {
|
||||
if doc.version < acadrust::DxfVersion::AC1027 {
|
||||
let entities: Vec<acadrust::Handle> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
acadrust::entities::EntityType::Extended(x) if matches!(x.data, acadrust::entities::ExtendedEntityData::Proxy(_)) => {
|
||||
Some(x.common.handle)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let objects: Vec<acadrust::Handle> =
|
||||
doc.objects.iter().filter_map(|(h, o)| matches!(o, acadrust::objects::ObjectType::ProxyObject(_)).then_some(*h)).collect();
|
||||
for h in &entities {
|
||||
doc.remove_entity(*h);
|
||||
}
|
||||
for h in &objects {
|
||||
doc.objects.remove(h);
|
||||
}
|
||||
return entities.len() + objects.len();
|
||||
}
|
||||
let number = |id: i32| -> Option<i32> {
|
||||
let name = classes.get(usize::try_from(id.checked_sub(500)?).ok()?)?;
|
||||
doc.classes.get_by_name(name).map(|c| i32::from(c.class_number))
|
||||
};
|
||||
let objects: Vec<(acadrust::Handle, i32)> = doc
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(h, o)| match o {
|
||||
acadrust::objects::ObjectType::ProxyObject(p) => number(p.class_id).map(|n| (*h, n)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let entities: Vec<(acadrust::Handle, i32)> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
acadrust::entities::EntityType::Extended(x) => match &x.data {
|
||||
acadrust::entities::ExtendedEntityData::Proxy(p) => number(p.class_id).map(|n| (x.common.handle, n)),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for (h, n) in objects {
|
||||
if let Some(acadrust::objects::ObjectType::ProxyObject(p)) = doc.objects.get_mut(&h) {
|
||||
p.class_id = n;
|
||||
}
|
||||
}
|
||||
let entities: std::collections::HashMap<acadrust::Handle, i32> = entities.into_iter().collect();
|
||||
for e in doc.entities_mut() {
|
||||
if let acadrust::entities::EntityType::Extended(x) = e {
|
||||
let Some(n) = entities.get(&x.common.handle).copied() else { continue };
|
||||
if let acadrust::entities::ExtendedEntityData::Proxy(p) = &mut x.data {
|
||||
p.class_id = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// `kept`: the version the DXF's carried-over groups were read from ([`dxf_to_dwg_kept_as`]).
|
||||
fn convert(dxf: &[u8], version: Option<acadrust::DxfVersion>, kept: Option<acadrust::DxfVersion>) -> Result<Vec<u8>, String> {
|
||||
// Reject a pathologically large DXF before handing it to the third-party decoder, which
|
||||
@@ -614,6 +711,7 @@ mod native {
|
||||
Some(d) => d,
|
||||
None => dxf.to_vec(),
|
||||
};
|
||||
let classes = dxf_class_names(&data);
|
||||
let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read()))
|
||||
.map_err(|_| "the DXF→DWG conversion failed".to_string())?
|
||||
.map_err(|e| format!("DXF: {e}"))?;
|
||||
@@ -625,6 +723,7 @@ mod native {
|
||||
None if doc.version < MIN_DWG_VERSION => doc.version = MIN_DWG_VERSION,
|
||||
None => {}
|
||||
}
|
||||
proxy_classes(&mut doc, &classes);
|
||||
other_layout_owners(&mut doc);
|
||||
table_style_rows(&mut doc, true);
|
||||
dimension_flags(&mut doc);
|
||||
@@ -1071,4 +1170,14 @@ mod tests {
|
||||
assert_eq!(dxf_to_dwg_kept_as(&dxf, "AC1032", "AC9999"), dxf_to_dwg_as(&dxf, "AC1032"));
|
||||
assert!(dxf_to_dwg_kept_as(&dxf, "AC0000", "AC1032").is_err(), "an unwritable version is still an error");
|
||||
}
|
||||
|
||||
/// The CLASS names in CLASSES order; nothing for a binary DXF or one without CLASSES.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn dxf_class_names_in_classes_order() {
|
||||
let dxf = b" 0\r\nSECTION\r\n 2\r\nHEADER\r\n 0\r\nENDSEC\r\n 0\r\nSECTION\r\n 2\r\nCLASSES\r\n 0\r\nCLASS\r\n 1\r\nWIDGETA\r\n 2\r\nAcDbWidgetA\r\n 3\r\nApp\r\n 0\r\nCLASS\r\n 1\r\nWIDGETB\r\n 2\r\nAcDbWidgetB\r\n 0\r\nENDSEC\r\n 0\r\nSECTION\r\n 2\r\nENTITIES\r\n 0\r\nTEXT\r\n 1\r\nNOT_A_CLASS\r\n 0\r\nENDSEC\r\n 0\r\nEOF\r\n";
|
||||
assert_eq!(native::dxf_class_names(dxf), ["WIDGETA", "WIDGETB"]);
|
||||
assert!(native::dxf_class_names(b" 0\nSECTION\n 2\nHEADER\n 0\nENDSEC\n 0\nEOF\n").is_empty());
|
||||
assert!(native::dxf_class_names(b"AutoCAD Binary DXF\r\n\x1a\0").is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,7 +538,9 @@ pub fn not_saved(d: &Drawing) -> std::collections::BTreeMap<String, usize> {
|
||||
const MODELER_TYPES: [&str; 3] = ["3DSOLID", "REGION", "BODY"];
|
||||
|
||||
/// [`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.
|
||||
/// store has a record for keeps its geometry and is not counted, and neither is another program's
|
||||
/// proxy entity: the DWG writes it with its class (before 2013 it has no class to name, and the
|
||||
/// DWG leaves it out).
|
||||
pub fn not_saved_as(d: &Drawing, version: Version) -> std::collections::BTreeMap<String, usize> {
|
||||
let mut out: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
|
||||
let stored = if version >= Version::R2013 { d.foreign.data_store_entities() } else { Default::default() };
|
||||
@@ -548,6 +550,9 @@ pub fn not_saved_as(d: &Drawing, version: Version) -> std::collections::BTreeMap
|
||||
if MODELER_TYPES.iter().any(|t| u.dxf_type.eq_ignore_ascii_case(t)) && stored.contains(&e.handle) {
|
||||
continue;
|
||||
}
|
||||
if version >= Version::R2013 && u.dxf_type.eq_ignore_ascii_case("ACAD_PROXY_ENTITY") {
|
||||
continue;
|
||||
}
|
||||
let n = out.entry(u.dxf_type.clone()).or_insert(0);
|
||||
*n = n.saturating_add(1);
|
||||
}
|
||||
|
||||
@@ -82,3 +82,19 @@ 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());
|
||||
}
|
||||
|
||||
/// Another program's proxy entity reaches a 2013 or later DWG with its class; before 2013 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};
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.add(&Space::Model, Common::default(), unknown("ACAD_PROXY_ENTITY")).unwrap();
|
||||
d.add(&Space::Model, Common::default(), unknown("MLINE")).unwrap();
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2814,6 +2814,50 @@ fn proxies_keep_their_class_when_classes_are_rewritten() {
|
||||
assert_eq!(names, ["VISUALSTYLE", "WIDGETX"]);
|
||||
}
|
||||
|
||||
/// The proxies of [`proxy_file`] keep their classes in a DWG too. A proxy names its class by
|
||||
/// number, 500 plus the class's place in the CLASSES the file lists; acadrust numbers the classes
|
||||
/// it writes in its own order (its default classes first), so the DXF's numbers named other
|
||||
/// classes there: a 2018 drawing's piping proxies came out as LARGE_RADIAL_DIMENSION and
|
||||
/// SECTIONOBJECT, and the ODA File Converter refused the file ("AcDbProxyObject can't be cast to
|
||||
/// AcDbBlockTableRecord"). A DWG before 2013 leaves them out (acadrust writes no class for them).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn proxies_keep_their_class_in_dwg() {
|
||||
use acadrust::entities::{EntityType, ExtendedEntityData};
|
||||
use acadrust::objects::ObjectType;
|
||||
let d = read(&proxy_file(), "p.dxf").unwrap();
|
||||
for version in [Version::R2018, Version::R2013, Version::R2010, Version::R2004] {
|
||||
let dwg = write_with(&d, "p.dwg", &SaveOptions { version: Some(version), binary: false }).unwrap();
|
||||
let doc = acadrust::DwgReader::from_stream(std::io::Cursor::new(dwg)).read().unwrap();
|
||||
let class_of = |id: i32| doc.classes.iter().find(|c| i32::from(c.class_number) == id).map(|c| c.dxf_name.clone());
|
||||
let mut objects: Vec<Option<String>> = doc
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|o| match o {
|
||||
ObjectType::ProxyObject(p) => Some(class_of(p.class_id)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
objects.sort();
|
||||
// Before 2013 acadrust writes no class for a proxy, so the DWG leaves the proxies out.
|
||||
let legacy = version < Version::R2013;
|
||||
let expected = if legacy { vec![] } else { vec![Some("VISUALSTYLE".to_string()), Some("WIDGETX".to_string())] };
|
||||
assert_eq!(objects, expected, "{version:?}: proxy objects");
|
||||
let entities: Vec<Option<String>> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Extended(x) => match &x.data {
|
||||
ExtendedEntityData::Proxy(p) => Some(class_of(p.class_id)),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let expected = if legacy { vec![] } else { vec![Some("WIDGETX".to_string())] };
|
||||
assert_eq!(entities, expected, "{version:?}: proxy entity");
|
||||
}
|
||||
}
|
||||
|
||||
/// A name that isn't valid Unicode still picks its format by extension.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user