forked from 1b-agent/opencadstudio-upstream
Guard constraint solving and DWG export against degenerate geometry
Retaining a line's length added a P2PDistance constraint, whose gradient divides by the current distance. For a zero-length line the solver got a NaN Jacobian and its SVD fallback never returned, freezing the editor (e.g. ECONSTRAINT on a line drawn by clicking one point twice). Hold coincident endpoints together with per-axis differences instead. Control-point splines read without knots are given a degree their control points can carry (at most n - 1) and a clamped knot vector when a drawing loads. Generating knots for fewer points than the degree underflowed during DWG write: a panic in debug builds, an unbounded allocation in release, and reachable from autosave. A lone control point with no knots or fit points is purged as corrupt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a3d65e2fc8
commit
1ac9309f85
3 files changed
+110
-6
No files matched your search
@@ -2146,8 +2146,12 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
|
||||
&& (s.knots.iter().any(|k| !k.is_finite())
|
||||
|| s.knots.windows(2).any(|w| w[1] < w[0])
|
||||
|| (!compact_periodic_knots && s.knots.len() != n + deg + 1));
|
||||
// A single control point with no knots and no fit points cannot
|
||||
// form a curve, and no knot vector exists for it.
|
||||
let lone_point = n == 1 && s.knots.is_empty() && s.fit_points.is_empty();
|
||||
n >= MAX_VERTS
|
||||
|| degree_bad
|
||||
|| lone_point
|
||||
|| s.control_points.iter().any(|p| !finite_vec3(p))
|
||||
|| knots_bad
|
||||
}
|
||||
@@ -2155,8 +2159,32 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Give control-point splines stored without knots a degree their control
|
||||
/// points can carry, and the matching clamped knot vector.
|
||||
///
|
||||
/// A degree-p curve needs at least p + 1 control points. Knot generation
|
||||
/// (here, and in the DWG writer for knot-less splines) subtracts in `usize`
|
||||
/// and underflows when there are fewer; in release builds the wrapped count
|
||||
/// drives an unbounded allocation. Lowering the degree keeps the geometry the
|
||||
/// points describe: two points are a straight segment at any degree.
|
||||
fn normalize_knotless_splines(doc: &mut CadDocument) {
|
||||
for entity in doc.entities_mut() {
|
||||
let EntityType::Spline(spline) = entity else {
|
||||
continue;
|
||||
};
|
||||
let n = spline.control_points.len();
|
||||
if !spline.knots.is_empty() || n < 2 || spline.degree < 1 {
|
||||
continue;
|
||||
}
|
||||
let degree = (spline.degree as usize).min(n - 1);
|
||||
spline.degree = degree as i32;
|
||||
spline.knots = acadrust::entities::Spline::generate_clamped_knots(degree, n);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn purge_corrupt_entities(doc: &mut CadDocument) -> usize {
|
||||
use crate::par::prelude::*;
|
||||
normalize_knotless_splines(doc);
|
||||
// Detection is pure and read-only; the per-vertex finite/extent checks on
|
||||
// large polylines dominate, so fan the scan out across cores. Gather
|
||||
// entity references in one pass, test in parallel, then remove serially
|
||||
@@ -2363,6 +2391,42 @@ mod corrupt_guard_tests {
|
||||
use acadrust::entities::{Arc, Circle, EntityType, Spline};
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
fn knotless_spline(degree: i32, points: usize) -> Spline {
|
||||
let mut spline = Spline::new();
|
||||
spline.degree = degree;
|
||||
spline.control_points = (0..points)
|
||||
.map(|index| Vector3::new(index as f64, index as f64, 0.0))
|
||||
.collect();
|
||||
spline
|
||||
}
|
||||
|
||||
// Degree 3 over two control points with no knot vector: generating knots
|
||||
// for it underflows, and writing it to DWG used to panic (debug) or
|
||||
// allocate without bound (release).
|
||||
#[test]
|
||||
fn knotless_spline_with_too_few_points_is_lowered_and_saves_as_dwg() {
|
||||
let mut doc = CadDocument::new();
|
||||
doc.add_entity(EntityType::Spline(knotless_spline(3, 2)));
|
||||
doc.add_entity(EntityType::Spline(knotless_spline(3, 1)));
|
||||
assert_eq!(purge_corrupt_entities(&mut doc), 1, "the lone point is dropped");
|
||||
let spline = doc
|
||||
.entities()
|
||||
.find_map(|entity| match entity {
|
||||
EntityType::Spline(spline) => Some(spline.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("the two-point spline is kept");
|
||||
assert_eq!(spline.degree, 1);
|
||||
assert_eq!(spline.knots, vec![0.0, 0.0, 1.0, 1.0]);
|
||||
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ocs_knotless_spline_{}.dwg",
|
||||
std::process::id()
|
||||
));
|
||||
save_as_version(&doc, &path, acadrust::DxfVersion::AC1032).expect("save");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
// Small but finite arcs are valid records. Kernel tessellation is bounded,
|
||||
// so opening must retain them instead of treating their size as corruption.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user