diff --git a/examples/inspect_polyline.rs b/examples/inspect_polyline.rs new file mode 100644 index 000000000..bd5994f51 --- /dev/null +++ b/examples/inspect_polyline.rs @@ -0,0 +1,174 @@ +// One-off inspector for diagnosing a DWG: dumps every LwPolyline/Polyline2D/ +// Polyline3D with its elevation, normal vector, and per-vertex coordinates. +// Run with: cargo run --example inspect_polyline -- + +use acadrust::entities::EntityType; +use acadrust::io::dwg::DwgReader; +use acadrust::DxfReader; +use std::env; + +fn main() -> Result<(), Box> { + let path = env::args().nth(1).ok_or("usage: inspect_polyline ")?; + let mut doc = if path.to_lowercase().ends_with(".dxf") { + DxfReader::from_file(&path)?.read()? + } else { + DwgReader::from_file(&path)?.read()? + }; + + // Cheap inline corruption check — mirrors the rules in src/io/mod.rs so the + // example reports the same drop count without needing a library crate. + let total = doc.entities().count(); + let bad = doc + .entities() + .filter(|e| { + use acadrust::entities::EntityType as E; + const MAX: usize = 100_000; + let bad_normal = |n: &acadrust::types::Vector3| { + !n.x.is_finite() + || !n.y.is_finite() + || !n.z.is_finite() + || ((n.x * n.x + n.y * n.y + n.z * n.z) - 1.0).abs() >= 0.21 + }; + match e { + E::LwPolyline(p) => { + bad_normal(&p.normal) + || p.vertices.len() > MAX + || !p.elevation.is_finite() + || p.elevation.abs() > 1e10 + } + E::Polyline2D(p) => { + bad_normal(&p.normal) + || p.vertices.len() > MAX + || !p.elevation.is_finite() + || p.elevation.abs() > 1e10 + } + _ => false, + } + }) + .count(); + println!("Entities: total={} corrupt={}", total, bad); + println!(); + let _ = &mut doc; // silence unused-mut warning if we don't mutate below + + let h = &doc.header; + println!( + "Header extents: min=({:.3},{:.3},{:.3}) max=({:.3},{:.3},{:.3})", + h.model_space_extents_min.x, + h.model_space_extents_min.y, + h.model_space_extents_min.z, + h.model_space_extents_max.x, + h.model_space_extents_max.y, + h.model_space_extents_max.z, + ); + println!(); + + let mut lwp = 0usize; + let mut p2d = 0usize; + let mut p3d = 0usize; + let mut pl = 0usize; + + for (i, e) in doc.entities().enumerate() { + match e { + EntityType::LwPolyline(p) => { + lwp += 1; + if p.common.layer != "PLN_Sev" { + continue; + } + let nx_abs = p.normal.x.abs(); + let ny_abs = p.normal.y.abs(); + let corrupt = !nx_abs.is_finite() + || nx_abs > 1.0 + || ny_abs > 1.0 + || p.vertices.len() > 1000; + if corrupt { + println!( + "[{}] CORRUPT handle={:?} verts={} elev={:.3e} normal=({:.2e},{:.2e},{:.2e})", + i, + p.common.handle, + p.vertices.len(), + p.elevation, + p.normal.x, + p.normal.y, + p.normal.z, + ); + } else { + println!( + "[{}] OK handle={:?} verts={} elev={:.3} normal=({:.4},{:.4},{:.4})", + i, + p.common.handle, + p.vertices.len(), + p.elevation, + p.normal.x, + p.normal.y, + p.normal.z, + ); + for (vi, v) in p.vertices.iter().take(3).enumerate() { + println!(" v{}: ({:.3}, {:.3}) bulge={}", vi, v.location.x, v.location.y, v.bulge); + } + } + } + EntityType::Polyline2D(p) => { + p2d += 1; + if p2d <= 5 { + println!( + "[{}] Polyline2D handle={:?} layer={:?}", + i, p.common.handle, p.common.layer + ); + println!( + " elevation={} thickness={} normal=({},{},{})", + p.elevation, p.thickness, p.normal.x, p.normal.y, p.normal.z + ); + println!(" closed={} verts={}", p.is_closed(), p.vertices.len()); + for (vi, v) in p.vertices.iter().enumerate() { + println!( + " v{}: ({}, {}, {}) bulge={}", + vi, v.location.x, v.location.y, v.location.z, v.bulge + ); + } + println!(); + } + } + EntityType::Polyline3D(p) => { + p3d += 1; + if p3d <= 5 { + println!( + "[{}] Polyline3D handle={:?} layer={:?}", + i, p.common.handle, p.common.layer + ); + println!(" closed={} verts={}", p.is_closed(), p.vertices.len()); + for (vi, v) in p.vertices.iter().enumerate() { + println!( + " v{}: ({}, {}, {})", + vi, v.position.x, v.position.y, v.position.z + ); + } + println!(); + } + } + EntityType::Polyline(p) => { + pl += 1; + if pl <= 5 { + println!( + "[{}] Polyline (heavy 3D) handle={:?} layer={:?}", + i, p.common.handle, p.common.layer + ); + println!(" closed={} verts={}", p.flags.is_closed(), p.vertices.len()); + for (vi, v) in p.vertices.iter().enumerate() { + println!( + " v{}: ({}, {}, {})", + vi, v.location.x, v.location.y, v.location.z + ); + } + println!(); + } + } + _ => {} + } + } + + println!( + "Totals: LwPolyline={} Polyline2D={} Polyline3D={} Polyline={}", + lwp, p2d, p3d, pl + ); + Ok(()) +} diff --git a/src/app/update.rs b/src/app/update.rs index 36e83602a..4514e0dbb 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -97,6 +97,12 @@ impl H7CAD { let entity_count = doc.entities().count(); self.command_line .push_output(&format!("Opened \"{name}\" — {entity_count} entities")); + if caches.corrupt_dropped > 0 { + self.command_line.push_error(&format!( + "Warning: {} corrupt entities dropped (parser junk — bad normals / counts)", + caches.corrupt_dropped + )); + } self.app_menu.push_recent(path.clone()); let current_is_empty = { diff --git a/src/io/mod.rs b/src/io/mod.rs index ba526cfe8..f251d16c2 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -61,9 +61,11 @@ pub async fn open_path_with_phase( let phase2 = phase.clone(); let (doc, caches) = std::thread::spawn(move || -> Result<_, String> { phase2.store(PHASE_PARSING, Ordering::Relaxed); - let doc = load_file(&path2)?; + let mut doc = load_file(&path2)?; + let dropped = purge_corrupt_entities(&mut doc); phase2.store(PHASE_CACHING, Ordering::Relaxed); - let caches = crate::scene::build_derived_caches(&doc); + let mut caches = crate::scene::build_derived_caches(&doc); + caches.corrupt_dropped = dropped; phase2.store(PHASE_FINALIZING, Ordering::Relaxed); Ok((doc, caches)) }) @@ -222,6 +224,87 @@ pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> { // ── Post-load fixups ────────────────────────────────────────────────────── +// ── Corrupt-entity guard ────────────────────────────────────────────────── +// +// acadrust's DWG parser occasionally desynchronises on certain files and +// produces entities with garbage fields: non-unit normals (components in +// 1e200+), nonsensical vertex counts (e.g. 100000), or infinite/NaN +// coordinates. Tessellating such entities triggers huge allocations and +// numerical blow-ups in the wire pipeline. +// +// `purge_corrupt_entities` scans the document and removes any entity that +// fails a cheap sanity check, returning the number dropped so the caller can +// surface it to the UI / log. + +fn finite_unit_normal(n: &acadrust::types::Vector3) -> bool { + let (x, y, z) = (n.x, n.y, n.z); + if !x.is_finite() || !y.is_finite() || !z.is_finite() { + return false; + } + let mag2 = x * x + y * y + z * z; + // Accept anything within ~10% of unit length. Real files sometimes + // store slightly denormalised normals from rounding. + (mag2 - 1.0).abs() < 0.21 +} + +fn finite_coord(v: f64) -> bool { + v.is_finite() && v.abs() < 1.0e12 +} + +fn finite_vec3(v: &acadrust::types::Vector3) -> bool { + finite_coord(v.x) && finite_coord(v.y) && finite_coord(v.z) +} + +/// Returns true if the entity looks like parser garbage and should be dropped. +fn is_entity_corrupt(e: &EntityType) -> bool { + use acadrust::entities::EntityType as E; + const MAX_VERTS: usize = 100_000; + match e { + E::LwPolyline(p) => { + !finite_unit_normal(&p.normal) + || p.vertices.len() > MAX_VERTS + || !finite_coord(p.elevation) + || p.elevation.abs() > 1.0e10 + } + E::Polyline2D(p) => { + !finite_unit_normal(&p.normal) + || p.vertices.len() > MAX_VERTS + || !finite_coord(p.elevation) + || p.elevation.abs() > 1.0e10 + } + E::Polyline3D(p) => { + p.vertices.len() > MAX_VERTS + || p.vertices.iter().any(|v| !finite_vec3(&v.position)) + } + E::Polyline(p) => { + p.vertices.len() > MAX_VERTS + || p.vertices.iter().any(|v| !finite_vec3(&v.location)) + } + E::Line(l) => !finite_vec3(&l.start) || !finite_vec3(&l.end), + E::Circle(c) => !finite_vec3(&c.center) || !finite_coord(c.radius), + E::Arc(a) => { + !finite_vec3(&a.center) + || !finite_coord(a.radius) + || !a.start_angle.is_finite() + || !a.end_angle.is_finite() + } + _ => false, + } +} + +pub fn purge_corrupt_entities(doc: &mut CadDocument) -> usize { + let bad: Vec = doc + .entities() + .filter(|e| is_entity_corrupt(e)) + .map(|e| e.common().handle) + .collect(); + let n = bad.len(); + for h in bad { + doc.remove_entity(h); + } + n +} + /// acadrust's ViewportStatusFlags::from_bits() maps bit 0 → is_on and bit 15 → locked, /// but the real DXF/DWG spec uses bit 15 (0x8000) → viewport on and bit 14 (0x4000) → locked. /// Files from AutoCAD and other tools always set bit 15 for active viewports, leaving bit 0 diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1916eea22..cc67dea0b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -69,6 +69,10 @@ pub struct DerivedCaches { pub hatches: HashMap, pub images: HashMap, pub meshes: HashMap, + /// Number of entities removed by the corrupt-entity guard during load. + /// Reported back to the UI so the user knows when a file had parser-junk + /// entities silently dropped. + pub corrupt_dropped: usize, } /// Build hatch / image / mesh caches from a document without needing `&mut Scene`. @@ -191,6 +195,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches { hatches, images, meshes, + corrupt_dropped: 0, } }