use std::collections::BTreeMap;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use acadrust::entities::EntityType;
use acadrust::types::Handle;
use acadrust::{CadDocument, DwgReader, DwgWriter, DxfReader, DxfWriter};
fn sibling_path(source: &Path, suffix: &str, ext: &str) -> PathBuf {
let stem = source
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "output".to_string());
let mut p = source.to_path_buf();
p.set_file_name(format!("{}{}.{}", stem, suffix, ext));
p
}
fn save_bytes(path: &Path, bytes: &[u8]) {
match std::fs::write(path, bytes) {
Ok(_) => println!(" Saved {} bytes → {}", bytes.len(), path.display()),
Err(e) => eprintln!(" WARNING: could not write {}: {}", path.display(), e),
}
}
fn entity_variant_name(entity: &EntityType) -> String {
let dbg = format!("{:?}", entity);
if let Some(paren_pos) = dbg.find('(') {
dbg[..paren_pos].to_string()
} else {
dbg
}
}
fn entity_type_counts(doc: &CadDocument) -> BTreeMap<String, usize> {
let mut map = BTreeMap::new();
for entity in doc.entities() {
*map.entry(entity_variant_name(entity)).or_insert(0) += 1;
}
map
}
fn normalize_common(common: &mut acadrust::entities::EntityCommon) {
common.handle = Handle::NULL;
common.owner_handle = Handle::NULL;
common.reactors.clear();
common.xdictionary_handle = None;
}
fn normalize_entity(entity: &mut EntityType) {
normalize_common(entity.common_mut());
match entity {
EntityType::Polyline3D(p) => {
for v in &mut p.vertices {
v.handle = Handle::NULL;
v.layer = String::new();
}
}
EntityType::PolyfaceMesh(pf) => {
pf.seqend_handle = None;
for v in &mut pf.vertices {
normalize_common(&mut v.common);
}
for f in &mut pf.faces {
normalize_common(&mut f.common);
}
}
EntityType::Hatch(h) => {
for path in &mut h.paths {
path.boundary_handles.clear();
}
}
EntityType::MLine(ml) => {
ml.style_handle = None;
}
EntityType::MultiLeader(mld) => {
mld.style_handle = None;
mld.line_type_handle = None;
mld.arrowhead_handle = None;
mld.text_style_handle = None;
mld.block_content_handle = None;
mld.context.text_style_handle = None;
mld.context.block_content_handle = None;
mld.context.scale_handle = None;
for root in &mut mld.context.leader_roots {
for line in &mut root.lines {
line.line_type_handle = None;
line.arrowhead_handle = None;
}
}
for attr in &mut mld.block_attributes {
attr.attribute_definition_handle = None;
}
}
EntityType::Tolerance(t) => {
t.dimension_style_handle = None;
}
EntityType::Shape(s) => {
s.style_handle = None;
}
EntityType::Leader(l) => {
l.annotation_handle = Handle::NULL;
}
EntityType::Viewport(v) => {
v.ucs_handle = Handle::NULL;
v.base_ucs_handle = Handle::NULL;
v.background_handle = Handle::NULL;
v.shade_plot_handle = Handle::NULL;
v.visual_style_handle = Handle::NULL;
}
EntityType::Dimension(d) => {
let base = d.base_mut();
base.block_name = String::new();
base.actual_measurement = 0.0;
}
EntityType::Insert(ins) => {
for attr in &mut ins.attributes {
normalize_common(&mut attr.common);
attr.attdef_handle = Handle::NULL;
}
}
EntityType::RasterImage(r) => {
r.definition_handle = None;
r.definition_reactor_handle = None;
}
EntityType::Mesh(m) => {
for edge in &mut m.edges {
if edge.crease.is_none() {
edge.crease = Some(0.0);
}
}
m.edges.sort_by(|a, b| a.start.cmp(&b.start).then(a.end.cmp(&b.end)));
}
EntityType::Text(t) => {
t.style = t.style.to_uppercase();
}
EntityType::MText(m) => {
m.style = m.style.to_uppercase();
}
_ => {}
}
}
fn field_diff(orig: &str, rt: &str) -> Vec<String> {
let orig_lines: Vec<&str> = orig.lines().collect();
let rt_lines: Vec<&str> = rt.lines().collect();
let max = orig_lines.len().max(rt_lines.len());
let mut diffs = Vec::new();
for i in 0..max {
let o = orig_lines.get(i).unwrap_or(&"<missing>");
let r = rt_lines.get(i).unwrap_or(&"<missing>");
if o != r {
diffs.push(format!(
" line {}: ORIG: {}\n RT: {}",
i + 1,
o.trim(),
r.trim()
));
}
}
diffs
}
fn print_document_summary(label: &str, doc: &CadDocument) {
println!("╔══════════════════════════════════════════════════════════════╗");
println!("║ {} ║", format!("{:<56}", label));
println!("╠══════════════════════════════════════════════════════════════╣");
println!("║ Version: {:?}", doc.version);
println!("║ Entities: {}", doc.entity_count());
println!("║ Layers: {}", doc.layers.len());
println!("║ LineTypes: {}", doc.line_types.len());
println!("║ TextStyles: {}", doc.text_styles.len());
println!("║ DimStyles: {}", doc.dim_styles.len());
println!("║ BlockRecs: {}", doc.block_records.len());
println!("║ AppIds: {}", doc.app_ids.len());
println!("║ Views: {}", doc.views.len());
println!("║ VPorts: {}", doc.vports.len());
println!("║ UCSs: {}", doc.ucss.len());
println!("║ Objects: {}", doc.objects.len());
println!("║ Classes: {}", doc.classes.len());
let counts = entity_type_counts(doc);
println!("║ ─── Entity breakdown ───");
for (name, count) in &counts {
println!("║ {:<30} {}", name, count);
}
let notifs: Vec<_> = doc.notifications.iter().collect();
if !notifs.is_empty() {
println!("║ ─── Notifications ({}) ───", notifs.len());
for n in notifs.iter().filter(|n| format!("{}", n.notification_type) == "Error") {
println!("║ [{}] {}", n.notification_type, n.message);
}
for n in notifs.iter().filter(|n| format!("{}", n.notification_type) != "Error").take(5) {
println!("║ [{}] {}...", n.notification_type, &n.message[..n.message.len().min(80)]);
}
if notifs.len() > 5 {
println!("║ ... and {} more notifications", notifs.len().saturating_sub(5));
}
}
println!("╚══════════════════════════════════════════════════════════════╝");
println!();
}
struct LossReport {
format: String,
count_diffs: Vec<String>,
entity_diffs: Vec<String>,
table_diffs: Vec<String>,
header_diffs: Vec<String>,
object_diffs: Vec<String>,
}
impl LossReport {
fn new(format: &str) -> Self {
Self {
format: format.to_string(),
count_diffs: Vec::new(),
entity_diffs: Vec::new(),
table_diffs: Vec::new(),
header_diffs: Vec::new(),
object_diffs: Vec::new(),
}
}
fn total(&self) -> usize {
self.count_diffs.len()
+ self.entity_diffs.len()
+ self.table_diffs.len()
+ self.header_diffs.len()
+ self.object_diffs.len()
}
fn print(&self) {
let total = self.total();
println!("┌──────────────────────────────────────────────────────────────┐");
println!(
"│ {} Roundtrip: {} difference(s) found{}│",
self.format,
total,
" ".repeat(38usize.saturating_sub(self.format.len() + format!("{}", total).len()))
);
println!("└──────────────────────────────────────────────────────────────┘");
if total == 0 {
println!(" ✓ PERFECT roundtrip — no data loss detected!\n");
return;
}
if !self.count_diffs.is_empty() {
println!("\n ── Count Mismatches ──");
for d in &self.count_diffs {
println!(" • {}", d);
}
}
if !self.table_diffs.is_empty() {
println!("\n ── Table Differences ──");
for d in &self.table_diffs {
println!(" • {}", d);
}
}
if !self.header_diffs.is_empty() {
println!("\n ── Header Variable Differences ──");
for d in &self.header_diffs {
println!(" • {}", d);
}
}
if !self.object_diffs.is_empty() {
println!("\n ── Object Differences ──");
for d in &self.object_diffs {
println!(" • {}", d);
}
}
if !self.entity_diffs.is_empty() {
println!("\n ── Entity Data Differences ({}) ──", self.entity_diffs.len());
for (i, d) in self.entity_diffs.iter().enumerate() {
if i >= 50 {
println!(
" ... and {} more entity differences",
self.entity_diffs.len() - 50
);
break;
}
println!(" • {}", d);
}
}
println!();
}
}
fn compare_documents(orig: &CadDocument, rt: &CadDocument, format: &str) -> LossReport {
let mut report = LossReport::new(format);
let orig_count = orig.entity_count();
let rt_count = rt.entity_count();
if orig_count != rt_count {
report.count_diffs.push(format!(
"Total entity count: {} → {} (Δ{})",
orig_count,
rt_count,
rt_count as isize - orig_count as isize
));
}
let orig_types = entity_type_counts(orig);
let rt_types = entity_type_counts(rt);
for (name, &orig_c) in &orig_types {
let rt_c = rt_types.get(name).copied().unwrap_or(0);
if orig_c != rt_c {
report.count_diffs.push(format!(
" {}: {} → {} (Δ{})",
name,
orig_c,
rt_c,
rt_c as isize - orig_c as isize
));
}
}
for (name, &rt_c) in &rt_types {
if !orig_types.contains_key(name) {
report.count_diffs.push(format!(
" {} appeared in roundtrip: 0 → {}",
name, rt_c
));
}
}
macro_rules! cmp_table {
($name:expr, $field:ident) => {
if orig.$field.len() != rt.$field.len() {
report.table_diffs.push(format!(
"{}: {} → {}",
$name,
orig.$field.len(),
rt.$field.len()
));
}
};
}
cmp_table!("Layers", layers);
cmp_table!("LineTypes", line_types);
cmp_table!("TextStyles", text_styles);
cmp_table!("BlockRecords", block_records);
cmp_table!("DimStyles", dim_styles);
cmp_table!("AppIds", app_ids);
cmp_table!("Views", views);
cmp_table!("VPorts", vports);
cmp_table!("UCSs", ucss);
if orig.objects.len() != rt.objects.len() {
report.object_diffs.push(format!(
"Object count: {} → {}",
orig.objects.len(),
rt.objects.len()
));
}
if orig.classes.len() != rt.classes.len() {
report.object_diffs.push(format!(
"Class count: {} → {}",
orig.classes.len(),
rt.classes.len()
));
}
compare_header(&mut report, &orig.header, &rt.header);
compare_entities(&mut report, orig, rt);
report
}
fn compare_header(
report: &mut LossReport,
orig: &acadrust::document::HeaderVariables,
rt: &acadrust::document::HeaderVariables,
) {
macro_rules! cmp {
($field:ident) => {
if orig.$field != rt.$field {
report.header_diffs.push(format!(
"{}: {:?} → {:?}",
stringify!($field),
orig.$field,
rt.$field
));
}
};
}
cmp!(associate_dimensions);
cmp!(ortho_mode);
cmp!(fill_mode);
cmp!(quick_text_mode);
cmp!(mirror_text);
cmp!(regen_mode);
cmp!(limit_check);
cmp!(show_model_space);
cmp!(world_view);
cmp!(retain_xref_visibility);
cmp!(display_silhouette);
cmp!(linear_unit_format);
cmp!(linear_unit_precision);
cmp!(angular_unit_format);
cmp!(angular_unit_precision);
cmp!(insertion_units);
cmp!(linetype_scale);
cmp!(text_height);
cmp!(dim_scale);
cmp!(dim_arrow_size);
cmp!(dim_text_height);
cmp!(dim_tolerance);
cmp!(dim_limits);
cmp!(dim_decimal_places);
cmp!(model_space_insertion_base);
cmp!(model_space_limits_min);
cmp!(model_space_limits_max);
cmp!(measurement);
}
fn compare_entities(report: &mut LossReport, orig: &CadDocument, rt: &CadDocument) {
let mut orig_by_type: BTreeMap<String, Vec<&EntityType>> = BTreeMap::new();
let mut rt_by_type: BTreeMap<String, Vec<&EntityType>> = BTreeMap::new();
for e in orig.entities() {
orig_by_type
.entry(entity_variant_name(e))
.or_default()
.push(e);
}
for e in rt.entities() {
rt_by_type
.entry(entity_variant_name(e))
.or_default()
.push(e);
}
for (type_name, orig_entities) in &orig_by_type {
let rt_entities = match rt_by_type.get(type_name) {
Some(v) => v,
None => {
report.entity_diffs.push(format!(
"{}: all {} entities LOST",
type_name,
orig_entities.len()
));
continue;
}
};
let count = orig_entities.len().min(rt_entities.len());
for i in 0..count {
let mut o = orig_entities[i].clone();
let mut r = rt_entities[i].clone();
let o_common = o.common();
let r_common = r.common();
let mut common_diffs = Vec::new();
if o_common.layer != r_common.layer {
common_diffs.push(format!(
"layer: {:?} → {:?}",
o_common.layer, r_common.layer
));
}
if o_common.color != r_common.color {
common_diffs.push(format!(
"color: {:?} → {:?}",
o_common.color, r_common.color
));
}
if o_common.line_weight != r_common.line_weight {
common_diffs.push(format!(
"line_weight: {:?} → {:?}",
o_common.line_weight, r_common.line_weight
));
}
if o_common.linetype != r_common.linetype {
common_diffs.push(format!(
"linetype: {:?} → {:?}",
o_common.linetype, r_common.linetype
));
}
if (o_common.linetype_scale - r_common.linetype_scale).abs() > 1e-10 {
common_diffs.push(format!(
"linetype_scale: {} → {}",
o_common.linetype_scale, r_common.linetype_scale
));
}
if o_common.invisible != r_common.invisible {
common_diffs.push(format!(
"invisible: {} → {}",
o_common.invisible, r_common.invisible
));
}
for cd in common_diffs {
report
.entity_diffs
.push(format!("{}[{}] common.{}", type_name, i, cd));
}
normalize_entity(&mut o);
normalize_entity(&mut r);
if o != r {
let o_dbg = format!("{:#?}", o);
let r_dbg = format!("{:#?}", r);
let diffs = field_diff(&o_dbg, &r_dbg);
if diffs.len() <= 5 {
for d in &diffs {
report
.entity_diffs
.push(format!("{}[{}] field diff:\n{}", type_name, i, d));
}
} else {
report.entity_diffs.push(format!(
"{}[{}]: {} field differences (showing first 5):\n{}",
type_name,
i,
diffs.len(),
diffs[..5].join("\n")
));
}
}
}
}
for type_name in rt_by_type.keys() {
if !orig_by_type.contains_key(type_name) {
report.entity_diffs.push(format!(
"{}: {} entities APPEARED (not in original)",
type_name,
rt_by_type[type_name].len()
));
}
}
}
fn main() {
let input_str = std::env::args()
.nth(1)
.unwrap_or_else(|| "tests/roundtrip/samplekitchen.dwg".to_string());
let source_path = Path::new(&input_str).to_path_buf();
println!("=================================================================");
println!(" Roundtrip Analysis: {}", source_path.display());
println!("=================================================================\n");
println!("Reading original DWG...");
let mut reader = match DwgReader::from_file(&source_path) {
Ok(r) => r,
Err(e) => {
eprintln!("ERROR: Could not open file: {}", e);
std::process::exit(1);
}
};
let original = match reader.read() {
Ok(doc) => doc,
Err(e) => {
eprintln!("ERROR: Could not read DWG: {}", e);
std::process::exit(1);
}
};
print_document_summary("ORIGINAL (from DWG file)", &original);
println!("Performing DWG roundtrip (write → read → disk)...");
let dwg_out_path = sibling_path(&source_path, "_rt", "dwg");
let dwg_rt = match DwgWriter::write_to_vec(&original) {
Ok(bytes) => {
save_bytes(&dwg_out_path, &bytes);
let mut r = DwgReader::from_stream(Cursor::new(bytes));
match r.read() {
Ok(doc) => Some(doc),
Err(e) => {
eprintln!(" WARNING: DWG re-read failed: {}", e);
None
}
}
}
Err(e) => {
eprintln!(" WARNING: DWG write failed: {}", e);
None
}
};
if let Some(ref rt) = dwg_rt {
print_document_summary("DWG ROUNDTRIPPED", rt);
}
println!("Performing DXF roundtrip (write → read → disk)...");
let dxf_out_path = sibling_path(&source_path, "_rt", "dxf");
let dxf_rt = {
let writer = DxfWriter::new(&original);
match writer.write_to_vec() {
Ok(bytes) => {
save_bytes(&dxf_out_path, &bytes);
match DxfReader::from_reader(Cursor::new(bytes)) {
Ok(r) => match r.read() {
Ok(doc) => Some(doc),
Err(e) => {
eprintln!(" WARNING: DXF re-read failed: {}", e);
None
}
},
Err(e) => {
eprintln!(" WARNING: DXF reader init failed: {}", e);
None
}
}
}
Err(e) => {
eprintln!(" WARNING: DXF write failed: {}", e);
None
}
}
};
if let Some(ref rt) = dxf_rt {
print_document_summary("DXF ROUNDTRIPPED", rt);
}
println!("\n=================================================================");
println!(" DATA LOSS ANALYSIS");
println!("=================================================================\n");
if let Some(ref rt) = dwg_rt {
let report = compare_documents(&original, rt, "DWG");
report.print();
println!();
deep_analysis(&original, rt, "DWG");
} else {
println!("DWG roundtrip: FAILED (could not complete write→read cycle)");
}
if let Some(ref rt) = dxf_rt {
let report = compare_documents(&original, rt, "DXF");
report.print();
println!();
deep_analysis(&original, rt, "DXF");
} else {
println!("DXF roundtrip: FAILED (could not complete write→read cycle)");
}
println!("=================================================================");
println!(" DWG DOUBLE-ROUNDTRIP STABILITY");
println!("=================================================================\n");
if let Some(ref rt1) = dwg_rt {
println!("Performing DWG double roundtrip...");
let dwg2_out_path = sibling_path(&source_path, "_rt2", "dwg");
match DwgWriter::write_to_vec(rt1) {
Ok(bytes2) => {
save_bytes(&dwg2_out_path, &bytes2);
let mut r2 = DwgReader::from_stream(Cursor::new(bytes2));
match r2.read() {
Ok(rt2) => {
let report = compare_documents(rt1, &rt2, "DWG double-roundtrip (RT1→RT2)");
report.print();
if !report.entity_diffs.is_empty() {
deep_analysis(rt1, &rt2, "DWG double-roundtrip");
}
}
Err(e) => eprintln!("DWG double roundtrip re-read failed: {}", e),
}
}
Err(e) => eprintln!("DWG double roundtrip write failed: {}", e),
}
}
println!("=================================================================");
println!(" Analysis complete.");
println!(" Output files:");
println!(" {}", dwg_out_path.display());
println!(" {}", dxf_out_path.display());
println!(" {}", sibling_path(&source_path, "_rt2", "dwg").display());
println!("=================================================================");
}
fn deep_analysis(orig: &CadDocument, rt: &CadDocument, format: &str) {
println!("─── Deep Analysis: {} ───────────────────────────────────────", format);
deep_polyface_mesh(orig, rt);
deep_solid3d(orig, rt);
deep_dimension(orig, rt);
deep_insert(orig, rt);
deep_notifications(rt, format);
println!();
}
fn deep_polyface_mesh(orig: &CadDocument, rt: &CadDocument) {
let orig_meshes: Vec<_> = orig
.entities()
.filter_map(|e| if let EntityType::PolyfaceMesh(m) = e { Some(m) } else { None })
.collect();
let rt_meshes: Vec<_> = rt
.entities()
.filter_map(|e| if let EntityType::PolyfaceMesh(m) = e { Some(m) } else { None })
.collect();
if orig_meshes.is_empty() {
return;
}
println!("\n PolyfaceMesh ({} in original, {} after roundtrip):", orig_meshes.len(), rt_meshes.len());
if rt_meshes.is_empty() {
println!(" ALL PolyfaceMesh entities LOST — check if they were re-read as generic Polyline.");
return;
}
let rt_by_handle: std::collections::HashMap<u64, &acadrust::entities::polyface_mesh::PolyfaceMesh> =
rt_meshes.iter().map(|m| (m.common.handle.value(), *m)).collect();
let mut lost_vertices = 0usize;
let mut lost_faces = 0usize;
let count = orig_meshes.len().min(rt_meshes.len());
let mut unmatched = 0usize;
for (i, o) in orig_meshes.iter().enumerate() {
let r = rt_by_handle
.get(&o.common.handle.value())
.copied()
.or_else(|| rt_meshes.get(i).copied());
let r = match r {
Some(r) => r,
None => { unmatched += 1; continue; }
};
let ov = o.vertices.len();
let rv = r.vertices.len();
let of_ = o.faces.len();
let rf = r.faces.len();
if ov != rv || of_ != rf {
println!(
" [{}] v:{}→{} f:{}→{} layer={:?} handle={:#X}→{:#X} owner={:#X}→{:#X}",
i, ov, rv, of_, rf, o.common.layer,
o.common.handle.value(), r.common.handle.value(),
o.common.owner_handle.value(), r.common.owner_handle.value()
);
lost_vertices += ov.saturating_sub(rv);
lost_faces += of_.saturating_sub(rf);
}
}
if unmatched > 0 {
println!(" {} original mesh(es) had no match in roundtrip output.", unmatched);
}
if lost_vertices == 0 && lost_faces == 0 && unmatched == 0 {
println!(" All PolyfaceMesh vertex/face counts preserved.");
} else {
println!(
" Summary: {} vertices lost, {} faces lost across {} affected meshes",
lost_vertices, lost_faces, count
);
}
}
fn deep_solid3d(orig: &CadDocument, rt: &CadDocument) {
let orig_solids: Vec<_> = orig
.entities()
.filter_map(|e| if let EntityType::Solid3D(s) = e { Some(s) } else { None })
.collect();
let rt_solids: Vec<_> = rt
.entities()
.filter_map(|e| if let EntityType::Solid3D(s) = e { Some(s) } else { None })
.collect();
if orig_solids.is_empty() {
return;
}
println!(
"\n Solid3D ({} entities):",
orig_solids.len()
);
let mut loss_categories: BTreeMap<String, usize> = BTreeMap::new();
let count = orig_solids.len().min(rt_solids.len());
for i in 0..count {
let o = &orig_solids[i];
let r = &rt_solids[i];
let o_acis = &o.acis_data;
let r_acis = &r.acis_data;
let mut issues = Vec::new();
if o_acis.version != r_acis.version {
issues.push(format!("version {:?}→{:?}", o_acis.version, r_acis.version));
}
if o_acis.is_binary != r_acis.is_binary {
issues.push(format!("is_binary {}→{}", o_acis.is_binary, r_acis.is_binary));
}
if !o_acis.sab_data.is_empty() && r_acis.sab_data.is_empty() {
issues.push(format!("sab_data {} bytes → 0 bytes", o_acis.sab_data.len()));
}
if o_acis.sab_data.len() != r_acis.sab_data.len() && !o_acis.sab_data.is_empty() && !r_acis.sab_data.is_empty() {
issues.push(format!("sab_data {} → {} bytes", o_acis.sab_data.len(), r_acis.sab_data.len()));
}
if o_acis.sat_data != r_acis.sat_data {
let o_lines = o_acis.sat_data.lines().count();
let r_lines = r_acis.sat_data.lines().count();
issues.push(format!("sat_data {}-line → {}-line", o_lines, r_lines));
}
if o.uid != r.uid {
issues.push(format!("uid {:?}→{:?}", o.uid, r.uid));
}
if o.point_of_reference != r.point_of_reference {
issues.push(format!(
"point_of_reference {:?}→{:?}",
o.point_of_reference, r.point_of_reference
));
}
if o.wires.len() != r.wires.len() {
issues.push(format!("wires {} → {}", o.wires.len(), r.wires.len()));
}
if o.silhouettes.len() != r.silhouettes.len() {
issues.push(format!("silhouettes {} → {}", o.silhouettes.len(), r.silhouettes.len()));
}
if !issues.is_empty() {
let key = issues.iter().map(|s| {
if s.starts_with("sab_data") { "sab_data_lost".to_string() }
else if s.starts_with("sat_data") { "sat_data_changed".to_string() }
else if s.starts_with("version") { "version_downgrade".to_string() }
else { s.clone() }
}).collect::<Vec<_>>().join("+");
*loss_categories.entry(key).or_insert(0) += 1;
if i < 3 {
println!(" [{}] layer={:?}: {}", i, o.common.layer, issues.join(", "));
}
}
}
if orig_solids.len() > rt_solids.len() {
println!(" MISSING: {} Solid3D entities not present after roundtrip", orig_solids.len() - rt_solids.len());
}
if !loss_categories.is_empty() {
println!(" Loss categories across all {} solids:", count);
for (cat, n) in &loss_categories {
println!(" [{} × {}]", n, cat);
}
} else if count == orig_solids.len() {
println!(" All Solid3D ACIS data preserved.");
}
}
fn deep_dimension(orig: &CadDocument, rt: &CadDocument) {
let orig_dims: Vec<_> = orig
.entities()
.filter_map(|e| if let EntityType::Dimension(d) = e { Some(d) } else { None })
.collect();
let rt_dims: Vec<_> = rt
.entities()
.filter_map(|e| if let EntityType::Dimension(d) = e { Some(d) } else { None })
.collect();
if orig_dims.is_empty() {
return;
}
println!(
"\n Dimension ({} in original, {} after roundtrip):",
orig_dims.len(),
rt_dims.len()
);
let count = orig_dims.len().min(rt_dims.len());
let mut field_loss: BTreeMap<String, usize> = BTreeMap::new();
for i in 0..count {
let ob = orig_dims[i].base();
let rb = rt_dims[i].base();
macro_rules! track {
($field:ident) => {
if ob.$field != rb.$field {
*field_loss.entry(stringify!($field).to_string()).or_insert(0) += 1;
if i < 2 {
println!(" [{}] {}: {:?} → {:?}", i, stringify!($field), ob.$field, rb.$field);
}
}
};
}
track!(definition_point);
track!(text_middle_point);
track!(insertion_point);
track!(text);
track!(normal);
track!(text_rotation);
track!(horizontal_direction);
track!(style_name);
track!(line_spacing_factor);
track!(attachment_point);
}
if field_loss.is_empty() {
println!(" All Dimension base fields preserved.");
} else {
println!(" Fields lost across {} dimensions:", count);
for (field, n) in &field_loss {
println!(" {} lost in {}/{} dimensions", field, n, count);
}
}
}
fn deep_insert(orig: &CadDocument, rt: &CadDocument) {
let orig_inserts: Vec<_> = orig
.entities()
.filter_map(|e| if let EntityType::Insert(i) = e { Some(i) } else { None })
.collect();
let rt_inserts: Vec<_> = rt
.entities()
.filter_map(|e| if let EntityType::Insert(i) = e { Some(i) } else { None })
.collect();
if orig_inserts.is_empty() {
return;
}
let count = orig_inserts.len().min(rt_inserts.len());
let mut attr_losses = 0usize;
let mut name_losses = 0usize;
let mut point_losses = 0usize;
let mut changed_names: Vec<(String, String)> = Vec::new();
for i in 0..count {
let o = &orig_inserts[i];
let r = &rt_inserts[i];
if o.block_name != r.block_name {
name_losses += 1;
changed_names.push((o.block_name.clone(), r.block_name.clone()));
}
if o.insert_point != r.insert_point { point_losses += 1; }
if o.attributes.len() != r.attributes.len() { attr_losses += 1; }
}
println!(
"\n Insert ({} entities):",
orig_inserts.len()
);
if name_losses == 0 && point_losses == 0 && attr_losses == 0 {
println!(" All Insert block_name, insert_point, and attribute counts preserved.");
} else {
if name_losses > 0 {
println!(" block_name changed: {} inserts", name_losses);
for (orig, rt) in changed_names.iter().take(10) {
println!(" {:?} -> {:?}", orig, rt);
}
}
if point_losses > 0 { println!(" insert_point changed: {} inserts", point_losses); }
if attr_losses > 0 { println!(" attribute count changed: {} inserts", attr_losses); }
}
}
fn deep_notifications(rt: &CadDocument, format: &str) {
let notifs: Vec<_> = rt.notifications.iter().collect();
let errors: Vec<_> = notifs
.iter()
.filter(|n| {
matches!(
n.notification_type,
acadrust::notification::NotificationType::Error
| acadrust::notification::NotificationType::Warning
) && !n.message.starts_with("Reading DWG file")
&& !n.message.starts_with("AC1021")
&& !n.message.starts_with("AC18")
&& !n.message.starts_with(" Section")
})
.collect();
if errors.is_empty() {
println!("\n {} re-read notifications: none (clean re-read).", format);
return;
}
let mut seen: BTreeMap<String, usize> = BTreeMap::new();
for n in &errors {
let key = n.message.chars().take(80).collect::<String>();
*seen.entry(key).or_insert(0) += 1;
}
println!("\n {} re-read warnings/errors ({} unique types):", format, seen.len());
for (msg, count) in seen.iter().take(20) {
if *count > 1 {
println!(" [{} ×] {}", count, msg);
} else {
println!(" {}", msg);
}
}
if seen.len() > 20 {
println!(" ... and {} more notification types", seen.len() - 20);
}
}