feat(io): drop parser-garbage entities at load time
acadrust's DWG reader occasionally desynchronises on certain files (observed on render.dwg: 1519 of 1763 LwPolylines arrived with non-unit normals in the 1e200+ range and 100k phantom vertices). Tessellating those entities allocates gigabytes and produces NaN-laden wires, freezing the UI. Add a pre-tessellation guard that scans freshly loaded documents and drops any entity failing a cheap sanity check: non-unit normal, non-finite or absurd coordinate, or vertex count above 100k. Count of dropped entities is threaded through DerivedCaches and surfaced as a warning in the command line so the user knows the file was patched up on the way in. Same file in DXF form parses cleanly (0 corrupt), so this is strictly a DWG-path workaround until the upstream parser bug is fixed. The new examples/inspect_polyline.rs is a one-off diagnostic that dumps LwPolyline/Polyline2D/Polyline3D fields plus a corrupt-entity count; useful for narrowing down the acadrust bug. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
586df77b2c
commit
b2588e1790
4 changed files with 270 additions and 2 deletions
174
examples/inspect_polyline.rs
Normal file
174
examples/inspect_polyline.rs
Normal file
|
|
@ -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 -- <path-to-dwg>
|
||||||
|
|
||||||
|
use acadrust::entities::EntityType;
|
||||||
|
use acadrust::io::dwg::DwgReader;
|
||||||
|
use acadrust::DxfReader;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let path = env::args().nth(1).ok_or("usage: inspect_polyline <file>")?;
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
@ -97,6 +97,12 @@ impl H7CAD {
|
||||||
let entity_count = doc.entities().count();
|
let entity_count = doc.entities().count();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Opened \"{name}\" — {entity_count} entities"));
|
.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());
|
self.app_menu.push_recent(path.clone());
|
||||||
|
|
||||||
let current_is_empty = {
|
let current_is_empty = {
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,11 @@ pub async fn open_path_with_phase(
|
||||||
let phase2 = phase.clone();
|
let phase2 = phase.clone();
|
||||||
let (doc, caches) = std::thread::spawn(move || -> Result<_, String> {
|
let (doc, caches) = std::thread::spawn(move || -> Result<_, String> {
|
||||||
phase2.store(PHASE_PARSING, Ordering::Relaxed);
|
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);
|
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);
|
phase2.store(PHASE_FINALIZING, Ordering::Relaxed);
|
||||||
Ok((doc, caches))
|
Ok((doc, caches))
|
||||||
})
|
})
|
||||||
|
|
@ -222,6 +224,87 @@ pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
||||||
|
|
||||||
// ── Post-load fixups ──────────────────────────────────────────────────────
|
// ── 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<acadrust::Handle> = 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,
|
/// 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.
|
/// 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
|
/// Files from AutoCAD and other tools always set bit 15 for active viewports, leaving bit 0
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ pub struct DerivedCaches {
|
||||||
pub hatches: HashMap<Handle, HatchModel>,
|
pub hatches: HashMap<Handle, HatchModel>,
|
||||||
pub images: HashMap<Handle, ImageModel>,
|
pub images: HashMap<Handle, ImageModel>,
|
||||||
pub meshes: HashMap<Handle, MeshModel>,
|
pub meshes: HashMap<Handle, MeshModel>,
|
||||||
|
/// 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`.
|
/// 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,
|
hatches,
|
||||||
images,
|
images,
|
||||||
meshes,
|
meshes,
|
||||||
|
corrupt_dropped: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue