brep: an edge on a line, circle or ellipse carries the NURBS form SMLib holds it as

The edge mirror of a face's NURBS form: Edge::nurbs keeps the NURBS SMLib holds a conic as, and
the edge's interval and its pcurves' parameter are its. Edge::parameters is the curve every
evaluation at a parameter uses; curve says what the edge is. Nothing writes the form yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-22 17:30:43 -04:00
parent a683077c43
commit b1326e48d6
38 changed files with 246 additions and 54 deletions
@@ -0,0 +1,4 @@
//! An edge on a line, circle or ellipse may save `nurbs`: the NURBS form it is parameterised on,
//! as SMLib holds it. Its interval and the pcurves' parameter are then that NURBS's, and `curve`
//! only says what the edge is. An older design has none and keeps the curve's own parameter. The
//! last release refuses a design with this change (the newer format number).
+26 -2
View File
@@ -132,8 +132,16 @@ pub struct OrientedEdge {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Edge {
/// What the edge is: its curve as the producer names it (a line, a circle…).
pub curve: Curve,
/// The modelled interval in the curve's own parameter, min < max.
/// The NURBS form the edge is parameterised on, when it differs from `curve`'s own
/// parameter: a circle or ellipse as SMLib holds it (every SMLib conic is a B-spline curve,
/// the trims on it in that NURBS's parameter). When given, `interval` and the pcurves' parameter
/// are this curve's, and `curve` only says what it is; it is a `Spline` or `RationalSpline` and
/// `curve` is a line, circle or ellipse (docs/native-brep.md, "Curves").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nurbs: Option<Curve>,
/// The modelled interval in the edge's parameter ([`Edge::parameters`]), min < max.
pub interval: [f64; 2],
/// `[start, end]`, at interval min and max; equal for a closed edge.
pub vertices: [u32; 2],
@@ -478,6 +486,14 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
}
}
impl Edge {
/// The curve the edge's parameter is on: its NURBS form when it has one, else its curve.
/// Everything that evaluates the edge at a parameter goes through this.
pub fn parameters(&self) -> &Curve {
self.nurbs.as_ref().unwrap_or(&self.curve)
}
}
impl Face {
/// The surface the face's (u, v) are on: its NURBS form when it has one, else its surface.
/// Everything that evaluates the face's domain or pcurves goes through this.
@@ -505,7 +521,15 @@ impl Brep {
}
for e in &self.edges {
curve(&e.curve, 0, &mut budget)?;
interval(&e.interval, curve_natural(&e.curve))?;
if let Some(n) = &e.nurbs {
curve(n, 0, &mut budget)?;
require(matches!(n, Curve::Spline { .. } | Curve::RationalSpline { .. }), "NURBS form is not a NURBS")?;
require(
matches!(e.curve, Curve::Line { .. } | Curve::Circle { .. } | Curve::Ellipse { .. }),
"NURBS form on a curve that is not a line, circle or ellipse",
)?;
}
interval(&e.interval, curve_natural(e.parameters()))?;
require(e.vertices.iter().all(|&v| (v as usize) < self.vertices.len()), "edge vertices")?;
tolerance(e.tolerance)?;
budget.add(e.uses.len())?;
+1 -1
View File
@@ -106,7 +106,7 @@ impl Brep {
let mut points = Vec::with_capacity(self.edges.len());
for (i, e) in self.edges.iter().enumerate() {
let at = Record::Edge(index(i));
let curve = CurveEval::new(&e.curve).map_err(|err| unevaluable(at, err))?;
let curve = CurveEval::new(e.parameters()).map_err(|err| unevaluable(at, err))?;
let [t0, t1] = e.interval;
let p = [t0, 0.5 * (t0 + t1), t1].map(|t| curve.point(t).ok_or(t));
let p = match p {
+2 -1
View File
@@ -225,7 +225,8 @@ impl Brep {
Ok(edge)
})
.collect::<Result<Vec<_>>>()?;
let new_vertices = vertices.old.iter().map(|&v| self.vertices.get(v as usize).cloned().ok_or(Invalid("vertex reference"))).collect::<Result<_>>()?;
let new_vertices =
vertices.old.iter().map(|&v| self.vertices.get(v as usize).cloned().ok_or(Invalid("vertex reference"))).collect::<Result<_>>()?;
let brep = Brep {
version: self.version,
tolerance: self.tolerance,
+1
View File
@@ -143,6 +143,7 @@ impl Brep {
}
for e in &mut out.edges {
e.curve = motion.curve(&e.curve);
e.nurbs = e.nurbs.as_ref().map(|n| motion.curve(n));
}
for v in &mut out.vertices {
v.point = motion.point(v.point);
+1 -1
View File
@@ -31,7 +31,7 @@ impl Brep {
for (e, (edge, ts)) in self.edges.iter().zip(cuts.iter_mut()).enumerate() {
ts.sort_by(f64::total_cmp);
ts.dedup();
let curve = if ts.is_empty() { None } else { Some(CurveEval::new(&edge.curve).map_err(|_| Invalid("a split edge's curve"))?) };
let curve = if ts.is_empty() { None } else { Some(CurveEval::new(edge.parameters()).map_err(|_| Invalid("a split edge's curve"))?) };
let [a, b] = edge.interval;
let mut ends = vec![edge.vertices[0]];
for &t in ts.iter() {
+7 -2
View File
@@ -83,6 +83,7 @@ fn faceted(points: &[[f64; 3]], shells: &[&[&[usize]]], regions: Vec<Region>) ->
None => {
let vs = [vertex(&mut b, a), vertex(&mut b, z)];
b.edges.push(Edge {
nurbs: None,
curve: Curve::Line { start: points[a], end: points[z] },
interval: [0., 1.],
vertices: vs,
@@ -138,7 +139,7 @@ fn hollow() -> Brep {
fn tube() -> Brep {
let f = a_frame();
let circle = |z: f64| Curve::Circle { frame: Frame { origin: [1., 2., 3. + z], ..f.clone() }, radius: 2. };
let edge = |curve, interval, vertices| Edge { curve, interval, vertices, tolerance: 1e-7, uses: vec![] };
let edge = |curve, interval, vertices| Edge { nurbs: None, curve, interval, vertices, tolerance: 1e-7, uses: vec![] };
let mut b = Brep {
version: VERSION,
tolerance: 1e-7,
@@ -182,7 +183,10 @@ fn one_face(surface: Surface, domain: [[f64; 2]; 2], edges: Vec<(Curve, [f64; 2]
domain,
loops: vec![Loop::Edges { edges: uses.iter().map(|&(edge, forward)| OrientedEdge { edge, forward, pcurve: segment(0., 1.) }).collect() }],
}],
edges: edges.into_iter().map(|(curve, interval, vertices)| Edge { curve, interval, vertices, tolerance: 1e-7, uses: vec![] }).collect(),
edges: edges
.into_iter()
.map(|(curve, interval, vertices)| Edge { nurbs: None, curve, interval, vertices, tolerance: 1e-7, uses: vec![] })
.collect(),
vertices: vertices.iter().map(|&point| Vertex { point, tolerance: 1e-7 }).collect(),
};
list_uses(&mut b);
@@ -230,6 +234,7 @@ fn sheet_with_lower_forms() -> Brep {
// Vertex order: edge vertices first (the wire edge's), then the vertex loop's, then the shell's.
b.vertices.extend([[2., 0., 0.], [3., 0., 0.], [0.5, 0.5, 0.], [5., 5., 5.]].map(|point| Vertex { point, tolerance: 1e-7 }));
b.edges.push(Edge {
nurbs: None,
curve: Curve::Line { start: [2., 0., 0.], end: [3., 0., 0.] },
interval: [0., 1.],
vertices: [n, n + 1],
+1
View File
@@ -218,6 +218,7 @@ impl Assembly {
}
}
let edges = self.edges.iter().map(|a| Edge {
nurbs: None,
curve: a.curve.clone(),
interval: a.interval,
vertices: a.vertices,
+1 -1
View File
@@ -451,7 +451,7 @@ pub fn tangent_chain(body: &Body, picks: &[Vec3]) -> Result<Vec<Vec3>> {
let mut out = picks.to_vec();
for e in chain.into_iter().skip(picked) {
let edge = rec.rec.edges.get(e).ok_or_else(|| KernelError::Failed("a chain edge".into()))?;
let mid = CurveEval::new(&edge.curve)
let mid = CurveEval::new(edge.parameters())
.ok()
.and_then(|c| c.point(0.5 * (edge.interval[0] + edge.interval[1])))
.ok_or_else(|| KernelError::Failed("a point on a chain edge".into()))?;
+1 -1
View File
@@ -71,7 +71,7 @@ mod tests {
if rec.plane_normal(f).is_some() || rec.plane_normal(g).is_some() {
continue;
}
let c = CurveEval::new(&e.curve).unwrap();
let c = CurveEval::new(e.parameters()).unwrap();
let [t0, t1] = e.interval;
for k in 1..10 {
let p = c.point(t0 + (t1 - t0) * k as f64 / 10.0).unwrap();
+1 -1
View File
@@ -305,7 +305,7 @@ fn edge_points(rec: &Faces, e: usize, tol: f64) -> Option<Vec<Vec3>> {
return Some(vec![a, b]);
}
let edge = rec.rec.edges.get(e)?;
let c = CurveEval::new(&edge.curve).ok()?;
let c = CurveEval::new(edge.parameters()).ok()?;
let [t0, t1] = edge.interval;
// Start from 8 pieces (a closed or turning curve's chord says nothing of its middle).
let mut out = vec![a];
+1 -1
View File
@@ -1266,7 +1266,7 @@ impl Body {
/// rest at the evaluator's points (capped).
fn edge_points(e: &brep::Edge, chord: f64) -> Result<Vec<Vec3>> {
const MAX_POINTS: usize = 4096;
let curve = brep::eval::CurveEval::new(&e.curve).map_err(|err| KernelError::Invalid(err.to_string()))?;
let curve = brep::eval::CurveEval::new(e.parameters()).map_err(|err| KernelError::Invalid(err.to_string()))?;
let [t0, t1] = e.interval;
let at = |t: f64| curve.point(t).ok_or_else(|| KernelError::Invalid(format!("edge curve has no point at {t}")));
if let Curve::Line { .. } = e.curve {
+2 -2
View File
@@ -258,7 +258,7 @@ fn chain_curves(label: &str, r: &Brep, chain: &Chain) -> Result<Vec<(CurveEval,
.iter()
.map(|&(e, forward)| {
let edge = r.edges.get(e).ok_or_else(|| KernelError::Invalid(format!("{label}: an edge reference")))?;
let c = CurveEval::new(&edge.curve).map_err(|err| KernelError::Invalid(format!("{label}: {err}")))?;
let c = CurveEval::new(edge.parameters()).map_err(|err| KernelError::Invalid(format!("{label}: {err}")))?;
let [t0, t1] = edge.interval;
Ok((c, if forward { [t0, t1] } else { [t1, t0] }))
})
@@ -450,7 +450,7 @@ fn face_of(s: &NurbsSurface, corners: &[Vec3]) -> Result<Body> {
let interval = [w[0], w[1]];
let edge = index(parts.edges.len())?;
let vertices = [index(e[0])?, index(e[1])?];
parts.edges.push(solidcraft_geom::brep::Edge { curve: curve.clone(), interval, vertices, tolerance, uses: Vec::new() });
parts.edges.push(solidcraft_geom::brep::Edge { nurbs: None, curve: curve.clone(), interval, vertices, tolerance, uses: Vec::new() });
pieces.push(solidcraft_geom::brep::OrientedEdge { edge, forward, pcurve: uv_line(uv(w[0]), uv(w[1]), interval) });
}
if !forward {
+2 -2
View File
@@ -175,7 +175,7 @@ impl ChainLine {
let fail = || KernelError::Failed("bridge: an edge has no point".into());
for (i, &(e, forward)) in chain.iter().enumerate() {
let edge = r.edges.get(e).ok_or_else(|| KernelError::Invalid("an edge reference".into()))?;
let c = CurveEval::new(&edge.curve).map_err(|err| KernelError::Failed(format!("bridge: {err}")))?;
let c = CurveEval::new(edge.parameters()).map_err(|err| KernelError::Failed(format!("bridge: {err}")))?;
let [t0, t1] = edge.interval;
let (a, z) = if forward { (t0, t1) } else { (t1, t0) };
for k in 0..=PER_EDGE {
@@ -776,7 +776,7 @@ fn support_faces(r: &Brep, chain: &Chain, leave: Vec3, size: f64) -> Result<Vec<
let mut out = Vec::with_capacity(chain.len());
for &(e, _) in chain {
let edge = r.edges.get(e).ok_or_else(|| KernelError::Invalid("an edge reference".into()))?;
let mid = CurveEval::new(&edge.curve)
let mid = CurveEval::new(edge.parameters())
.ok()
.and_then(|c| c.point(0.5 * (edge.interval[0] + edge.interval[1])))
.ok_or_else(|| KernelError::Failed("bridge: an edge has no point".into()))?;
+2 -1
View File
@@ -842,6 +842,7 @@ pub fn planar_face_record(plane: &Plane, r: &Region2) -> Result<solidcraft_geom:
let e = u32::try_from(edges.len()).map_err(|_| bad("size"))?;
let (li, k) = (u32::try_from(li).map_err(|_| bad("size"))?, u32::try_from(k).map_err(|_| bad("size"))?);
edges.push(rec::Edge {
nurbs: None,
curve,
interval,
vertices: [start, end],
@@ -866,7 +867,7 @@ pub fn planar_face_record(plane: &Plane, r: &Region2) -> Result<solidcraft_geom:
ids.iter()
.filter_map(|&e| {
let edge = edges.get(e as usize)?;
Some(pcurve::EdgeUse { edge: e, curve: &edge.curve, interval: edge.interval, forward: true, tolerance, given: None })
Some(pcurve::EdgeUse { edge: e, curve: edge.parameters(), interval: edge.interval, forward: true, tolerance, given: None })
})
.collect()
})
+2 -2
View File
@@ -208,7 +208,7 @@ fn two_sided(adjacent: &[Vec<usize>], index: usize) -> bool {
fn nearest_edge(edges: &[b::Edge], p: Vec3, reach: f64) -> Result<usize> {
let mut best: Option<(f64, usize)> = None;
for (index, edge) in edges.iter().enumerate() {
let Ok(curve) = CurveEval::new(&edge.curve) else { continue };
let Ok(curve) = CurveEval::new(edge.parameters()) else { continue };
let [t0, t1] = edge.interval;
for k in 0..=PICK_SAMPLES {
let Some(q) = curve.point(t0 + (t1 - t0) * k as f64 / PICK_SAMPLES as f64) else { continue };
@@ -265,7 +265,7 @@ fn measure(body: &Body, edges: &[usize], samples: usize, tolerances: ContinuityT
continue;
}
budget -= samples;
let report = match (face(first), face(second), CurveEval::new(&edge.curve)) {
let report = match (face(first), face(second), CurveEval::new(edge.parameters())) {
(Some(a), Some(b), Ok(curve)) => seam(index, &curve, edge.interval, a, b, samples, tolerances),
_ => None,
};
+1 -1
View File
@@ -60,7 +60,7 @@ impl<'a> DistanceShape<'a> {
pub fn new(body: &'a Body, mesh: &'a Mesh, chord: f64) -> DistanceShape<'a> {
let record = body.record();
let faces: Vec<Option<SurfaceEval>> = record.faces.iter().map(|f| SurfaceEval::new(f.parameters()).ok()).collect();
let edges = record.edges.iter().map(|e| CurveEval::new(&e.curve).ok().map(|c| (c, e.interval))).collect();
let edges = record.edges.iter().map(|e| CurveEval::new(e.parameters()).ok().map(|c| (c, e.interval))).collect();
let surfaces = faces.iter().map(|_| OnceLock::new()).collect();
let chord = if chord.is_finite() && chord > 0.0 { chord } else { 0.0 };
DistanceShape { body, mesh, set: TriangleSet::new(mesh), chord, faces, edges, surfaces, face_tris: OnceLock::new() }
+2 -2
View File
@@ -42,7 +42,7 @@ fn wall_angles(b: &Brep, face: usize, o: Vec3, axis: Vec3, radial: Vec3, tangent
}
// The circle's parameter is its angle: the wall spans the edge's interval about the
// angle of its middle (a whole turn, from the seam, for a wall in one piece).
let c = CurveEval::new(&e.curve).ok()?;
let c = CurveEval::new(e.parameters()).ok()?;
let [t0, t1] = e.interval;
let am = angle(c.point(0.5 * (t0 + t1))?, o, axis, radial, tangent);
let half = 0.5 * (t1 - t0);
@@ -509,7 +509,7 @@ fn face_axial_bounds(b: &Brep, face: usize, o: Vec3, axis: Vec3) -> Result<Optio
let edge = b.edges.get(e as usize).ok_or_else(|| fail("edge"))?;
match &edge.curve {
Curve::Line { .. } => {
let c = CurveEval::new(&edge.curve).map_err(|e| fail(&e.to_string()))?;
let c = CurveEval::new(edge.parameters()).map_err(|e| fail(&e.to_string()))?;
for t in edge.interval {
let z = height(c.point(t).ok_or_else(|| fail("edge point"))?);
add(z, z)?;
+6 -6
View File
@@ -303,7 +303,7 @@ impl Beside {
let edge = record.edges.get(self.edge).ok_or_else(|| KernelError::Failed("patch: edge reference".into()))?;
let [a, z] = edge.interval;
let tau = if self.forward { t } else { a + z - t };
let curve = CurveEval::new(&edge.curve).map_err(|e| KernelError::Invalid(e.to_string()))?;
let curve = CurveEval::new(edge.parameters()).map_err(|e| KernelError::Invalid(e.to_string()))?;
let Some((p, d)) = curve.derivs(tau) else { return Ok(None) };
let Some(tangent) = (d * if self.forward { 1.0 } else { -1.0 }).normalized() else { return Ok(None) };
let uv =
@@ -442,7 +442,7 @@ fn picked_loop(record: &b::Brep, at: &[Vec3], reach: f64) -> Result<Vec<(usize,
fn nearest_edge(record: &b::Brep, p: Vec3, reach: f64) -> Option<usize> {
let mut best: Option<(f64, usize)> = None;
for (i, e) in record.edges.iter().enumerate() {
let Ok(c) = CurveEval::new(&e.curve) else { continue };
let Ok(c) = CurveEval::new(e.parameters()) else { continue };
let [t0, t1] = e.interval;
for k in 0..=32 {
let Some(q) = c.point(t0 + (t1 - t0) * k as f64 / 32.0) else { continue };
@@ -468,7 +468,7 @@ struct Side {
impl Side {
fn new(record: &b::Brep, e: usize, forward: bool) -> Result<Side> {
let edge = record.edges.get(e).ok_or_else(|| KernelError::Failed("patch: edge reference".into()))?;
let curve = CurveEval::new(&edge.curve).map_err(|err| KernelError::Invalid(format!("edge {e} cannot be sampled: {err}")))?;
let curve = CurveEval::new(edge.parameters()).map_err(|err| KernelError::Invalid(format!("edge {e} cannot be sampled: {err}")))?;
let v = edge.vertices[if forward { 0 } else { 1 }];
let start = record.vertices.get(v as usize).map(|v| Vec3::new(v.point[0], v.point[1], v.point[2]));
let start = start.ok_or_else(|| KernelError::Failed("patch: vertex reference".into()))?;
@@ -785,7 +785,7 @@ fn middle(vertices: &mut Vec<[f64; 3]>, made: &mut std::collections::HashMap<usi
if let Some(v) = made.get(&k) {
return Ok(*v);
}
let p = CurveEval::new(&edge.curve).ok().and_then(|c| c.point(t)).ok_or_else(|| KernelError::Failed("patch: an edge has no middle".into()))?;
let p = CurveEval::new(edge.parameters()).ok().and_then(|c| c.point(t)).ok_or_else(|| KernelError::Failed("patch: an edge has no middle".into()))?;
let v = u32::try_from(vertices.len()).map_err(|_| KernelError::Failed("patch: vertex budget".into()))?;
vertices.push([p.x, p.y, p.z]);
made.insert(k, v);
@@ -882,7 +882,7 @@ mod tests {
let b::Loop::Edges { edges } = l else { continue };
for u in edges {
let e = &r.edges[u.edge as usize];
let c = CurveEval::new(&e.curve).unwrap();
let c = CurveEval::new(e.parameters()).unwrap();
let [t0, t1] = e.interval;
for k in 0..=24 {
worst = worst.max(to_surface(&face.surface, c.point(t0 + (t1 - t0) * k as f64 / 24.0).unwrap()));
@@ -1252,7 +1252,7 @@ mod tests {
.filter(|e| e.uses.len() == 1)
.map(|e| {
let [t0, t1] = e.interval;
CurveEval::new(&e.curve).unwrap().point(0.5 * (t0 + t1)).unwrap()
CurveEval::new(e.parameters()).unwrap().point(0.5 * (t0 + t1)).unwrap()
})
.collect()
}
+1 -1
View File
@@ -35,7 +35,7 @@ pub(crate) fn on_split_line(pts: &[Vec3], keep: &[Vec<Vec3>]) -> bool {
/// (the middle one at the middle of its interval).
pub(crate) fn edge_points(b: &Brep, e: u32) -> Vec<Vec3> {
let Some(edge) = b.edges.get(e as usize) else { return Vec::new() };
let Ok(c) = CurveEval::new(&edge.curve) else { return Vec::new() };
let Ok(c) = CurveEval::new(edge.parameters()) else { return Vec::new() };
let [t0, t1] = edge.interval;
(0..=4).filter_map(|k| c.point(t0 + (t1 - t0) * k as f64 / 4.0)).collect()
}
@@ -22,7 +22,7 @@ fn triangle_with_intersection_edge(support_gap: f64) -> b::Brep {
};
let curve = |k: usize| if k == 0 { traced.clone() } else { Curve::Line { start: corners[k], end: corners[(k + 1) % 3] } };
let edges = (0..3)
.map(|k| b::Edge { curve: curve(k), interval: [0.0, 1.0], vertices: [k as u32, ((k + 1) % 3) as u32], tolerance: 1e-6, uses: Vec::new() })
.map(|k| b::Edge { nurbs: None, curve: curve(k), interval: [0.0, 1.0], vertices: [k as u32, ((k + 1) % 3) as u32], tolerance: 1e-6, uses: Vec::new() })
.collect();
let uses = (0..3)
.map(|k| b::OrientedEdge {
+2 -2
View File
@@ -345,7 +345,7 @@ fn chain_polyline(r: &b::Brep, chain: &Chain) -> Result<Vec<Vec3>> {
let mut out = Vec::with_capacity(chain.len() * (PER_EDGE + 1));
for &(e, forward) in chain {
let edge = r.edges.get(e).ok_or_else(|| KernelError::Invalid("an edge reference".into()))?;
let c = CurveEval::new(&edge.curve).map_err(|err| KernelError::Failed(format!("match: {err}")))?;
let c = CurveEval::new(edge.parameters()).map_err(|err| KernelError::Failed(format!("match: {err}")))?;
let [t0, t1] = edge.interval;
let (a, z) = if forward { (t0, t1) } else { (t1, t0) };
for k in 0..=PER_EDGE {
@@ -420,7 +420,7 @@ fn side_edges(r: &b::Brep, face: usize, patch: &Patch, side: Side, tol: f64) ->
for u in edges {
let e = u.edge as usize;
let Some(edge) = r.edges.get(e) else { continue };
let Ok(c) = CurveEval::new(&edge.curve) else { continue };
let Ok(c) = CurveEval::new(edge.parameters()) else { continue };
let Some(p) = c.point(0.5 * (edge.interval[0] + edge.interval[1])) else { continue };
let Some(uv) = patch.parameters(p) else { continue };
if patch.surface.point(uv).is_none_or(|q| q.dist(p) > tol) {
+2 -2
View File
@@ -10,7 +10,7 @@ use std::sync::Arc;
/// Points along a record edge (ends and three inside).
fn edge_points(e: &brep::Edge) -> Vec<Vec3> {
let Ok(c) = CurveEval::new(&e.curve) else { return Vec::new() };
let Ok(c) = CurveEval::new(e.parameters()) else { return Vec::new() };
let [t0, t1] = e.interval;
(0..=4).filter_map(|k| c.point(t0 + (t1 - t0) * k as f64 / 4.0)).collect()
}
@@ -18,7 +18,7 @@ fn edge_points(e: &brep::Edge) -> Vec<Vec3> {
/// The point halfway along a record edge's interval.
fn edge_middle(e: &brep::Edge) -> Option<Vec3> {
let [t0, t1] = e.interval;
CurveEval::new(&e.curve).ok()?.point(0.5 * (t0 + t1))
CurveEval::new(e.parameters()).ok()?.point(0.5 * (t0 + t1))
}
fn failed(e: impl std::fmt::Display) -> KernelError {
+1 -1
View File
@@ -161,7 +161,7 @@ fn polyhedron_record(hs: &[HalfSpace], verts: &[Vec3], faces: &[(usize, Vec<usiz
Ok(i)
};
let vs = [number(s)?, number(t)?];
edges.push(rec::Edge { curve: rec::Curve::Line { start: p(s)?, end: p(t)? }, interval: [0.0, 1.0], vertices: vs, tolerance, uses });
edges.push(rec::Edge { nurbs: None, curve: rec::Curve::Line { start: p(s)?, end: p(t)? }, interval: [0.0, 1.0], vertices: vs, tolerance, uses });
}
let count = u32::try_from(out.len()).map_err(|_| bad())?;
let brep = rec::Brep {
+1 -1
View File
@@ -1272,7 +1272,7 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
}
let record_edges = edges
.iter()
.map(|e| b::Edge { curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance: tol, uses: Vec::new() })
.map(|e| b::Edge { nurbs: None, curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance: tol, uses: Vec::new() })
.collect();
let record_vertices = vertices.iter().map(|point| b::Vertex { point: *point, tolerance: tol }).collect();
let loose = b::Brep { version: b::VERSION, tolerance: tol, regions, shells, faces: record_faces, edges: record_edges, vertices: record_vertices };
+2 -2
View File
@@ -377,7 +377,7 @@ pub(crate) fn regions(split: &Body, tool: &SplitTool) -> Result<(Vec<usize>, usi
.collect::<Result<_>>()?;
let tol = (split.size() * 2e-3).max(1e-4);
let along = |e: &brep::Edge| -> bool {
let Ok(c) = CurveEval::new(&e.curve) else { return false };
let Ok(c) = CurveEval::new(e.parameters()) else { return false };
let [t0, t1] = e.interval;
let at: Vec<Vec3> = [0.25, 0.5, 0.75].iter().filter_map(|s| c.point(t0 + (t1 - t0) * s)).collect();
at.len() == 3 && fields.iter().any(|g| at.iter().all(|p| g.at(*p).abs() <= tol))
@@ -521,7 +521,7 @@ fn new_lines(split: &Body, h: &History) -> Result<Vec<Vec<Vec3>>> {
if prov.origins.iter().any(|o| matches!(o, EdgeOrigin::Edge { .. })) {
continue;
}
let c = CurveEval::new(&e.curve).map_err(|err| failed("split face", err))?;
let c = CurveEval::new(e.parameters()).map_err(|err| failed("split face", err))?;
let n = if matches!(e.curve, brep::Curve::Line { .. }) { 1 } else { 64 };
let [t0, t1] = e.interval;
let pts: Option<Vec<Vec3>> = (0..=n).map(|k| c.point(t0 + (t1 - t0) * k as f64 / n as f64)).collect();
+1 -1
View File
@@ -128,7 +128,7 @@ fn picked_faces(record: &brep::Brep, size: f64, at: &[Vec3]) -> Result<Vec<usize
.edges
.iter()
.enumerate()
.map(|(i, e)| CurveEval::new(&e.curve).map_err(|err| KernelError::Failed(format!("extend: edge {i}'s curve: {err}"))))
.map(|(i, e)| CurveEval::new(e.parameters()).map_err(|err| KernelError::Failed(format!("extend: edge {i}'s curve: {err}"))))
.collect::<Result<_>>()?;
let reach = size * 0.05 + 1e-6;
let mut faces = Vec::new();
+1 -1
View File
@@ -83,7 +83,7 @@ pub fn edge_circle(b: &Body, edge: usize) -> Option<EdgeCircle> {
}
let record = b.record();
let e = record.edges.get(edge)?;
let c = CurveEval::new(&e.curve).ok()?;
let c = CurveEval::new(e.parameters()).ok()?;
let [t0, t1] = e.interval;
let (start, end) = (c.point(t0)?, c.point(t1)?);
if let Curve::Circle { frame, radius } = &e.curve {
+3 -3
View File
@@ -967,7 +967,7 @@ fn face_of(mut parts: Parts, surface: b::Surface, uses: Vec<(b::Curve, [f64; 2],
let tolerance = parts.tolerance;
let mut given = Vec::with_capacity(uses.len());
for (curve, interval, vertices, forward, pc) in uses {
parts.edges.push(b::Edge { curve, interval, vertices, tolerance, uses: Vec::new() });
parts.edges.push(b::Edge { nurbs: None, curve, interval, vertices, tolerance, uses: Vec::new() });
given.push((forward, pc.map(|(a, z)| uv_line(a, z, interval))));
}
let loop_uses = parts
@@ -977,7 +977,7 @@ fn face_of(mut parts: Parts, surface: b::Surface, uses: Vec<(b::Curve, [f64; 2],
.enumerate()
.map(|(i, (e, (forward, pc)))| {
let edge = u32::try_from(i).map_err(|_| KernelError::Failed("ruled surface: an edge".into()))?;
Ok(b::pcurve::EdgeUse { edge, curve: &e.curve, interval: e.interval, forward: *forward, tolerance, given: pc.as_ref() })
Ok(b::pcurve::EdgeUse { edge, curve: e.parameters(), interval: e.interval, forward: *forward, tolerance, given: pc.as_ref() })
})
.collect::<Result<Vec<_>>>()?;
let pcurves = b::pcurve::pcurves(&surface, &[loop_uses], tolerance).map_err(|e| KernelError::Failed(format!("ruled surface: {e}")))?;
@@ -1296,7 +1296,7 @@ mod tests {
.edges
.iter()
.filter(|e| {
let c = solidcraft_geom::brep::eval::CurveEval::new(&e.curve).unwrap();
let c = solidcraft_geom::brep::eval::CurveEval::new(e.parameters()).unwrap();
on(c.point(0.5 * (e.interval[0] + e.interval[1])).unwrap())
})
.count()
+1 -1
View File
@@ -196,7 +196,7 @@ fn nearest_boundary_edge(x: &Body, side: &ToolSide) -> Result<Vec3> {
let record = crate::native::brep_record(x)?;
let mut best: Option<(f64, Vec3)> = None;
for e in record.edges.iter().filter(|e| e.uses.len() == 1) {
let curve = CurveEval::new(&e.curve).map_err(|err| KernelError::Failed(format!("trim: an edge's curve: {err}")))?;
let curve = CurveEval::new(e.parameters()).map_err(|err| KernelError::Failed(format!("trim: an edge's curve: {err}")))?;
let Some(mid) = curve.point(0.5 * (e.interval[0] + e.interval[1])) else { continue };
let d = side.at(mid).abs();
if d.is_finite() && best.is_none_or(|(b, _)| d < b) {
+7 -6
View File
@@ -376,7 +376,7 @@ pub(crate) fn tensor_sheet(n: &NurbsSurface) -> Result<Body> {
let mut uses = Vec::with_capacity(4);
for (i, (ctrl, knots, [a, z], [pa, pz], forward)) in sides.into_iter().enumerate() {
let (curve, interval) = spline_curve(&ctrl, knots)?;
parts.edges.push(b::Edge { curve, interval, vertices: [index(a)?, index(z)?], tolerance: parts.tolerance, uses: Vec::new() });
parts.edges.push(b::Edge { nurbs: None, curve, interval, vertices: [index(a)?, index(z)?], tolerance: parts.tolerance, uses: Vec::new() });
uses.push(b::OrientedEdge { edge: index(i)?, forward, pcurve: uv_line(pa, pz, interval) });
}
parts.faces.push((b::Face { nurbs: None, surface: spline_surface(n), domain: n.domain(), loops: vec![b::Loop::Edges { edges: uses }] }, true));
@@ -428,7 +428,7 @@ fn sketch_loop(parts: &mut Parts, plane: &Plane, segs: &[Seg2]) -> Result<b::Loo
let (curve, pcurve) = crate::freeform::segment(plane, s)?;
let vertices = [index(v0 + i)?, index(v0 + (i + 1) % segs.len())?];
let edge = index(parts.edges.len())?;
parts.edges.push(b::Edge { curve, interval: crate::freeform::INTERVAL, vertices, tolerance: parts.tolerance, uses: Vec::new() });
parts.edges.push(b::Edge { nurbs: None, curve, interval: crate::freeform::INTERVAL, vertices, tolerance: parts.tolerance, uses: Vec::new() });
edges.push(b::OrientedEdge { edge, forward: true, pcurve });
}
Ok(b::Loop::Edges { edges })
@@ -440,7 +440,7 @@ pub(crate) fn edge_near(body: &Body, p: Vec3) -> Option<usize> {
let r = body.record();
let mut best: Option<(f64, usize)> = None;
for (i, e) in r.edges.iter().enumerate() {
let Ok(c) = CurveEval::new(&e.curve) else { continue };
let Ok(c) = CurveEval::new(e.parameters()) else { continue };
let [t0, t1] = e.interval;
for k in 0..=32 {
let Some(q) = c.point(t0 + (t1 - t0) * k as f64 / 32.0) else { continue };
@@ -667,7 +667,7 @@ fn without_t_junctions(c: Canonical, offsets: &Offsets, tol: f64) -> Result<Cano
let point = |v: u32| r.vertices.get(v as usize).map(|x| Vec3::new(x.point[0], x.point[1], x.point[2]));
let mut splits: Vec<(u32, f64)> = Vec::new();
for (e, (edge, ke)) in r.edges.iter().zip(&input_of_edge).enumerate() {
let Ok(curve) = CurveEval::new(&edge.curve) else { continue };
let Ok(curve) = CurveEval::new(edge.parameters()) else { continue };
let ends: Vec<Vec3> = edge.vertices.iter().filter_map(|v| point(*v)).collect();
// Only vertices within a chord's reach of the edge's samples can lie on it.
let [a, z] = edge.interval;
@@ -816,6 +816,7 @@ pub fn extend(body: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
let (Some(a), Some(z)) = (parts.vertices.get(i), parts.vertices.get((i + 1) % m)) else { continue };
let curve = b::Curve::Line { start: a.point, end: z.point };
parts.edges.push(b::Edge {
nurbs: None,
curve,
interval: [0.0, 1.0],
vertices: [index(i)?, index((i + 1) % m)?],
@@ -828,7 +829,7 @@ pub fn extend(body: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
.iter()
.enumerate()
.map(|(i, e)| {
Ok(b::pcurve::EdgeUse { edge: index(i)?, curve: &e.curve, interval: e.interval, forward: true, tolerance: e.tolerance, given: None })
Ok(b::pcurve::EdgeUse { edge: index(i)?, curve: e.parameters(), interval: e.interval, forward: true, tolerance: e.tolerance, given: None })
})
.collect::<Result<Vec<_>>>()?;
let surface = b::Surface::Plane { origin: a3(origin), u_axis: a3(du), v_axis: a3(dv) };
@@ -899,7 +900,7 @@ fn check_chains(chains: &[Vec<Seg2>], what: &str) -> Result<()> {
fn swept_from(r: &Brep, start: &Plane, pieces: &[Numbered]) -> Vec<FaceProv> {
let mut from: Vec<BTreeSet<usize>> = vec![BTreeSet::new(); r.faces.len()];
for e in &r.edges {
let Some(mid) = CurveEval::new(&e.curve).ok().and_then(|c| c.point(0.5 * (e.interval[0] + e.interval[1]))) else { continue };
let Some(mid) = CurveEval::new(e.parameters()).ok().and_then(|c| c.point(0.5 * (e.interval[0] + e.interval[1]))) else { continue };
let near = 4.0 * e.tolerance.max(r.tolerance);
if start.height(mid).abs() > near {
continue;
+1 -1
View File
@@ -53,7 +53,7 @@ pub(crate) fn sample_edge(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> R
}
return Ok(Nodes { points: pts, params });
}
let curve = CurveEval::new(&edge.curve).map_err(|e| fail(e.to_string()))?;
let curve = CurveEval::new(edge.parameters()).map_err(|e| fail(e.to_string()))?;
let at =
|t: f64| -> Result<Vec3> { curve.point(t).filter(|p| p.is_finite()).ok_or_else(|| fail(format!("the curve does not evaluate at t = {t}"))) };
// A closed curve's chord between its ends is empty: start it from a few segments. An open
+1 -1
View File
@@ -119,7 +119,7 @@ fn patch_faces(patches: &[BezierPatch]) -> Result<Parts> {
let knots = std::iter::repeat_n(0.0, degree + 1).chain(std::iter::repeat_n(1.0, degree + 1)).collect();
let (curve, interval) = spline_curve(&seg, knots)?;
let e = index(parts.edges.len())?;
parts.edges.push(b::Edge { curve, interval, vertices, tolerance: 0.0, uses: Vec::new() });
parts.edges.push(b::Edge { nurbs: None, curve, interval, vertices, tolerance: 0.0, uses: Vec::new() });
edges.insert((ka, kb), e);
(e, (ka, kb))
}
+2 -2
View File
@@ -1162,7 +1162,7 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerances: &Tolerance
for ((&e, vertices), uses) in edge_order.iter().zip(edge_vertices).zip(edge_uses) {
let edge = edges.get(e as usize).ok_or_else(|| invalid("edge"))?;
let source = edge_source.get(e as usize).copied().ok_or_else(|| invalid("edge"))?;
edge_records.push(b::Edge { curve: edge.curve.clone(), interval: edge.interval, vertices, tolerance: tolerances.edge(source), uses });
edge_records.push(b::Edge { nurbs: None, curve: edge.curve.clone(), interval: edge.interval, vertices, tolerance: tolerances.edge(source), uses });
}
let mut face_records = Vec::with_capacity(face_order.len());
for (fi, &f) in face_order.iter().enumerate() {
@@ -1481,7 +1481,7 @@ fn pcurves(
fn outer_area(face: &Face, edges: &[Edge], tolerance: f64) -> Res<f64> {
let Some(Loop::Edges(outer)) = face.loops.first() else { return Ok(0.0) };
let records: Vec<b::Edge> =
edges.iter().map(|e| b::Edge { curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance, uses: Vec::new() }).collect();
edges.iter().map(|e| b::Edge { nurbs: None, curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance, uses: Vec::new() }).collect();
let given = face.given.first().map(std::slice::from_ref).unwrap_or(&[]);
let curves = pcurves(face.nurbs.as_ref().unwrap_or(&face.surface), face.domain, &[Loop::Edges(outer.clone())], given, &records, tolerance)?;
let mut pts: Vec<[f64; 2]> = Vec::new();
+1 -1
View File
@@ -69,7 +69,7 @@ fn polyhedron(corners: &[[f64; 3]], cycles: &[&[usize]]) -> Record {
.zip(uses)
.map(|(&(s, t), uses)| {
let vs = [number(s, &mut vertices), number(t, &mut vertices)];
Edge { curve: Curve::Line { start: corners[s], end: corners[t] }, interval: [0.0, 1.0], vertices: vs, tolerance: 1e-6, uses }
Edge { nurbs: None, curve: Curve::Line { start: corners[s], end: corners[t] }, interval: [0.0, 1.0], vertices: vs, tolerance: 1e-6, uses }
})
.collect();
Record {
+1
View File
@@ -842,6 +842,7 @@ impl<'b, 'a> Builder<'b, 'a> {
for &e in &edge_order {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
edges.push(Edge {
nurbs: None,
curve: edge.curve.clone(),
interval: edge.interval,
vertices: [vmap(edge.vertices[0])?, vmap(edge.vertices[1])?],
+146
View File
@@ -0,0 +1,146 @@
# Kernel complexity ladder
Generated by `cargo xtask kernel-cases --ladder`. The ladder in [`tests/kernel-cases/ladder/`](../tests/kernel-cases/ladder/) orders kernel operations by complexity: each tier builds on the shapes of the tiers before it, so the last case carries everything. Every case is a test with its own right answer (analysis or invariants, as in the [regression rig](kernel-cases.md)); the golden (`tests/kernel-cases/ladder/golden.json`) records each case's time and what it made, orders each tier by time and flags slowdowns. The cases run on SMLib.
The **frontier** is the first case of a tier, in ladder order, that fails (fail, crash or hang).
## Tiers
| Tier | What | Cases | Results | Frontier |
|---|---|---|---|---|
| 01-primitives | primitives with closed-form volumes | 6 | 6 pass | – |
| 02-booleans | booleans in the degeneracy classes, and cuts that end in a single line | 20 | 1 fail, 19 pass | 018-hole-tangent-to-side-face |
| 03-analytic-profiles | extrudes and revolves of line-and-arc profiles | 9 | 9 pass | – |
| 04-spline-profiles | spline profiles, sweeps and lofts | 10 | 10 pass | – |
| 05-holes-and-taps | holes and tapped holes | 13 | 1 fail, 12 pass | 010-hole-tangent-to-end-face |
| 06-threads | modelled threads | 10 | 1 fail, 7 pass, 2 unsupported | 006-external-m10-into-chamfer |
| 07-fillets | constant, variable and corner fillets | 17 | 3 fail, 12 pass, 2 unsupported | 012-variable-into-rounded-corner |
| 08-chamfers | chamfers | 8 | 1 fail, 7 pass | 008-part-chamfers |
| 09-shells-and-offsets | shells and offsets | 11 | 2 fail, 9 pass | 010-shell-under-rounded-top |
| 10-patterns-and-mirrors | patterns and mirrors | 8 | 1 fail, 7 pass | 008-part-vents |
| 11-cumulative-parts | parts combining every tier | 4 | 3 fail, 1 pass | 002-flange |
Cases where SMLib's check gave no verdict on a body (SMLib can't take the record yet), so only the record's checks graded validity: 0.
## Cases, in ladder order
Times are the commands' own, in seconds, from the case's process; **golden** is the recorded time.
| Case | What | Answer | Result | s | golden s | changes |
|---|---|---|---|---|---|---|
| ladder/01-primitives/001-box | a 60x40x20 box: the stock every later tier starts from | analytic | pass | 0.003 | – | status was unsupported |
| ladder/01-primitives/002-cylinder | a cylinder r10 h30 | analytic | pass | 0.031 | – | status was unsupported |
| ladder/01-primitives/004-frustum | a cone frustum r10 -> r5, h20 (revolved trapezoid) | analytic | pass | 0.035 | – | status was unsupported |
| ladder/01-primitives/003-cone | a cone r10 h30 (no cone primitive: a revolved right triangle, its axis a sketch line) | analytic | pass | 0.028 | – | status was unsupported |
| ladder/01-primitives/005-sphere | a sphere r10 | analytic | pass | 0.009 | – | status was unsupported |
| ladder/01-primitives/006-torus | a torus R20 r5 | analytic | pass | 1.466 | – | status was unsupported |
| ladder/02-booleans/004-edge-on-edge-no-overlap | a cut tool that shares only the top front edge of the stock | analytic | pass | 0.010 | – | status was unsupported |
| ladder/02-booleans/003-pocket-flush-with-top | a 20x20x10 pocket whose tool's top is the stock's top face | analytic | pass | 0.011 | – | status was unsupported |
| ladder/02-booleans/015-knife-edge-wedge-cut | a triangular prism cut whose slope meets the bottom face along one line (a knife edge), its other faces on the stock's | analytic | pass | 0.007 | – | status was unsupported |
| ladder/02-booleans/002-coplanar-overlap-join | two 60x40x20 boxes overlapping 30x20 in plan, tops and bottoms coplanar | analytic | pass | 0.007 | – | status was unsupported |
| ladder/02-booleans/019-notch-flush-with-end-face | a notch 40x20 through the height, its end flush with the stock's end face: a U | analytic | pass | 0.007 | – | status was unsupported |
| ladder/02-booleans/016-v-groove-pinches-bottom-to-line | a V groove whose apex touches the bottom face along a line, cut through: two pieces | analytic | pass | 0.007 | – | status was unsupported |
| ladder/02-booleans/013-cut-across-cylinder-seam | a 10x4 notch through a cylinder r10 across its +X side (where the seam lies) | analytic | pass | 0.063 | – | status was unsupported |
| ladder/02-booleans/001-coincident-face-join | a 60x40x10 box joined exactly on the stock's top face: one 60x40x30 box | analytic | pass | 0.007 | – | status was unsupported |
| ladder/02-booleans/009-near-tangent-overlap-1e-3 | a cylinder r10 along X dipping 1e-3 mm into the top face, cut: a groove 1e-3 deep | analytic | pass | 0.032 | – | status was unsupported |
| ladder/02-booleans/020-part-back-wall | the running part: a 60x10x15 back wall joined on the stock's top, flush with its back and ends | analytic | pass | 0.009 | – | status was unsupported |
| ladder/02-booleans/005-vertex-on-face-no-overlap | an inverted cone whose apex touches the middle of the top face, cut | analytic | pass | 0.042 | – | status was unsupported |
| ladder/02-booleans/018-hole-tangent-to-side-face | a through hole r10 tangent to the front face: the front face pinched to a line | analytic | **FAIL**: measure Part: the operation failed: not supported yet: tessellation: face 4 (plane): boundary crosses itself (want measurable); body_count 0 (want 1); volume_mm3 0 (want 41716.814693) | 0.068 | – | status was unsupported |
| ladder/02-booleans/010-near-tangent-overlap-1e-6 | a cylinder r10 along X dipping 1e-6 mm into the top face, cut | invariants | pass | 0.034 | – | status was unsupported |
| ladder/02-booleans/006-tangent-along-line-no-overlap | a cylinder r10 along X lying on the top face (tangent along a line), cut | analytic | pass | 0.031 | – | status was unsupported |
| ladder/02-booleans/008-near-tangent-gap-1e-6 | a cylinder r10 along X 1e-6 mm above the top face, cut | analytic | pass | 0.032 | – | status was unsupported |
| ladder/02-booleans/007-near-tangent-gap-1e-3 | a cylinder r10 along X 1e-3 mm above the top face, cut | analytic | pass | 0.032 | – | status was unsupported |
| ladder/02-booleans/017-cylinder-tangent-top-and-bottom | a cylinder r10 along X tangent to both the top and bottom faces, cut: two pieces with zero-angle cusps | analytic | pass | 0.060 | – | status was unsupported |
| ladder/02-booleans/011-tangent-along-circle-through-hole | a sphere r10 sitting in a through hole r10 at the top edge (tangent along the hole's top circle), cut | analytic | pass | 0.094 | – | status was unsupported |
| ladder/02-booleans/014-cut-through-sphere-pole | a 4x4 square post cut off the top of a sphere r10 through its pole | analytic | pass | 0.024 | – | status was unsupported |
| ladder/02-booleans/012-tangent-along-circle-bowl | a blind hole r10 10 deep, then a sphere r10 centred on its floor: the floor disk vanishes exactly | analytic | pass | 0.103 | – | status was unsupported |
| ladder/03-analytic-profiles/001-extrude-rect-with-arc-end | a 60x40 profile with a semicircular end r20, extruded 20 | analytic | pass | 0.016 | – | status was unsupported |
| ladder/03-analytic-profiles/003-extrude-tapered-circle | a circle r10 extruded 20 with a -5 deg taper (inward): a cone frustum | analytic | pass | 0.043 | – | status was unsupported |
| ladder/03-analytic-profiles/007-revolve-180-touching-axis | a 15x20 rectangle on the axis revolved 180 deg: a half cylinder | analytic | pass | 0.017 | – | status was unsupported |
| ladder/03-analytic-profiles/008-extrude-slot | a slot (centres 40 apart, width 20) extruded 10 | analytic | pass | 0.020 | – | status was unsupported |
| ladder/03-analytic-profiles/002-extrude-annulus | an annulus r20/r10 extruded 15 | analytic | pass | 0.057 | – | status was unsupported |
| ladder/03-analytic-profiles/004-revolve-full-off-axis | a 10x30 rectangle 10 mm off the Z axis revolved 360 deg: a tube | analytic | pass | 0.055 | – | status was unsupported |
| ladder/03-analytic-profiles/006-revolve-partial-90 | a 10x10 rectangle 10 mm off the axis revolved 90 deg | analytic | pass | 0.013 | – | status was unsupported |
| ladder/03-analytic-profiles/005-revolve-touching-axis-hemisphere | a quarter disk r20 with an edge on the Z axis revolved 360 deg: a hemisphere | analytic | pass | 0.085 | – | status was unsupported |
| ladder/03-analytic-profiles/009-part-boss-and-slot | the running part: a revolved boss r8 h10 joined on the top, a slot 15x8 cut through the base | analytic | pass | 0.148 | – | status was unsupported |
| ladder/04-spline-profiles/001-extrude-bezier-arch | a profile closed by a cubic Bezier arch (control-point spline, 4 points), extruded 10 | analytic | pass | 0.010 | – | status was unsupported |
| ladder/04-spline-profiles/008-loft-square-to-circle | a loft from a 40 square at z=0 to a circle r10 at z=30 | invariants | pass | 0.018 | – | status was unsupported |
| ladder/04-spline-profiles/006-loft-circle-frustum | a loft between circles r20 at z=0 and r10 at z=30 | analytic | pass | 0.039 | – | status was unsupported |
| ladder/04-spline-profiles/007-loft-three-circles | a smooth loft through circles r20, r10, r20 at z = 0, 20, 40 | invariants | pass | 0.059 | – | status was unsupported |
| ladder/04-spline-profiles/005-loft-square-frustum | a loft between a 40 square at z=0 and a 20 square at z=30 | analytic | pass | 0.005 | – | status was unsupported |
| ladder/04-spline-profiles/009-extrude-periodic-fit-spline | a closed periodic fit-point spline through 6 points, extruded 10 | invariants | pass | 0.021 | – | status was unsupported |
| ladder/04-spline-profiles/004-sweep-square-along-bezier | a 4x4 square swept along the same Bezier path | analytic | pass | 0.034 | – | status was unsupported |
| ladder/04-spline-profiles/002-revolve-bezier-profile | a profile with a Bezier side (r from 20 to 15 over z 0..30) revolved 360 deg about Z | analytic | pass | 0.092 | – | status was unsupported |
| ladder/04-spline-profiles/003-pipe-along-bezier | a pipe d4 along a planar Bezier path | analytic | pass | 0.115 | – | status was unsupported |
| ladder/04-spline-profiles/010-part-bezier-window | the running part: a Bezier-arched window cut through the back wall | analytic | pass | 0.212 | – | status was unsupported |
| ladder/05-holes-and-taps/010-hole-tangent-to-end-face | a d10 through hole tangent to the end face x=0 | analytic | **FAIL**: measure Part: the operation failed: not supported yet: tessellation: face 4 (plane): triangulation does not (want measurable); body_count 0 (want 1); volume_mm3 0 (want 46429.203673) | 0.055 | – | status was unsupported |
| ladder/05-holes-and-taps/002-simple-blind-flat | a d10 blind hole 10 deep, flat bottom | analytic | pass | 0.062 | – | status was unsupported |
| ladder/05-holes-and-taps/009-hole-0.5-from-edge | a d10 through hole whose wall is 0.5 mm from the end face | analytic | pass | 0.045 | – | status was unsupported |
| ladder/05-holes-and-taps/001-simple-through | a d10 through hole in the stock's top | analytic | pass | 0.045 | – | status was unsupported |
| ladder/05-holes-and-taps/006-tapped-m6-through | an M6 tapped hole through: the tap drill d5 (major - pitch), cosmetic thread | analytic | pass | 0.038 | – | status was unsupported |
| ladder/05-holes-and-taps/004-counterbore | a counterbored hole: d6.6 through, d11 x 6.4 counterbore | analytic | pass | 0.121 | – | status was unsupported |
| ladder/05-holes-and-taps/007-tapped-m8-blind | an M8 tapped blind hole 12 deep with a 118 deg drill point | analytic | pass | 0.064 | – | status was unsupported |
| ladder/05-holes-and-taps/003-drilled-blind-118 | a d10 drilled hole 10 deep to the shoulder with a 118 deg point | analytic | pass | 0.073 | – | status was unsupported |
| ladder/05-holes-and-taps/005-countersink | a countersunk hole: d6.6 through, d12 x 90 deg countersink | analytic | pass | 0.140 | – | status was unsupported |
| ladder/05-holes-and-taps/008-tapped-m5-into-curved-face | an M5 tapped hole drilled radially through a cylinder r20 lying along X | analytic | pass | 0.134 | – | status was unsupported |
| ladder/05-holes-and-taps/011-crossing-holes-steinmetz | two d10 through holes whose axes cross: vertical and along Y at z=10 | analytic | pass | 0.244 | – | status was unsupported |
| ladder/05-holes-and-taps/012-crossing-tapped-holes-offset | two M6 tapped holes (d5) crossing with axes 2 mm apart | analytic | pass | 0.128 | – | status was unsupported |
| ladder/05-holes-and-taps/013-part-holes | the running part: an M6 tapped through hole, an M5 tapped blind hole in the boss, a d3.3 hole down through wall and base | analytic | pass | 0.604 | – | status was unsupported |
| ladder/06-threads/003-external-m10-partial-length | a modelled M10 thread over 15 of the 30 mm: it stops square in the middle of the face | invariants | pass | 1.146 | – | status was unsupported |
| ladder/06-threads/005-internal-m10-through | a modelled internal M10 thread in a d8.5 (tap drill) hole through a 30x30x20 block | analytic | pass | 1.392 | – | status was unsupported |
| ladder/06-threads/004-external-m10-to-shoulder | a modelled M10 thread on a d10 shaft running down to a d16 shoulder | invariants | pass | 2.444 | – | status was unsupported |
| ladder/06-threads/001-external-m10-full | a modelled M10 thread over the whole of a d10 x 30 cylinder | analytic | pass | 2.593 | – | status was unsupported |
| ladder/06-threads/006-external-m10-into-chamfer | a d10 x 30 cylinder with a 1 mm chamfer on top, then a modelled M10 thread over its cylinder face, running out through the chamfer | analytic | **FAIL**: record_valid geometry: portable B-rep: face 25 loop 0 use 1: pcurve middle lifts off its edge's curve by 0.000045 (want ok) | 2.764 | – | status was unsupported |
| ladder/06-threads/002-external-m10x1-fine | a modelled fine M10x1 thread over a d10 x 30 cylinder | analytic | pass | 4.590 | – | status was unsupported |
| ladder/06-threads/010-part-threaded-boss | the running part with a modelled M16 thread on its d16 boss | invariants | pass | 1.482 | – | status was unsupported |
| ladder/06-threads/007-external-m10-at-filleted-shoulder | a modelled M10 thread on a d10 shaft whose root at the d16 shoulder has an r1 fillet | invariants | pass | 2.568 | – | status was unsupported |
| ladder/06-threads/008-multi-start-thread | a two-start M10 thread (lead 3, pitch 1.5) | invariants | not supported yet: solid.thread has no start count: multi-start threads | – | – | |
| ladder/06-threads/009-thread-runout-length | an M10 thread that vanishes over a 2-pitch runout before a shoulder | invariants | not supported yet: solid.thread has no runout (vanishing thread) length | – | – | |
| ladder/07-fillets/006-variable-r5-to-zero | a variable round on the top front edge, r5 at x=0 down to 0 at the x=60 vertex | analytic | pass | 0.018 | – | status was unsupported |
| ladder/07-fillets/005-variable-r5-to-r1 | a variable round on the top front edge, r5 at x=0 to r1 at x=60 | analytic | pass | 0.018 | – | status was unsupported |
| ladder/07-fillets/007-variable-r5-to-1e-3 | a variable round on the top front edge, r5 down to 1e-3 mm | analytic | pass | 0.018 | – | status was unsupported |
| ladder/07-fillets/015-corner-g2-patch | three r5 rounds at a corner with the g2 corner patch | invariants | not supported yet: step 2 (solid.fillet): invalid input: not supported yet: a fillet with G2 corner patches with the SMLib kernel | 0.005 | – | |
| ladder/07-fillets/008-constant-r1e-3 | a constant 1e-3 mm round on the top front edge | analytic | pass | 0.012 | – | status was unsupported |
| ladder/07-fillets/004-convex-and-concave-r5 | an L block: r5 on its inner (concave) vertical edge and on an outer (convex) one | analytic | pass | 0.025 | – | status was unsupported |
| ladder/07-fillets/001-single-edge-r5 | an r5 round on the stock's top front edge | analytic | pass | 0.012 | – | status was unsupported |
| ladder/07-fillets/012-variable-into-rounded-corner | a vertical r5 round, then a variable round r1 -> r5 along the top back edge into it | invariants | **FAIL**: step 3 (solid.fillet): SMLib variable fillet: operation: SMLib failed (status 1001) | 0.020 | – | status was unsupported |
| ladder/07-fillets/014-curved-edge-r2 | an r2 round on the top edge of a cylinder r10 h20 | analytic | pass | 0.113 | – | status was unsupported |
| ladder/07-fillets/011-chain-ending-at-corner | r5 rounds on two top edges meeting at a corner whose vertical edge stays sharp | invariants | pass | 0.017 | – | status was unsupported |
| ladder/07-fillets/002-four-vertical-edges-r5 | r5 rounds on the four vertical edges | analytic | pass | 0.028 | – | status was unsupported |
| ladder/07-fillets/009-corner-three-equal-r5 | r5 rounds on the three edges meeting at the corner (60,40,20): a sphere corner | analytic | pass | 0.025 | – | status was unsupported |
| ladder/07-fillets/010-corner-unequal-r7-r3 | an r7 vertical round at a corner, then r3 along a top edge, which runs on round the r7 arc to the other top edge | invariants | pass | 0.042 | – | status was unsupported |
| ladder/07-fillets/003-tangent-chain-r3 | an r3 round all round the top of the box with r5 vertical rounds: a tangent chain of lines and arcs | analytic | pass | 0.090 | – | status was unsupported |
| ladder/07-fillets/016-corner-three-sequential-r5-r3-r7 | rounds r5 and r3 on the two top edges at a corner, then r7 on the vertical edge under them | invariants | **FAIL**: step 4 (solid.fillet): SMLib fillet: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT) | 0.051 | – | status was unsupported |
| ladder/07-fillets/017-part-rounds | the running part: r5 on the front vertical edges, r3 on the back wall's top front edge, r1 where the boss meets the top | analytic | **FAIL**: step 21 (solid.fillet): SMLib fillet: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT) | 0.986 | – | status was unsupported |
| ladder/07-fillets/013-corner-setback | three r5 rounds at a corner with setbacks of 8 mm along each edge | invariants | not supported yet: solid.fillet has no corner setback distances | – | – | |
| ladder/08-chamfers/002-two-distances-5-3 | a 5 x 3 chamfer on the top front edge | analytic | pass | 0.009 | – | status was unsupported |
| ladder/08-chamfers/001-equal-5 | a 5 mm chamfer on the top front edge | analytic | pass | 0.010 | – | status was unsupported |
| ladder/08-chamfers/003-distance-angle-5-30 | a chamfer 5 mm at 30 deg on the top front edge | analytic | pass | 0.010 | – | status was unsupported |
| ladder/08-chamfers/005-corner-two-mitred-3 | 3 mm chamfers on two top edges meeting at a corner (mitred) | analytic | pass | 0.014 | – | status was unsupported |
| ladder/08-chamfers/004-corner-three-equal-3 | 3 mm chamfers on the three edges meeting at a corner | analytic | pass | 0.015 | – | status was unsupported |
| ladder/08-chamfers/007-chamfer-then-fillets | a 3 mm chamfer on the top front edge, then r2 rounds on the front vertical edges that meet it | invariants | pass | 0.029 | – | status was unsupported |
| ladder/08-chamfers/006-curved-edge-2 | a 2 mm chamfer on the top edge of a cylinder r10 h20 | analytic | pass | 0.117 | – | status was unsupported |
| ladder/08-chamfers/008-part-chamfers | the running part: 1 mm on the boss top edge, 2 mm on the back wall's top back edge | analytic | **FAIL**: step 21 (solid.fillet): SMLib fillet: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT) | 0.999 | – | status was unsupported |
| ladder/09-shells-and-offsets/007-offset-top-face-5 | the stock's top face offset 5 mm | analytic | pass | 0.016 | – | status was unsupported |
| ladder/09-shells-and-offsets/003-tube-two-open-faces | the stock shelled 2 mm, open top and bottom | analytic | pass | 0.017 | – | status was unsupported |
| ladder/09-shells-and-offsets/001-box-open-top-t2 | the stock shelled 2 mm, open at the top | analytic | pass | 0.021 | – | status was unsupported |
| ladder/09-shells-and-offsets/004-thin-wall-0.2 | the stock shelled 0.2 mm, open at the top | analytic | pass | 0.015 | – | status was unsupported |
| ladder/09-shells-and-offsets/009-shell-outside-t2 | the stock shelled 2 mm outward, open at the top | analytic | pass | 0.044 | – | status was unsupported |
| ladder/09-shells-and-offsets/008-offset-cylinder-wall-2 | a cylinder r10 h20 with its side face offset 2 mm | analytic | pass | 0.155 | – | status was unsupported |
| ladder/09-shells-and-offsets/002-cylinder-open-top-t1 | a cylinder r10 h30 shelled 1 mm, open at the top | analytic | pass | 0.160 | – | status was unsupported |
| ladder/09-shells-and-offsets/006-shell-rounds-r1-thinner-than-t2 | the box with r1 vertical rounds shelled 2 mm: the inner corners close sharp | analytic | pass | 0.063 | – | status was unsupported |
| ladder/09-shells-and-offsets/005-shell-rounds-r5-t2 | the box with r5 vertical rounds shelled 2 mm open at the top: the inner corners r3 | analytic | pass | 0.164 | – | status was unsupported |
| ladder/09-shells-and-offsets/010-shell-under-rounded-top | the stock with r3 rounds on its four top edges, shelled 2 mm open at the bottom | invariants | **FAIL**: step 3 (solid.shell): SMLib shell: SMLib's check refuses the result: 1 failed tests: SmBSplineCurve::Test #13 Bad CurveCusp: 2D BSplineCurve Warning: Extremely | 0.179 | – | status was unsupported |
| ladder/09-shells-and-offsets/011-part-shell | the running part shelled 1.5 mm open at the bottom | invariants | **FAIL**: step 21 (solid.fillet): SMLib fillet: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT) | 0.998 | – | status was unsupported |
| ladder/10-patterns-and-mirrors/007-pattern-bodies | a pin r3 h10 patterned 4 times at 10 mm as bodies | analytic | pass | 0.037 | – | status was unsupported |
| ladder/10-patterns-and-mirrors/005-mirror-feature | a d10 hole in the stock mirrored about x=30 as a feature | analytic | pass | 0.126 | – | status was unsupported |
| ladder/10-patterns-and-mirrors/004-mirror-body-joined | a 30x40x20 half with a d10 hole mirrored about x=30 and joined: the faces at x=30 coincide | analytic | pass | 0.138 | – | status was unsupported |
| ladder/10-patterns-and-mirrors/001-rect-pattern-holes | a d5 hole patterned 3 x 2 at 20 mm in a 60x40x10 plate | analytic | pass | 0.572 | – | status was unsupported |
| ladder/10-patterns-and-mirrors/003-overlapping-pattern-leaves-core | six r5 cuts patterned round a circle r6 in a disk r20: they overlap and cut a core free | analytic | pass | 0.836 | – | status was unsupported |
| ladder/10-patterns-and-mirrors/002-circular-pattern-holes | a d6 hole patterned 8 times round a disk r30 h10 | analytic | pass | 1.322 | – | status was unsupported |
| ladder/10-patterns-and-mirrors/006-circular-pattern-counterbores | a counterbored hole (d5, d9 x 3) patterned 6 times round a disk r30 h10 | analytic | pass | 2.310 | – | status was unsupported |
| ladder/10-patterns-and-mirrors/008-part-vents | the running part with a d3 vent hole in its top patterned 4 times at 5 mm | invariants | **FAIL**: step 21 (solid.fillet): SMLib fillet: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT) | 0.978 | – | status was unsupported |
| ladder/11-cumulative-parts/002-flange | a revolved flange and hub with a bore, six bolt holes, a hub chamfer and a hub-root round | analytic | **FAIL**: step 7 (solid.chamfer): SMLib chamfer: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT) | 1.244 | – | status was unsupported |
| ladder/11-cumulative-parts/001-bracket | an L bracket: concave r5 round, 5 mm outer chamfer, a hole in each leg | analytic | pass | 0.153 | – | status was unsupported |
| ladder/11-cumulative-parts/003-threaded-flange | the flange with a modelled M30 thread on its hub, between the round and the chamfer, and M8 tapped holes in the disk | invariants | **FAIL**: step 7 (solid.chamfer): SMLib chamfer: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT) | 1.171 | – | status was unsupported |
| ladder/11-cumulative-parts/004-housing | the running part with every tier: back wall, boss, slot, spline window, tapped holes, rounds, chamfers, shell, vents, and a modelled M16 thread on the boss | invariants | **FAIL**: step 21 (solid.fillet): SMLib fillet: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT) | 0.913 | – | status was unsupported |
+7
View File
@@ -120,6 +120,13 @@ kernel traces it again. Its point at t is the point of both surfaces in the plan
through the leader's point at t, normal to the leader there (truck's `subs`
converges to it within 10⁻⁶ mm).
An edge on a `line`, `circle` or `ellipse` may also carry `nurbs`, a `spline`
or `rational_spline`: the NURBS form it is parameterised on, as SMLib holds it
(every SMLib conic is a B-spline curve, the trims on it in that NURBS's
parameter, not the angle). The edge's interval and its pcurves' parameter are
then the NURBS's, and `curve` only says what the edge is (centre, radius). This
mirrors a face's `nurbs`: faces and edges are held as SMLib holds them.
### Surfaces
Natural normals are ∂S/∂u × ∂S/∂v.