Drawing: validate section geometry contracts (#2053) (#2085)

Thanks @0xlillith.
This commit is contained in:
selune authored and GitHub committed 2026-10-18 00:29:12 -04:00
1 parent 36b6496ad5
commit 7c9f8a5895
6 files changed
+1097

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ pub mod mesh_distance;
pub mod network;
mod plane;
mod profile;
pub mod section;
pub mod spline_space;
pub mod tess;
pub mod tspline;
+291
View File
@@ -0,0 +1,291 @@
//! Backend-neutral section geometry in a model-space plane, in millimetres and radians.
//!
//! Curve and wire indices identify items in this result only, not persistent model geometry.
//! Producers own source identity, topology and accuracy evidence. [`SectionGeometry::validate_structure`]
//! checks bounded payloads, references and connected wires; it does not certify simple loops,
//! winding, hole nesting, region separation, source coincidence or an approximation bound.
//! Material producers must supply CCW outer loops and CW holes in the plane's x/y basis,
//! whose normal is x cross y. Conversion preserves those supplied orientations.
//!
//! Stored coordinates are finite and at most 1e12 mm in absolute value. Radii and conic
//! weights lie in 1e-12..=1e12; arc starts in -1e6..=1e6 radians and absolute sweeps in
//! 1e-12..=one turn (with 1e-12 radians of allowance). Positive conic weights are the supported
//! subset. These arithmetic limits do not promise a small absolute error at extreme magnitudes.
//! Closure tolerance is 1e-9..=1 mm, independent of approximation accuracy.
//!
//! Validate after deserialization and before use or serialization. The DTOs deliberately retain
//! [`Plane`]'s existing serde representation and construction behavior. Validation after parsing
//! cannot bound parsing allocations: an encoded-input boundary must also limit bytes and nesting.
//! The per-wire/per-region conversions revalidate the whole result on each call; they are not an
//! optimized batch-conversion API. Existing profile area/containment methods are not general
//! exact or certified measures for conics and freeform curves. Profile values contain geometry
//! only: callers retain the original result's accuracy declarations and local identities.
use std::f64::consts::TAU;
use serde::{Deserialize, Serialize};
use crate::{Loop2, MAX_BEZIER_DEGREE, Plane, Region2, Seg2, Vec2, Vec3};
/// Aggregate input records/references, and the selected conversion's output items, at most.
/// Input counts curves, wires, regions, contacts, curve uses and hole references together.
pub const MAX_ITEMS: usize = 100_000;
const MAX_NUMBER: f64 = 1e12;
const MIN_POSITIVE: f64 = 1e-12;
const MAX_START_ANGLE: f64 = 1e6;
const FRAME_TOLERANCE: f64 = 1e-8;
/// A section's geometric result, without source identity, freshness or persistence semantics.
/// Empty, open-wire and contact-only results remain distinct; no material is inferred from them.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SectionGeometry {
pub plane: Plane,
pub curves: Vec<SectionCurve>,
pub wires: Vec<SectionWire>,
pub regions: Vec<SectionRegion>,
/// Isolated contacts in plane coordinates, not invented closed regions.
pub contacts: Vec<Vec2>,
}
/// One curve and the producer's declared accuracy relative to its source.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SectionCurve {
pub geometry: Seg2,
pub accuracy: CurveAccuracy,
}
/// How the producer represents its source; structure validation cannot certify this declaration.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum CurveAccuracy {
/// An analytic representation without fitting; not a certificate about the source or topology.
Analytic,
/// A producer-established bound, not merely a requested fitting tolerance.
Bounded { max_deviation_mm: f64 },
/// A deviation measured at samples only, with no bound between them. Zero remains sampled.
Sampled { max_sample_deviation_mm: f64 },
}
/// An ordered connected chain. `closed` is explicit, never inferred or repaired.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SectionWire {
pub curves: Vec<CurveUse>,
pub closed: bool,
}
/// An oriented use of a curve by its result-local index.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CurveUse {
pub curve: u32,
pub forward: bool,
}
/// A producer-declared material region referring to closed wires. Shared wires are permitted.
/// The outer wire cannot also be a hole, and a hole cannot occur twice in this one region.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SectionRegion {
pub outer: u32,
pub holes: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("section geometry: {0}")]
pub struct Invalid(pub &'static str);
type Result<T> = std::result::Result<T, Invalid>;
fn require(condition: bool, message: &'static str) -> Result<()> {
if condition { Ok(()) } else { Err(Invalid(message)) }
}
struct Budget(usize);
impl Budget {
fn add(&mut self, n: usize) -> Result<()> {
self.0 = self.0.checked_add(n).ok_or(Invalid("item budget"))?;
require(self.0 <= MAX_ITEMS, "item budget")
}
}
fn numbers(values: impl IntoIterator<Item = f64>) -> Result<()> {
require(values.into_iter().all(|v| v.is_finite() && v.abs() <= MAX_NUMBER), "non-finite or excessive coordinate")
}
fn point(p: Vec2) -> Result<()> {
numbers([p.x, p.y])
}
fn vector(p: Vec3) -> Result<()> {
numbers([p.x, p.y, p.z])
}
/// Strictly check an existing plane without re-orthonormalizing it or changing its serde format.
/// Unit-length and orthogonality checks use 1e-8; the normal is derived as x cross y.
pub fn validate_plane(plane: &Plane) -> Result<()> {
vector(plane.origin)?;
vector(plane.x)?;
vector(plane.y)?;
require(
(plane.x.len() - 1.0).abs() <= FRAME_TOLERANCE
&& (plane.y.len() - 1.0).abs() <= FRAME_TOLERANCE
&& plane.x.dot(plane.y).abs() <= FRAME_TOLERANCE,
"nonorthonormal plane",
)
}
fn controls(points: &[Vec2]) -> Result<()> {
for p in points {
point(*p)?;
}
let Some((first, rest)) = points.split_first() else { return Err(Invalid("empty controls")) };
require(rest.iter().any(|p| p != first), "collapsed curve")
}
fn curve(curve: &SectionCurve) -> Result<()> {
match curve.geometry {
Seg2::Line { a, b } => controls(&[a, b])?,
Seg2::Arc { center, radius, start, sweep } => {
point(center)?;
require(radius.is_finite() && (MIN_POSITIVE..=MAX_NUMBER).contains(&radius), "arc radius")?;
require(start.is_finite() && start.abs() <= MAX_START_ANGLE, "arc start angle")?;
require(sweep.is_finite() && (MIN_POSITIVE..=TAU + 1e-12).contains(&sweep.abs()), "arc sweep")?;
}
Seg2::Cubic { p0, p1, p2, p3 } => controls(&[p0, p1, p2, p3])?,
Seg2::Conic { a, apex, b, w } => {
controls(&[a, apex, b])?;
require(w.is_finite() && (MIN_POSITIVE..=MAX_NUMBER).contains(&w), "conic weight")?;
}
Seg2::Bezier { n, ref p } => {
require((4..=MAX_BEZIER_DEGREE).contains(&usize::from(n)), "Bezier degree")?;
// The unused slots are serialized too: reject hidden non-finite/excessive values.
for q in p {
point(*q)?;
}
controls(p.get(..=usize::from(n)).ok_or(Invalid("Bezier degree"))?)?;
}
}
require(curve.geometry.start().is_finite() && curve.geometry.end().is_finite(), "non-finite curve endpoint")?;
match curve.accuracy {
CurveAccuracy::Analytic => Ok(()),
CurveAccuracy::Bounded { max_deviation_mm } => deviation(max_deviation_mm),
CurveAccuracy::Sampled { max_sample_deviation_mm } => deviation(max_sample_deviation_mm),
}
}
fn deviation(value: f64) -> Result<()> {
require(value.is_finite() && (0.0..=MAX_NUMBER).contains(&value), "accuracy deviation")
}
impl SectionGeometry {
/// Check finite supported payloads, aggregate counts, references and wire connectivity.
/// This does not certify material validity, winding, nesting, source identity or accuracy.
/// Count preflight precedes geometry evaluation; encoded input needs separate parsing limits.
pub fn validate_structure(&self, closure_tolerance_mm: f64) -> Result<()> {
let mut budget = Budget(0);
budget.add(self.curves.len())?;
budget.add(self.wires.len())?;
budget.add(self.regions.len())?;
budget.add(self.contacts.len())?;
for wire in &self.wires {
budget.add(wire.curves.len())?;
}
for region in &self.regions {
budget.add(region.holes.len())?;
}
require(closure_tolerance_mm.is_finite() && (1e-9..=1.0).contains(&closure_tolerance_mm), "closure tolerance must be between 1e-9 and 1 mm")?;
validate_plane(&self.plane)?;
for c in &self.curves {
curve(c)?;
}
for p in &self.contacts {
point(*p)?;
}
for wire in &self.wires {
require(!wire.curves.is_empty(), "empty wire")?;
let mut first = None;
let mut last: Option<Vec2> = None;
for use_ in &wire.curves {
let c = &self.curves.get(use_.curve as usize).ok_or(Invalid("curve reference"))?.geometry;
let (a, b) = (c.start(), c.end());
let (start, end) = if use_.forward { (a, b) } else { (b, a) };
require(last.is_none_or(|p| p.dist(start) <= closure_tolerance_mm), "disconnected wire")?;
first.get_or_insert(start);
last = Some(end);
}
if wire.closed {
require(first.zip(last).is_some_and(|(a, b)| a.dist(b) <= closure_tolerance_mm), "open closed wire")?;
}
}
// One stamp array for all regions: neither pairwise duplicate checks nor clearing the
// entire array per region. Sharing the same wire between different regions is allowed.
let mut seen = vec![usize::MAX; self.wires.len()];
for (stamp, region) in self.regions.iter().enumerate() {
for id in std::iter::once(&region.outer).chain(&region.holes) {
let wire = self.wires.get(*id as usize).ok_or(Invalid("wire reference"))?;
require(wire.closed, "region boundary is not closed")?;
let mark = seen.get_mut(*id as usize).ok_or(Invalid("wire reference"))?;
require(*mark != stamp, "duplicate region boundary")?;
*mark = stamp;
}
}
Ok(())
}
/// Copy one closed wire to a profile loop, preserving its oriented curve uses.
/// Revalidates the whole result; an open wire is never silently closed. Floating-point
/// reversal must also keep the converted loop connected within the same tolerance.
pub fn to_loop(&self, wire: u32, closure_tolerance_mm: f64) -> Result<Loop2> {
self.validate_structure(closure_tolerance_mm)?;
let wire = self.wires.get(wire as usize).ok_or(Invalid("wire reference"))?;
require(wire.closed, "wire is not closed")?;
let mut budget = Budget(0);
budget.add(1)?;
budget.add(wire.curves.len())?;
self.copy_loop(wire, closure_tolerance_mm)
}
/// Copy one declared region to a profile, preserving winding and hole order.
/// Revalidates the whole result and preflights expanded output before allocating it.
/// This does not certify the producer's region. The original result is unchanged; the
/// returned profile contains no accuracy declarations or result-local identities.
pub fn to_region(&self, region: u32, closure_tolerance_mm: f64) -> Result<Region2> {
self.validate_structure(closure_tolerance_mm)?;
let region = self.regions.get(region as usize).ok_or(Invalid("region reference"))?;
let mut budget = Budget(0);
budget.add(1)?;
budget.add(region.holes.len())?;
for id in std::iter::once(&region.outer).chain(&region.holes) {
let wire = self.wires.get(*id as usize).ok_or(Invalid("wire reference"))?;
budget.add(wire.curves.len())?;
}
let outer = self.copy_loop(self.wires.get(region.outer as usize).ok_or(Invalid("wire reference"))?, closure_tolerance_mm)?;
let mut holes = Vec::with_capacity(region.holes.len());
for id in &region.holes {
holes.push(self.copy_loop(self.wires.get(*id as usize).ok_or(Invalid("wire reference"))?, closure_tolerance_mm)?);
}
Ok(Region2 { outer, holes })
}
// Called only while the publicly validated result remains immutably borrowed and the
// selected output's expansion has been counted. Index access stays fallible nonetheless.
fn copy_loop(&self, wire: &SectionWire, closure_tolerance_mm: f64) -> Result<Loop2> {
let mut segs = Vec::with_capacity(wire.curves.len());
for use_ in &wire.curves {
let c = self.curves.get(use_.curve as usize).ok_or(Invalid("curve reference"))?.geometry;
segs.push(if use_.forward { c } else { c.reversed() });
}
let loop_ = Loop2 { segs };
// Reversing an arc changes its stored start to start + sweep. Subtracting the sweep
// at its new end need not recover the original angle exactly at large radii.
require(loop_.is_closed(closure_tolerance_mm), "converted loop is not closed within tolerance")?;
Ok(loop_)
}
}
#[cfg(test)]
#[path = "section_tests.rs"]
mod tests;
+409
View File
@@ -0,0 +1,409 @@
use std::f64::consts::PI;
use super::*;
use crate::{MAX_BEZIER_CTRL, bezier};
const TOL: f64 = 1e-6;
fn empty(plane: Plane) -> SectionGeometry {
SectionGeometry { plane, curves: vec![], wires: vec![], regions: vec![], contacts: vec![] }
}
fn analytic(geometry: Seg2) -> SectionCurve {
SectionCurve { geometry, accuracy: CurveAccuracy::Analytic }
}
fn used(curve: u32, forward: bool) -> CurveUse {
CurveUse { curve, forward }
}
fn plate() -> SectionGeometry {
let p = Vec2::new;
SectionGeometry {
plane: Plane::XY.offset(10.0),
curves: vec![
// Deliberately stored backwards: the wire's use restores the CCW rectangle.
analytic(Seg2::Line { a: p(100.0, 0.0), b: p(0.0, 0.0) }),
analytic(Seg2::Line { a: p(100.0, 0.0), b: p(100.0, 60.0) }),
analytic(Seg2::Line { a: p(100.0, 60.0), b: p(0.0, 60.0) }),
analytic(Seg2::Line { a: p(0.0, 60.0), b: p(0.0, 0.0) }),
analytic(Seg2::Arc { center: p(50.0, 30.0), radius: 5.0, start: 0.0, sweep: PI }),
analytic(Seg2::Arc { center: p(50.0, 30.0), radius: 5.0, start: PI, sweep: PI }),
],
wires: vec![
SectionWire { curves: vec![used(0, false), used(1, true), used(2, true), used(3, true)], closed: true },
SectionWire { curves: vec![used(5, false), used(4, false)], closed: true },
],
regions: vec![SectionRegion { outer: 0, holes: vec![1] }],
contacts: vec![],
}
}
#[test]
fn section_frame_preserves_named_translated_tilted_offset_and_flip_conventions() {
for plane in [Plane::XY, Plane::XZ, Plane::YZ] {
validate_plane(&plane).unwrap();
}
assert_eq!(Plane::XZ.normal(), Vec3::Y);
assert_eq!(Plane::XZ.to_world(Vec2::new(3.0, 4.0)), Vec3::new(3.0, 0.0, -4.0));
let plane = Plane::from_normal(Vec3::new(13.0, -7.0, 19.0), Vec3::new(2.0, -3.0, 5.0)).unwrap();
validate_plane(&plane).unwrap();
let local = Vec2::new(7.0, -11.0);
let height = 4.5;
let point = plane.to_world(local) + plane.normal() * height;
assert!(plane.to_local(point).dist(local) < 1e-12);
assert!((plane.height(point) - height).abs() < 1e-12);
let moved = plane.offset(8.0);
validate_plane(&moved).unwrap();
assert!((plane.height(moved.origin) - 8.0).abs() < 1e-12);
for about_y in [false, true] {
let flipped = moved.flipped(about_y);
validate_plane(&flipped).unwrap();
assert_eq!(flipped.origin, moved.origin);
assert_eq!(flipped.normal(), -moved.normal());
assert_eq!(flipped.flipped(about_y), moved);
let on_plane = moved.to_world(local);
let mirrored = if about_y { Vec2::new(-local.x, local.y) } else { Vec2::new(local.x, -local.y) };
assert!(flipped.to_local(on_plane).dist(mirrored) < 1e-12);
assert!((flipped.height(point) + moved.height(point)).abs() < 1e-12);
}
}
#[test]
fn strict_frame_validation_does_not_repair_raw_or_deserialized_axes() {
let bad = Plane { x: Vec3::X * 2.0, ..Plane::XY };
assert!(Plane::new(bad.origin, bad.x, bad.y).is_some());
assert_eq!(validate_plane(&bad), Err(Invalid("nonorthonormal plane")));
assert_eq!(bad.x, Vec3::X * 2.0);
let restored: Plane = serde_json::from_value(serde_json::json!({
"origin": [0, 0, 0], "x_dir": [1, 0, 0], "y_dir": [1, 1, 0]
}))
.unwrap();
assert_eq!(validate_plane(&restored), Err(Invalid("nonorthonormal plane")));
for plane in [
Plane { origin: Vec3::new(f64::NAN, 0.0, 0.0), ..Plane::XY },
Plane { origin: Vec3::new(1e12 + 1.0, 0.0, 0.0), ..Plane::XY },
Plane { x: Vec3::new(f64::MAX, 0.0, 0.0), ..Plane::XY },
Plane { y: Vec3::new(0.0, f64::INFINITY, 0.0), ..Plane::XY },
Plane { x: Vec3::ZERO, ..Plane::XY },
Plane { y: Vec3::X, ..Plane::XY },
] {
assert!(validate_plane(&plane).is_err());
}
validate_plane(&Plane { origin: Vec3::new(1e12, -1e12, 1e12), ..Plane::XY }).unwrap();
}
#[test]
fn line_arc_region_conversion_preserves_winding_and_analytic_area() {
let section = plate();
let before = section.clone();
section.validate_structure(TOL).unwrap();
let outer = section.to_loop(0, TOL).unwrap();
let hole = section.to_loop(1, TOL).unwrap();
assert!(outer.is_closed(TOL) && hole.is_closed(TOL));
assert!((outer.signed_area() - 6000.0).abs() < 1e-10);
assert!((hole.signed_area() + 25.0 * PI).abs() < 1e-10);
let region = section.to_region(0, TOL).unwrap();
assert_eq!(region.outer, outer);
assert_eq!(region.holes, vec![hole]);
assert!((region.area() - (6000.0 - 25.0 * PI)).abs() < 1e-10);
assert_eq!(section, before);
}
#[test]
fn empty_contacts_and_open_wires_remain_distinct_without_inferred_material() {
let empty = empty(Plane::XY);
empty.validate_structure(TOL).unwrap();
assert_eq!(empty.to_region(0, TOL), Err(Invalid("region reference")));
let mut contact = empty.clone();
contact.contacts.push(Vec2::new(2.0, 3.0));
contact.validate_structure(TOL).unwrap();
assert_ne!(contact, empty);
let mut open = empty.clone();
open.curves = vec![analytic(Seg2::Line { a: Vec2::ZERO, b: Vec2::X }), analytic(Seg2::Line { a: Vec2::X, b: Vec2::new(1.0, 1.0) })];
open.wires = vec![SectionWire { curves: vec![used(0, true), used(1, true)], closed: false }];
open.validate_structure(TOL).unwrap();
assert_eq!(open.to_loop(0, TOL), Err(Invalid("wire is not closed")));
open.regions.push(SectionRegion { outer: 0, holes: vec![] });
assert_eq!(open.validate_structure(TOL), Err(Invalid("region boundary is not closed")));
// Even a geometrically closed chain remains explicitly open when the producer says so.
let mut declared_open = plate();
declared_open.regions.clear();
declared_open.wires[0].closed = false;
declared_open.validate_structure(TOL).unwrap();
assert_eq!(declared_open.to_loop(0, TOL), Err(Invalid("wire is not closed")));
}
#[test]
fn bad_references_empty_disconnected_and_unclosed_wires_fail() {
let mut section = plate();
section.wires[0].curves[0].curve = u32::MAX;
assert_eq!(section.validate_structure(TOL), Err(Invalid("curve reference")));
let mut section = plate();
section.wires[0].curves.clear();
assert_eq!(section.validate_structure(TOL), Err(Invalid("empty wire")));
let mut section = plate();
section.wires[0].curves[1].forward = false;
assert_eq!(section.validate_structure(TOL), Err(Invalid("disconnected wire")));
let mut section = plate();
section.wires[0].curves.pop();
assert_eq!(section.validate_structure(TOL), Err(Invalid("open closed wire")));
let mut section = plate();
section.regions[0].outer = u32::MAX;
assert_eq!(section.validate_structure(TOL), Err(Invalid("wire reference")));
let mut section = plate();
section.regions[0].holes[0] = u32::MAX;
assert_eq!(section.validate_structure(TOL), Err(Invalid("wire reference")));
assert_eq!(plate().to_loop(u32::MAX, TOL), Err(Invalid("wire reference")));
assert_eq!(plate().to_region(u32::MAX, TOL), Err(Invalid("region reference")));
}
#[test]
fn duplicate_boundaries_are_local_to_a_region_and_large_reference_tables_are_bounded() {
let mut section = plate();
section.regions[0].holes.push(1);
assert_eq!(section.validate_structure(TOL), Err(Invalid("duplicate region boundary")));
let mut section = plate();
section.regions[0].holes.push(0);
assert_eq!(section.validate_structure(TOL), Err(Invalid("duplicate region boundary")));
let mut section = plate();
section.regions.push(section.regions[0].clone());
section.validate_structure(TOL).unwrap();
assert_eq!(section.to_region(0, TOL).unwrap(), section.to_region(1, TOL).unwrap());
// Deliberately coincident boundaries: this tests a large legal reference graph, not material.
let mut many = empty(Plane::XY);
many.curves.push(analytic(Seg2::Arc { center: Vec2::ZERO, radius: 1.0, start: 0.0, sweep: TAU }));
many.wires = vec![SectionWire { curves: vec![used(0, true)], closed: true }; 201];
many.regions = vec![SectionRegion { outer: 0, holes: (1..=200).collect() }; 200];
many.validate_structure(TOL).unwrap();
let last = many.regions.last_mut().unwrap();
*last.holes.last_mut().unwrap() = 1;
assert_eq!(many.validate_structure(TOL), Err(Invalid("duplicate region boundary")));
}
#[test]
fn aggregate_counts_fail_before_geometry_or_reference_inspection() {
let mut section = empty(Plane::XY);
section.contacts = vec![Vec2::new(f64::NAN, 0.0); MAX_ITEMS + 1];
assert_eq!(section.validate_structure(TOL), Err(Invalid("item budget")));
let mut section = empty(Plane::XY);
section.wires.push(SectionWire { curves: vec![used(u32::MAX, false); MAX_ITEMS], closed: false });
assert_eq!(section.validate_structure(TOL), Err(Invalid("item budget")));
let mut section = empty(Plane::XY);
section.regions.push(SectionRegion { outer: u32::MAX, holes: vec![u32::MAX; MAX_ITEMS] });
assert_eq!(section.validate_structure(TOL), Err(Invalid("item budget")));
let mut section = empty(Plane::XY);
section.contacts = vec![Vec2::ZERO; MAX_ITEMS / 2];
section.wires.push(SectionWire { curves: vec![used(u32::MAX, false); MAX_ITEMS / 2], closed: false });
assert_eq!(section.validate_structure(TOL), Err(Invalid("item budget")));
// Exactly the item limit is accepted, independently of these points' material meaning.
let mut boundary = empty(Plane::XY);
boundary.contacts = vec![Vec2::ZERO; MAX_ITEMS];
boundary.validate_structure(TOL).unwrap();
let mut count = Budget(MAX_ITEMS);
assert_eq!(count.add(usize::MAX), Err(Invalid("item budget")));
}
#[test]
fn closure_tolerance_is_bounded_and_not_used_to_collapse_short_curves() {
for tolerance in [f64::NAN, f64::INFINITY, -1.0, 0.0, 1e-10, 1.01] {
assert!(plate().validate_structure(tolerance).is_err());
}
for tolerance in [1e-9, 1.0] {
plate().validate_structure(tolerance).unwrap();
}
let mut section = empty(Plane::XY);
section.curves.push(analytic(Seg2::Line { a: Vec2::ZERO, b: Vec2::new(1e-10, 0.0) }));
section.validate_structure(1.0).unwrap();
let mut gap = plate();
if let Seg2::Line { b, .. } = &mut gap.curves[3].geometry {
b.x = 5e-7;
}
gap.validate_structure(1e-6).unwrap();
assert_eq!(gap.validate_structure(1e-8), Err(Invalid("open closed wire")));
}
#[test]
fn curve_number_and_domain_errors_are_rejected_before_evaluation() {
let p = Vec2::new;
let arc = |radius, start, sweep| Seg2::Arc { center: Vec2::ZERO, radius, start, sweep };
let conic = |w| Seg2::Conic { a: Vec2::X, apex: p(1.0, 1.0), b: Vec2::Y, w };
let invalid = [
Seg2::Line { a: Vec2::ZERO, b: Vec2::ZERO },
Seg2::Line { a: Vec2::ZERO, b: p(f64::NAN, 0.0) },
Seg2::Line { a: Vec2::ZERO, b: p(f64::MAX, 0.0) },
arc(-1.0, 0.0, PI),
arc(0.0, 0.0, PI),
arc(1e-13, 0.0, PI),
arc(1e12 + 1.0, 0.0, PI),
arc(f64::INFINITY, 0.0, PI),
arc(1.0, f64::NAN, PI),
arc(1.0, 1e6 + 1.0, PI),
arc(1.0, 0.0, 0.0),
arc(1.0, 0.0, 1e-13),
arc(1.0, 0.0, TAU + 1e-8),
arc(1.0, 0.0, f64::INFINITY),
conic(-0.5),
conic(0.0),
conic(1e-13),
conic(1e12 + 1.0),
conic(f64::NAN),
Seg2::Cubic { p0: Vec2::X, p1: Vec2::X, p2: Vec2::X, p3: Vec2::X },
Seg2::Conic { a: Vec2::X, apex: Vec2::X, b: Vec2::X, w: 1.0 },
];
for geometry in invalid {
let mut section = empty(Plane::XY);
section.curves.push(analytic(geometry));
assert!(section.validate_structure(TOL).is_err(), "accepted {geometry:?}");
}
let mut section = empty(Plane::XY);
section.contacts.push(p(f64::INFINITY, 0.0));
assert!(section.validate_structure(TOL).is_err());
}
#[test]
fn bezier_degree_and_every_serialized_control_slot_are_validated() {
for degree in [4u8, 9] {
let mut p = [Vec2::ZERO; MAX_BEZIER_CTRL];
for (i, point) in p.iter_mut().enumerate().take(usize::from(degree) + 1) {
*point = Vec2::new(i as f64, (i % 2) as f64);
}
let mut section = empty(Plane::XY);
section.curves.push(analytic(Seg2::Bezier { n: degree, p }));
section.validate_structure(TOL).unwrap();
for wrong in [0, 3, 10, u8::MAX] {
section.curves[0].geometry = Seg2::Bezier { n: wrong, p };
assert_eq!(section.validate_structure(TOL), Err(Invalid("Bezier degree")));
}
}
let mut p = [Vec2::ZERO; MAX_BEZIER_CTRL];
p[4] = Vec2::X;
for value in [f64::NAN, f64::INFINITY, 1e12 + 1.0] {
p[MAX_BEZIER_CTRL - 1] = Vec2::new(value, 0.0);
let mut section = empty(Plane::XY);
section.curves.push(analytic(Seg2::Bezier { n: 4, p }));
assert_eq!(section.validate_structure(TOL), Err(Invalid("non-finite or excessive coordinate")));
}
let mut section = empty(Plane::XY);
section.curves.push(analytic(Seg2::Bezier { n: 4, p: [Vec2::ZERO; MAX_BEZIER_CTRL] }));
assert_eq!(section.validate_structure(TOL), Err(Invalid("collapsed curve")));
}
#[test]
fn supported_curve_limits_keep_evaluation_finite_without_claiming_accuracy() {
let p = Vec2::new;
let mut section = empty(Plane::XY);
section.curves = vec![
analytic(Seg2::Line { a: p(-1e12, 1e12), b: p(1e12, -1e12) }),
analytic(Seg2::Arc { center: p(1e12, 1e12), radius: 1e12, start: 1e6, sweep: -TAU }),
analytic(Seg2::Arc { center: Vec2::ZERO, radius: 1e-12, start: 0.0, sweep: 1e-12 }),
analytic(Seg2::Conic { a: p(-1e12, 0.0), apex: p(0.0, 1e12), b: p(1e12, 0.0), w: 1e12 }),
analytic(Seg2::Conic { a: p(-1e12, 0.0), apex: p(0.0, 1e12), b: p(1e12, 0.0), w: 1e-12 }),
analytic(bezier(&[p(0.0, 0.0), p(1e12, 0.0), p(0.0, 1e12), p(-1e12, 0.0)]).unwrap()),
];
let controls: Vec<Vec2> = (0..=9).map(|i| p(i as f64 * 1e11, if i % 2 == 0 { 1e12 } else { -1e12 })).collect();
section.curves.push(analytic(bezier(&controls).unwrap()));
section.validate_structure(TOL).unwrap();
for curve in &section.curves {
for t in [0.0, 0.5, 1.0] {
assert!(curve.geometry.point_at(t).is_finite());
assert!(curve.geometry.derivative(t).is_finite());
assert!(curve.geometry.derivatives(t, 4).is_some_and(|d| d.iter().all(|v| v.is_finite())));
}
}
}
#[test]
fn transient_serialization_preserves_accuracy_tags_and_plane_names() {
let mut section = plate();
section.curves[1].accuracy = CurveAccuracy::Bounded { max_deviation_mm: 0.01 };
section.curves[2].accuracy = CurveAccuracy::Sampled { max_sample_deviation_mm: 0.0 };
section.validate_structure(TOL).unwrap();
let value = serde_json::to_value(&section).unwrap();
assert!(value["plane"].get("x_dir").is_some());
assert!(value["plane"].get("y_dir").is_some());
assert_eq!(value["curves"][0]["accuracy"], serde_json::json!({"kind": "analytic"}));
assert_eq!(value["curves"][1]["accuracy"], serde_json::json!({"kind": "bounded", "max_deviation_mm": 0.01}));
assert_eq!(value["curves"][2]["accuracy"], serde_json::json!({"kind": "sampled", "max_sample_deviation_mm": 0.0}));
let restored: SectionGeometry = serde_json::from_value(value.clone()).unwrap();
restored.validate_structure(TOL).unwrap();
assert_eq!(section, restored);
let mut unknown = value;
unknown["certificate"] = serde_json::json!(true);
assert!(serde_json::from_value::<SectionGeometry>(unknown).is_err());
assert!(serde_json::from_value::<CurveAccuracy>(serde_json::json!({"kind": "bounded", "max_deviation_mm": 0.1, "exact": true})).is_err());
assert!(serde_json::from_value::<CurveAccuracy>(serde_json::json!({"kind": "exact"})).is_err());
}
#[test]
fn accuracy_values_are_validated_without_promoting_zero_sampled_error() {
for value in [f64::NAN, f64::INFINITY, -0.01, 1e12 + 1.0] {
for accuracy in [CurveAccuracy::Bounded { max_deviation_mm: value }, CurveAccuracy::Sampled { max_sample_deviation_mm: value }] {
let mut section = plate();
section.curves[0].accuracy = accuracy;
assert_eq!(section.validate_structure(TOL), Err(Invalid("accuracy deviation")));
}
}
for value in [0.0, 1e12] {
let mut section = plate();
section.curves[0].accuracy = CurveAccuracy::Sampled { max_sample_deviation_mm: value };
section.validate_structure(TOL).unwrap();
section.to_region(0, TOL).unwrap();
assert_eq!(section.curves[0].accuracy, CurveAccuracy::Sampled { max_sample_deviation_mm: value });
}
}
#[test]
fn conversion_revalidates_mutated_data_and_does_not_trust_an_earlier_success() {
let mut section = plate();
section.validate_structure(TOL).unwrap();
section.curves[0].geometry = Seg2::Line { a: Vec2::ZERO, b: Vec2::new(f64::NAN, 0.0) };
assert_eq!(section.to_loop(0, TOL), Err(Invalid("non-finite or excessive coordinate")));
assert_eq!(section.to_region(0, TOL), Err(Invalid("non-finite or excessive coordinate")));
let mut section = plate();
section.to_region(0, TOL).unwrap();
section.contacts = vec![Vec2::ZERO; MAX_ITEMS];
assert_eq!(section.to_region(0, TOL), Err(Invalid("item budget")));
}
#[test]
fn reversed_large_arc_conversion_rechecks_actual_connectivity_without_snapping() {
let arc = Seg2::Arc { center: Vec2::ZERO, radius: 1e12, start: 0.1, sweep: 1.0 };
let mut section = empty(Plane::XY);
section.curves = vec![analytic(arc), analytic(Seg2::Line { a: arc.start(), b: arc.end() })];
section.wires.push(SectionWire { curves: vec![used(0, false), used(1, true)], closed: true });
section.regions.push(SectionRegion { outer: 0, holes: vec![] });
let before = section.clone();
// Source endpoints coincide exactly, but (start + sweep) - sweep changes the reversed
// arc's endpoint by more than this tolerance at this radius.
section.validate_structure(1e-9).unwrap();
assert!(arc.reversed().end().dist(arc.start()) > 1e-9);
assert_eq!(section.to_loop(0, 1e-9), Err(Invalid("converted loop is not closed within tolerance")));
assert_eq!(section.to_region(0, 1e-9), Err(Invalid("converted loop is not closed within tolerance")));
assert_eq!(section, before);
// A caller's explicit larger tolerance can accept the same result without moving endpoints.
let converted = section.to_loop(0, 0.01).unwrap();
assert_eq!(converted.segs[0], arc.reversed());
assert!(converted.is_closed(0.01));
}
#[test]
fn connected_bow_tie_and_wrong_winding_are_not_certified_or_silently_repaired() {
let p = Vec2::new;
let loop_ = Loop2::polygon(&[p(0.0, 0.0), p(2.0, 2.0), p(0.0, 2.0), p(2.0, 0.0)]);
let mut bow = empty(Plane::XY);
bow.curves = loop_.segs.iter().copied().map(analytic).collect();
bow.wires.push(SectionWire { curves: (0..4).map(|i| used(i, true)).collect(), closed: true });
bow.regions.push(SectionRegion { outer: 0, holes: vec![] });
bow.validate_structure(TOL).unwrap();
assert_eq!(bow.to_loop(0, TOL).unwrap(), loop_);
// A successful structural check does not establish a material region, including its winding.
let mut reversed = plate();
reversed.wires[0].curves.reverse();
for use_ in &mut reversed.wires[0].curves {
use_.forward = !use_.forward;
}
reversed.validate_structure(TOL).unwrap();
let converted = reversed.to_region(0, TOL).unwrap();
assert!((converted.outer.signed_area() + 6000.0).abs() < 1e-10);
}