fix(io): keep zero-radius circles and arcs on open

Problem

The open guard rejects circles and arcs with a radius below 1e-10. A normal
open fails when a drawing contains one. The user gets the recovery prompt.
AutoCAD and nanoCAD open these drawings.

The check came from ace99541. Its comment says the tessellator cannot sample
a degenerate curve. That was true of truck's parameter_division. Commit
8c7ebdc9 removed truck. Commit 96be8214 made the kernel sample circles and
arcs by sweep angle with a floored segment count. The reason for the check
no longer exists.

Change

- Remove the near-zero radius check for circles and arcs.
- Keep the checks for non-finite values and radii above 1e10.
- Update the guard comments. The purge rejects parser desync. It does not
  reject degenerate geometry. Be generous in what you accept and strict in
  what you produce.
- Add tests: a zero radius passes the guard, an absurd radius does not.
- Change the automation purge test sample from radius 0 to radius 1e11.

Behavior

- Zero-radius circles and arcs load. They render as dots.
- Tessellation, hit testing, snapping, and hatch boundaries accept a zero
  radius.
- The DXF writer does not change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Christopher OverbeckandClaude Fable 5.1 committed 2026-09-15 16:17:29 -05:00
1 parent 7aff9fa046
commit 8e14b10104
2 files changed
+49 -9

No files matched your search

+3 -1
View File
@@ -1414,7 +1414,9 @@ mod tests {
doc.add_entity(acadrust::EntityType::Circle(good)).unwrap();
let mut corrupt = acadrust::entities::Circle::new();
corrupt.center = acadrust::types::Vector3::new(1.0, 1.0, 0.0);
corrupt.radius = 0.0; // io::is_entity_corrupt rejects a zero-radius circle
// Past any drawing's extent: io::is_entity_corrupt treats it as parser
// desync. (A zero radius is legal geometry and survives the purge.)
corrupt.radius = 1.0e11;
doc.add_entity(acadrust::EntityType::Circle(corrupt)).unwrap();
let bytes = crate::io::save_to_bytes(&doc, "dxf", doc.version)
.expect("save a document containing a corrupt entity");
+46 -8
View File
@@ -2022,6 +2022,12 @@ fn sync_current_styles_on_save(doc: &mut CadDocument) {
// `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.
//
// The checks target what a desynchronised parser produces, not what a CAD
// package might legitimately write. Degenerate-but-legal geometry — a
// zero-radius circle, a zero-sweep arc, a zero-length line — is kept, since
// the strict open path treats every drop as a corrupt file and sends the
// user through recovery for it.
fn finite_unit_normal(n: &acadrust::types::Vector3) -> bool {
let (x, y, z) = (n.x, n.y, n.z);
@@ -2079,20 +2085,23 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
|| p.vertices.iter().any(|v| !finite_vec3(&v.location))
}
E::Line(l) => !finite_vec3(&l.start) || !finite_vec3(&l.end),
// A zero or near-zero radius is legal, if useless, geometry: AutoCAD
// and nanoCAD write and open such circles and arcs, and real
// drawings carry them (collapsed dimension arcs, point markers).
// Every consumer here copes with it — the kernel tessellates by
// sweep angle and floors the segment count, the circle shader draws
// the distance to the radius, and the tangent snap solver returns
// nothing when the cursor sits inside the radius — so only the
// desync signatures are rejected: non-finite values and radii past
// any drawing's extent.
E::Circle(c) => {
!finite_vec3(&c.center)
|| !finite_coord(c.radius)
// Reject zero- or near-zero circles: they tessellate into a
// degenerate curve the tessellator cannot sample.
|| c.radius.abs() < 1.0e-10
|| c.radius.abs() > 1.0e10
!finite_vec3(&c.center) || !finite_coord(c.radius) || c.radius.abs() > 1.0e10
}
E::Arc(a) => {
!finite_vec3(&a.center)
|| !finite_coord(a.radius)
|| !a.start_angle.is_finite()
|| !a.end_angle.is_finite()
|| a.radius.abs() < 1.0e-10
|| a.radius.abs() > 1.0e10
|| !finite_unit_normal(&a.normal)
}
@@ -2342,7 +2351,7 @@ mod layer_roundtrip_tests {
#[cfg(test)]
mod corrupt_guard_tests {
use super::*;
use acadrust::entities::{Arc, EntityType, Spline};
use acadrust::entities::{Arc, Circle, EntityType, Spline};
use acadrust::types::Vector3;
// Small but finite arcs are valid records. Kernel tessellation is bounded,
@@ -2382,6 +2391,35 @@ mod corrupt_guard_tests {
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
// Zero-radius circles and arcs are legal DXF that AutoCAD and nanoCAD
// open without complaint. A strict open that drops them fails the whole
// file, so they must pass the guard; only absurd radii are desync.
#[test]
fn keeps_zero_radius_circle_and_arc() {
let mut c = Circle::new();
c.center = Vector3::new(206.2, 150.7, 0.0);
c.radius = 0.0;
assert!(!is_entity_corrupt(&EntityType::Circle(c)));
let mut a = Arc::new();
a.center = Vector3::new(223.5, 174.5, 0.0);
a.radius = 0.0;
a.start_angle = 0.0;
a.end_angle = 0.0;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
#[test]
fn drops_absurd_radius_circle() {
let mut c = Circle::new();
c.radius = 1.0e11;
assert!(is_entity_corrupt(&EntityType::Circle(c)));
let mut c = Circle::new();
c.radius = f64::NAN;
assert!(is_entity_corrupt(&EntityType::Circle(c)));
}
// Parser desync emits 100_000-control-point splines; building a kernel
// NURBS from one and tessellating it OOMs. The control-point cap rejects it.
#[test]