mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 19:15:57 +00:00
Records: the item budget bounds input, not the kernel's own results (Refs #3992)
The portable record's 4,000,000-item budget refused a real result: the join of a level-4 Menger sponge's level-3 sponges ended with "SMLib export: the mapped B-rep does not validate: portable B-rep: item budget". The budget guards records from outside the kernel, and a body the kernel built in memory from inputs already bounded where they came in isn't hostile. Brep::validate keeps the budget for records read from outside: native design caches (decode_native_body, behind its own 256 MiB, 1M-per-array and 20M-value caps) and STEP import. The new Brep::validate_built runs every other check, without the cap, on what the kernel builds: SMLib's export of an operation's result, a body's record (Body::from_record, after the decoder has checked a file's), a part of a validated record, the record handed back to SMLib, and the validity check. Its checks are linear in a record already in memory. SMLib's export bounded its counts by the same budget; a count is now bounded by SMLib's own longest array (flat::MAX_LEN, checked on entry), which keeps every offset worked out from the counts from overflowing, and the redundant topology-length cap goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+29
-4
@@ -24,6 +24,9 @@ mod topology;
|
||||
pub use curve2::Curve2;
|
||||
|
||||
pub const VERSION: u32 = 4;
|
||||
/// The most items (records, points, knots, uses) [`Brep::validate`] takes in a record that comes
|
||||
/// from outside the kernel: a file, the control channel, pasted data. A record the kernel built
|
||||
/// ([`Brep::validate_built`]) has no such cap.
|
||||
pub const MAX_ITEMS: usize = 4_000_000;
|
||||
pub const MAX_SHELLS: usize = 1024;
|
||||
const MAX_DEPTH: usize = 16;
|
||||
@@ -266,11 +269,19 @@ fn require(condition: bool, message: &'static str) -> Result<()> {
|
||||
if condition { Ok(()) } else { Err(Invalid(message)) }
|
||||
}
|
||||
|
||||
struct Budget(usize);
|
||||
/// The items counted so far and the most allowed.
|
||||
struct Budget {
|
||||
used: usize,
|
||||
limit: usize,
|
||||
}
|
||||
impl Budget {
|
||||
/// The budget of a record from outside the kernel.
|
||||
fn input() -> Budget {
|
||||
Budget { used: 0, limit: MAX_ITEMS }
|
||||
}
|
||||
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")
|
||||
self.used = self.used.checked_add(n).ok_or(Invalid("item budget"))?;
|
||||
require(self.used <= self.limit, "item budget")
|
||||
}
|
||||
}
|
||||
fn numbers(values: impl IntoIterator<Item = f64>) -> Result<()> {
|
||||
@@ -584,12 +595,26 @@ impl Brep {
|
||||
/// list of uses and the canonical order. Does not certify watertightness, that edges lie on
|
||||
/// their surfaces or that pcurves lift onto their edges ([`Brep::check_geometry`] spot-checks
|
||||
/// those).
|
||||
///
|
||||
/// For a record from outside the kernel (a file, the control channel, pasted data): it is
|
||||
/// refused past [`MAX_ITEMS`] items, so hostile input is bounded before anything walks it.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
self.validate_within(Budget::input())
|
||||
}
|
||||
|
||||
/// [`Brep::validate`] for a record the kernel built in memory (an operation's result, a
|
||||
/// body's own record, a part of a validated record): every check but the item budget. The
|
||||
/// record already sits in memory, its size set by inputs that were bounded where they came
|
||||
/// in, and the checks are linear in it; a cap here would only refuse a real result.
|
||||
pub fn validate_built(&self) -> Result<()> {
|
||||
self.validate_within(Budget { used: 0, limit: usize::MAX })
|
||||
}
|
||||
|
||||
fn validate_within(&self, mut budget: Budget) -> Result<()> {
|
||||
require(self.version == VERSION, "unsupported version")?;
|
||||
tolerance(self.tolerance)?;
|
||||
require(!self.shells.is_empty() && self.shells.len() <= MAX_SHELLS, "shell count")?;
|
||||
require(!self.regions.is_empty() && self.regions.len() <= 2 * MAX_SHELLS + 1, "region count")?;
|
||||
let mut budget = Budget(0);
|
||||
budget.add(self.regions.len() + self.faces.len() + self.edges.len())?;
|
||||
for v in &self.vertices {
|
||||
points(&[v.point], &mut budget)?;
|
||||
|
||||
@@ -100,12 +100,12 @@ mod tests {
|
||||
#[test]
|
||||
fn pcurves_validate_and_round_trip() {
|
||||
let line = Curve2::Spline { knots: vec![0., 0., 1., 1.], points: vec![[0., 0.], [1., 2.]] };
|
||||
curve2(&line, &mut Budget(0)).unwrap();
|
||||
curve2(&line, &mut Budget::input()).unwrap();
|
||||
assert_eq!(line.interval(), Some([0., 1.]));
|
||||
assert_eq!(serde_json::from_str::<Curve2>(&serde_json::to_string(&line).unwrap()).unwrap(), line);
|
||||
let bad = Curve2::RationalSpline { knots: vec![0., 0., 1., 1.], homogeneous: vec![[0., 0., 1.], [1., 2., 0.]] };
|
||||
assert!(curve2(&bad, &mut Budget(0)).is_err());
|
||||
assert!(curve2(&bad, &mut Budget::input()).is_err());
|
||||
let nan = Curve2::Spline { knots: vec![0., 0., 1., 1.], points: vec![[0., f64::NAN], [1., 2.]] };
|
||||
assert!(curve2(&nan, &mut Budget(0)).is_err());
|
||||
assert!(curve2(&nan, &mut Budget::input()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//!
|
||||
//! A spot check, not a certificate: a fixed number of evaluations per record (three points of
|
||||
//! each edge, [`AREA_SAMPLES`] of each use, one projection per vertex loop), so the work is
|
||||
//! linear in the item budget [`Brep::validate`] caps. Every distance is held to the record's own tolerance:
|
||||
//! linear in the record. Every distance is held to the record's own tolerance:
|
||||
//! an edge's for its pcurves, an edge's and its vertex's together where they meet, a vertex's on
|
||||
//! its surface. A point that cannot be evaluated is an error, never a panic.
|
||||
use super::eval::{CurveEval, SurfaceEval};
|
||||
|
||||
@@ -370,7 +370,7 @@ impl Brep {
|
||||
edges: new_edges,
|
||||
vertices: new_vertices,
|
||||
};
|
||||
brep.validate()?;
|
||||
brep.validate_built()?;
|
||||
Ok(Part { brep, faces: faces.old, edges: edges.old, vertices: vertices.old })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ fn check_size(loops: &[Vec<EdgeUse<'_>>], tolerance: f64) -> Result<()> {
|
||||
/// Each use's pcurve: the given one, or its closed form; a use with neither is its `Failed`
|
||||
/// error, any other error fails the face.
|
||||
fn each_pcurve(surface_record: &Surface, on: &On, loops: &[Vec<EdgeUse<'_>>], tolerance: f64) -> Result<Vec<Vec<Result<Curve2>>>> {
|
||||
let mut budget = Budget(0);
|
||||
let mut budget = Budget::input();
|
||||
surface(surface_record, 0, &mut budget).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
// A seam edge used twice is worked out once.
|
||||
let mut done: Vec<(u32, Result<Curve2>)> = Vec::new();
|
||||
|
||||
@@ -741,19 +741,42 @@ fn a_reversed_face_is_walked_backwards() {
|
||||
b.validate().unwrap();
|
||||
}
|
||||
|
||||
/// A valid record of more than [`MAX_ITEMS`] items: the sheet with its first edge a polyline of
|
||||
/// that many points (#3992).
|
||||
fn over_the_item_budget() -> Brep {
|
||||
let mut b = sheet();
|
||||
b.edges[0].curve = Curve::Polyline { points: (0..MAX_ITEMS).map(|i| [i as f64 * 1e-6, 0., 0.]).collect() };
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_item_budget_bounds_input_records_and_not_the_kernels_own() {
|
||||
// A record from a file or the control channel is refused past the budget; the same record
|
||||
// built by the kernel in memory (a boolean's result) is its own size and is taken (#3992).
|
||||
let big = over_the_item_budget();
|
||||
assert_eq!(big.validate().map_err(|e| e.0), Err("item budget"));
|
||||
big.validate_built().unwrap();
|
||||
// Every other check still holds for a built record.
|
||||
let mut collapsed = big;
|
||||
if let Curve::Polyline { points } = &mut collapsed.edges[0].curve {
|
||||
points[1] = points[0];
|
||||
}
|
||||
assert_eq!(collapsed.validate_built().map_err(|e| e.0), Err("collapsed polyline segment"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spline_domains_weights_and_nesting_are_bounded() {
|
||||
let mut budget = Budget(0);
|
||||
let mut budget = Budget::input();
|
||||
assert!(knots(&[0., 0., 1., 1.], 2, &mut budget).is_ok());
|
||||
for bad in [vec![0., 0., 0., 0.], vec![0., 1., 0., 1.], vec![0., 0., f64::INFINITY, 1.]] {
|
||||
assert!(knots(&bad, 2, &mut Budget(0)).is_err());
|
||||
assert!(knots(&bad, 2, &mut Budget::input()).is_err());
|
||||
}
|
||||
let c = Curve::RationalSpline { knots: vec![0., 0., 1., 1.], homogeneous: vec![[0., 0., 0., 0.], [1., 0., 0., 1.]] };
|
||||
assert!(curve(&c, 0, &mut Budget(0)).is_err());
|
||||
assert!(curve(&c, 0, &mut Budget::input()).is_err());
|
||||
let p = Surface::Plane { origin: [0.; 3], u_axis: [1., 0., 0.], v_axis: [2., 0., 0.] };
|
||||
assert!(surface(&p, 0, &mut Budget(0)).is_err());
|
||||
assert!(curve(&Curve::Line { start: [0.; 3], end: [1.; 3] }, MAX_DEPTH + 1, &mut Budget(0)).is_err());
|
||||
assert!(Budget(MAX_ITEMS).add(1).is_err());
|
||||
assert!(surface(&p, 0, &mut Budget::input()).is_err());
|
||||
assert!(curve(&Curve::Line { start: [0.; 3], end: [1.; 3] }, MAX_DEPTH + 1, &mut Budget::input()).is_err());
|
||||
assert!(Budget { used: MAX_ITEMS, limit: MAX_ITEMS }.add(1).is_err());
|
||||
assert_eq!(curve_natural(&Curve::Spline { knots: vec![0., 0., 0., 1., 2., 2., 2.], points: vec![[0.; 3]; 4] }), Natural::Range(0., 2.));
|
||||
}
|
||||
|
||||
@@ -762,7 +785,7 @@ fn exact_procedural_records_are_validated_and_round_trip() {
|
||||
let plane = |z: f64| Box::new(Surface::Plane { origin: [0., 0., z], u_axis: [1., 0., 0.], v_axis: [0., 1., 0.] });
|
||||
let leader = Box::new(Curve::Polyline { points: vec![[0.; 3], [0.; 3], [1., 0., 0.]] });
|
||||
let c = Curve::SurfaceIntersection { surfaces: [plane(0.), plane(1.)], leader: leader.clone(), tolerance: 1e-6 };
|
||||
curve(&c, 0, &mut Budget(0)).unwrap();
|
||||
curve(&c, 0, &mut Budget::input()).unwrap();
|
||||
assert_eq!(curve_natural(&c), Natural::Range(0., 2.));
|
||||
assert_eq!(serde_json::from_str::<Curve>(&serde_json::to_string(&c).unwrap()).unwrap(), c);
|
||||
let line = Box::new(Curve::Line { start: [0.; 3], end: [1.; 3] });
|
||||
@@ -771,17 +794,17 @@ fn exact_procedural_records_are_validated_and_round_trip() {
|
||||
Curve::SurfaceIntersection { surfaces: [plane(0.), plane(1.)], leader: leader.clone(), tolerance: f64::NAN },
|
||||
Curve::SurfaceIntersection { surfaces: [plane(f64::INFINITY), plane(1.)], leader, tolerance: 1e-6 },
|
||||
] {
|
||||
assert!(curve(&bad, 0, &mut Budget(0)).is_err());
|
||||
assert!(curve(&bad, 0, &mut Budget::input()).is_err());
|
||||
}
|
||||
let identity = [[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]];
|
||||
let s = Surface::PlacedRevolution { profile: line.clone(), origin: [0.; 3], axis: [0., 0., 1.], matrix: identity, forward: false };
|
||||
surface(&s, 0, &mut Budget(0)).unwrap();
|
||||
surface(&s, 0, &mut Budget::input()).unwrap();
|
||||
assert_eq!(surface_natural(&s), [Natural::Periodic, UNBOUNDED]);
|
||||
assert_eq!(serde_json::from_str::<Surface>(&serde_json::to_string(&s).unwrap()).unwrap(), s);
|
||||
let mut flat = identity;
|
||||
flat[2] = [0.; 4];
|
||||
let bad = Surface::PlacedRevolution { profile: line, origin: [0.; 3], axis: [0., 0., 1.], matrix: flat, forward: true };
|
||||
assert!(surface(&bad, 0, &mut Budget(0)).is_err());
|
||||
assert!(surface(&bad, 0, &mut Budget::input()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -799,16 +822,16 @@ fn analytic_records_are_unbounded_with_independent_frames() {
|
||||
let narrowing = Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: -0.2 };
|
||||
assert_eq!(surface_natural(&narrowing)[1], Natural::Range(f64::NEG_INFINITY, apex));
|
||||
for flat in [0., 1e-13, -1e-13, std::f64::consts::FRAC_PI_2, -std::f64::consts::FRAC_PI_2] {
|
||||
assert!(surface(&Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: flat }, 0, &mut Budget(0)).is_err());
|
||||
assert!(surface(&Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: flat }, 0, &mut Budget::input()).is_err());
|
||||
}
|
||||
for s in surfaces {
|
||||
surface(&s, 0, &mut Budget(0)).unwrap();
|
||||
surface(&s, 0, &mut Budget::input()).unwrap();
|
||||
let encoded = serde_json::to_string(&s).unwrap();
|
||||
assert_eq!(serde_json::from_str::<Surface>(&encoded).unwrap(), s);
|
||||
assert!(!encoded.contains("\"angle\"") && !encoded.contains("height") && !encoded.contains("longitude"));
|
||||
}
|
||||
for c in [Curve::Circle { frame: f.clone(), radius: 2. }, Curve::Ellipse { frame: f.clone(), radii: [2., 3.] }] {
|
||||
curve(&c, 0, &mut Budget(0)).unwrap();
|
||||
curve(&c, 0, &mut Budget::input()).unwrap();
|
||||
assert_eq!(serde_json::from_str::<Curve>(&serde_json::to_string(&c).unwrap()).unwrap(), c);
|
||||
assert_eq!(curve_natural(&c), Natural::Periodic);
|
||||
// A periodic edge may start anywhere and run up to one turn.
|
||||
@@ -816,10 +839,10 @@ fn analytic_records_are_unbounded_with_independent_frames() {
|
||||
}
|
||||
let mut bad = f;
|
||||
bad.y_axis = bad.x_axis;
|
||||
assert!(frame(&bad, &mut Budget(0)).is_err());
|
||||
assert!(frame(&bad, &mut Budget::input()).is_err());
|
||||
assert!(interval(&[2., 1.], UNBOUNDED).is_err());
|
||||
assert!(radius(f64::NAN).is_err());
|
||||
assert!(surface(&Surface::Torus { frame: a_frame(), major_radius: 1., minor_radius: 2., tube_start: 0.0 }, 0, &mut Budget(0)).is_err());
|
||||
assert!(surface(&Surface::Torus { frame: a_frame(), major_radius: 1., minor_radius: 2., tube_start: 0.0 }, 0, &mut Budget::input()).is_err());
|
||||
}
|
||||
|
||||
/// A cylinder face round its axis with a hole beside its seam: the seam at 350 degrees (so its
|
||||
|
||||
@@ -376,11 +376,11 @@ fn same_shared<T: PartialEq>(a: &Option<std::sync::Arc<T>>, b: &Option<std::sync
|
||||
}
|
||||
}
|
||||
|
||||
/// `record`, checked to be a body's: valid structure (budgets, references, loops, regions,
|
||||
/// `record`, checked to be a body's: valid structure (references, loops, regions,
|
||||
/// canonical order) and at least one face. A solid's faces bound material; a surface body's lie
|
||||
/// in open shells.
|
||||
fn solid_record(record: Brep) -> Result<Brep> {
|
||||
record.validate().map_err(|e| KernelError::Invalid(e.to_string()))?;
|
||||
record.validate_built().map_err(|e| KernelError::Invalid(e.to_string()))?;
|
||||
if record.faces.is_empty() {
|
||||
return Err(KernelError::Failed("empty result".into()));
|
||||
}
|
||||
@@ -477,7 +477,8 @@ impl Body {
|
||||
}
|
||||
|
||||
/// The body of a schema 4 record (a solid, or a surface body of open shells): checked for
|
||||
/// structure (budgets, references, loops, regions, canonical order). Its faces face as the
|
||||
/// structure (references, loops, regions, canonical order; a record from a file is checked
|
||||
/// against the file budgets where it is read). Its faces face as the
|
||||
/// record says.
|
||||
pub(crate) fn from_record(record: Brep) -> Result<Body> {
|
||||
Ok(Body::bare(solid_record(record)?, None))
|
||||
|
||||
@@ -416,6 +416,23 @@ mod tests {
|
||||
assert!(decode_native_body(&huge).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_kernel_body_past_the_file_item_budget_is_taken_and_a_file_of_it_refused() {
|
||||
// The box with its first edge a polyline of the file item budget's worth of points: a
|
||||
// record the kernel's own operations may make (#3992), and more than a file may carry.
|
||||
let body = crate::box_solid(Vec3::ZERO, Vec3::new(10., 20., 30.)).unwrap();
|
||||
let mut record = body.record().clone();
|
||||
let [t0, t1] = record.edges[0].interval;
|
||||
let n = solidcraft_geom::brep::MAX_ITEMS;
|
||||
let points = (0..n).map(|i| [i as f64 * 1e-6, 0., 0.]).collect();
|
||||
record.edges[0].curve = solidcraft_geom::brep::Curve::Polyline { points };
|
||||
assert!(t0 >= 0.0 && t1 <= (n - 1) as f64);
|
||||
Body::from_record(record.clone()).unwrap();
|
||||
let saved = SavedBody { solid: record, metadata: metadata::encode(&body).unwrap(), runtime: Runtime { size: 0.0, coarse_missing: None } };
|
||||
let refused = validate_topology(&saved).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(refused.contains("item budget"), "{refused}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_preserves_face_appearance_and_authored_boundaries() {
|
||||
let mut body = crate::box_solid(Vec3::new(1., 2., 3.), Vec3::new(10., 20., 30.))
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Validity {
|
||||
pub fn validity(body: &Body) -> Validity {
|
||||
let record = brep_record(body).map_err(|e| e.to_string());
|
||||
let checked = record.as_ref().map_err(Clone::clone).and_then(|r| {
|
||||
r.validate().map_err(|e| format!("structure: {e}"))?;
|
||||
r.validate_built().map_err(|e| format!("structure: {e}"))?;
|
||||
r.check_geometry().map_err(|e| format!("geometry: {e}"))
|
||||
});
|
||||
Validity { record: checked, smlib: smlib_validity(body) }
|
||||
|
||||
@@ -265,8 +265,10 @@ fn at<T: Copy>(a: &[T], k: usize, what: &str) -> Res<T> {
|
||||
fn idx(x: i64, n: usize, what: &str) -> Res<usize> {
|
||||
usize::try_from(x).ok().filter(|i| *i < n).ok_or_else(|| invalid(format!("{what}: {x} out of range")))
|
||||
}
|
||||
/// A count in the arrays: at most SMLib's longest array ([`flat::MAX_LEN`], checked on entry), as
|
||||
/// no block is longer than the array it lies in, so offsets worked out from counts cannot overflow.
|
||||
fn count(x: i64, what: &str) -> Res<usize> {
|
||||
usize::try_from(x).ok().filter(|n| *n <= b::MAX_ITEMS).ok_or_else(|| invalid(format!("{what}: count {x}")))
|
||||
usize::try_from(x).ok().filter(|n| *n <= flat::MAX_LEN).ok_or_else(|| invalid(format!("{what}: count {x}")))
|
||||
}
|
||||
fn index(i: usize) -> Res<u32> {
|
||||
u32::try_from(i).map_err(|_| invalid("index"))
|
||||
@@ -320,7 +322,7 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
|
||||
let nv = count(at(flat.i(FACE_NURBS_V_VERTEX_COUNT), j, "NURBS face")?, "NURBS face")?;
|
||||
let ou = count(at(flat.i(FACE_NURBS_U_ORDER), j, "NURBS face")?, "NURBS face")?;
|
||||
let ov = count(at(flat.i(FACE_NURBS_V_ORDER), j, "NURBS face")?, "NURBS face")?;
|
||||
let n = nu.checked_mul(nv).filter(|n| *n <= b::MAX_ITEMS).ok_or_else(|| invalid("NURBS face size"))?;
|
||||
let n = nu.checked_mul(nv).filter(|n| *n <= flat::MAX_LEN).ok_or_else(|| invalid("NURBS face size"))?;
|
||||
let slice = |v: &[f64], from: usize, len: usize| v.get(from..from + len).map(<[f64]>::to_vec).ok_or_else(|| invalid("NURBS face arrays"));
|
||||
let pts = slice(flat.f(FACE_NURBS_CVS), 3 * at_.cv, 3 * n)?;
|
||||
let u_knots = slice(flat.f(FACE_NURBS_U_KNOTS), at_.uk, nu + ou)?;
|
||||
@@ -739,7 +741,6 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> {
|
||||
|| flat.i(EDGEUSE_ORIENTATION).len() != n_edgeuses
|
||||
|| flat.i(LOOP_VERTEX).len() != n_loops
|
||||
|| flat.i(EDGE_VERTICES).len() != 2 * n_edges
|
||||
|| [n_faceuses, n_faces, n_loops, n_edgeuses, n_edges, n_vertices].iter().any(|n| *n > b::MAX_ITEMS)
|
||||
{
|
||||
return Err(invalid("array lengths"));
|
||||
}
|
||||
@@ -1309,7 +1310,7 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerances: &Tolerance
|
||||
})
|
||||
.collect::<Res<Vec<_>>>()?;
|
||||
let brep = b::Brep { version: b::VERSION, tolerance, regions, shells, faces: face_records, edges: edge_records, vertices };
|
||||
brep.validate().map_err(|e| invalid(format!("the mapped B-rep does not validate: {e}")))?;
|
||||
brep.validate_built().map_err(|e| invalid(format!("the mapped B-rep does not validate: {e}")))?;
|
||||
let placed = |new: &[Option<u32>]| {
|
||||
new.iter().map(|i| i.map(|i| i as usize).ok_or_else(|| invalid("an entity outside the canonical walk"))).collect::<Res<Vec<_>>>()
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ fn z_axis(f: &b::Frame) -> [f64; 3] {
|
||||
/// The record as flat arrays. `trusted`: the record comes from a modeller that keeps SMLib's
|
||||
/// rules (SMLib then skips its healer); otherwise SMLib heals it on import.
|
||||
pub fn arrays(brep: &b::Brep, trusted: bool) -> R<Arrays> {
|
||||
brep.validate().map_err(|e| Unsupported::Invalid(e.0))?;
|
||||
brep.validate_built().map_err(|e| Unsupported::Invalid(e.0))?;
|
||||
let brep = analytic::analytic(brep)?;
|
||||
laid_out(brep.as_ref(), trusted, true, &HashSet::new())
|
||||
}
|
||||
@@ -93,7 +93,7 @@ pub fn arrays(brep: &b::Brep, trusted: bool) -> R<Arrays> {
|
||||
/// bilinear patch, to compare what SMLib makes of splines with what it makes of its own analytic
|
||||
/// surfaces. A revolution is still refused (SMLib has no such surface).
|
||||
pub fn arrays_keeping_splines(brep: &b::Brep, trusted: bool) -> R<Arrays> {
|
||||
brep.validate().map_err(|e| Unsupported::Invalid(e.0))?;
|
||||
brep.validate_built().map_err(|e| Unsupported::Invalid(e.0))?;
|
||||
laid_out(brep, trusted, false, &HashSet::new())
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -214,6 +214,12 @@ closed chains through shared vertices; every edge's `uses` lists exactly its use
|
||||
once each, primary first, with `upper` as stated; pcurve intervals; parameter
|
||||
ranges; and the canonical order.
|
||||
|
||||
The item cap is for records from outside the kernel: a native file, a STEP file,
|
||||
the control channel. `Brep::validate_built` runs every other check on a record the
|
||||
kernel built in memory (an operation's result, a body's own record, a part of one):
|
||||
that record already sits in memory, its size set by inputs bounded where they came
|
||||
in, and the checks are linear in it, so a cap there would only refuse a real result.
|
||||
|
||||
These are structural and resource guards, not a geometric validity certificate.
|
||||
The validator does not check that edges lie on their surfaces, that pcurves lift
|
||||
onto their edges, that a closed shell is watertight or that shells nest as the
|
||||
@@ -231,8 +237,8 @@ edge loop's pcurves enclose (u, v) area (more than 10⁻⁹ of the square of the
|
||||
loop's extent), and exactly one loop of each face, the outer, runs
|
||||
counter-clockwise about the natural normal, the rest clockwise (told apart by
|
||||
their areas, not their order); every vertex loop's vertex lies on its face's
|
||||
surface (within the vertex's). It makes a fixed number of evaluations per record, so its work is linear in the item
|
||||
budget, and a point it cannot evaluate is an error naming the record. It is not
|
||||
surface (within the vertex's). It makes a fixed number of evaluations per record, so its work is linear in the
|
||||
record, and a point it cannot evaluate is an error naming the record. It is not
|
||||
a certificate either: between the points it checks, an edge may still leave its
|
||||
surface. The truck adapter runs it before building topology, which truck does
|
||||
without checks of its own.
|
||||
|
||||
Reference in New Issue
Block a user