SMLib import: an extruded circle is a cylinder over the face's trimmed range

This replaces the spline fallback of 64efbe7f6. A face SMLib holds as a rational spline that is
an extruded circle was recognised as a cylinder over the spline's whole parameter range: for the
upper half of the peg's wall after the front top round that range runs 393 degrees round (u in
[-15, 40.18], SMLib's support surface extended past the face), and SMLib refuses a cylinder past
one turn. The face itself, its loops' box, spans u in [0, 8 pi]: half a turn. The cylinder now
takes the face's trimmed range, the least and greatest u and v of its uses' pcurves over their
edges' intervals, found to rounding on each Bezier span (the values at the pieces' ends bound the
extreme from below, the control points from above; pieces that cannot beat the best are dropped,
the others halved). A face whose trims run more than one turn round is no face of a cylinder: it
is refused as an invalid record, never kept as a spline.

The peg (40 x 16 x 10 bar, R8 end, a disc R8 extruded 10 up on its top, joined), front top edge
rounded r2: the body measures to 1e-7 of its closed form, the three r2 corner rounds after it
build and measure, and the upper wall comes back from SMLib a cylinder of radius 8 (engine test).

The rig case of the round of the edge where that round meets the wall (tangent at one end) still
fails, for another reason: on the wall as its exact cylinder SMLib refuses the round as larger
than the faces beside its edges allow (it fades to nothing at the tangent end) instead of
returning a body its own check refuses. Its KNOWN_FAILING line says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 07:35:35 -04:00
committed by Irakli Kourtis
parent dfa84e84d1
commit 6cc7597502
3 changed files with 132 additions and 17 deletions
+26 -5
View File
@@ -467,11 +467,13 @@ fn peg_front_round(from: f64) -> f64 {
simpson(|y| spandrel(y) * (wall(y) - from), 0.0, 2.0, 20_000)
}
/// The peg's front top edge rounded r2: SMLib's result holds the cylinder's wall as a rational
/// spline whose parameter range runs more than a turn round. As a cylinder over that range (past
/// 360°) SMLib refused it on the way back (SM_ERR_INVALID_INPUT), so the round seemed to build
/// and the body could not be measured or changed. It stays the spline: the body measures, and a
/// round of the three edges at the bar's other top corner builds on it.
/// The peg's front top edge rounded r2: SMLib's result holds the upper half of the cylinder's
/// wall as a rational spline whose parameter range runs more than a turn round, past the face's
/// trims (half a turn). Taken as a cylinder over the spline's range (past 360°) SMLib refused it
/// on the way back (SM_ERR_INVALID_INPUT), so the round seemed to build and the body could not
/// be measured or changed. It goes back as the cylinder over the face's own range: the body
/// measures, a round of the three edges at the bar's other top corner builds on it, and that
/// wall comes back from SMLib a cylinder of radius 8.
#[test]
fn rounds_on_a_bar_running_tangent_into_a_cylinder() {
let mut s = Session::default();
@@ -491,6 +493,25 @@ fn rounds_on_a_bar_running_tangent_into_a_cylinder() {
let bad = b.body.validity();
assert!(bad.is_empty(), "{}: {bad:?}", b.name);
}
// The wall's upper half (x < 40, z in [10, 20], round (40, 8)): every face on the surface
// through (32, 8, 15) is a cylinder of radius 8.
let state = s.model.state();
let rec = state.bodies[0].body.record();
let p0 = solidcraft_geom::Vec3::new(32.0, 8.0, 15.0);
let on: Vec<&solidcraft_geom::brep::Surface> = rec
.faces
.iter()
.map(|f| &f.surface)
.filter(|s| {
let e = solidcraft_geom::brep::eval::SurfaceEval::new(s).ok();
e.and_then(|e| e.project(p0, None, 1e-6).and_then(|uv| e.point(uv))).is_some_and(|q| q.dist(p0) < 1e-6)
})
.collect();
let kinds: Vec<String> = on.iter().map(|s| format!("{s:?}").chars().take(48).collect()).collect();
assert!(
!on.is_empty() && on.iter().all(|s| matches!(s, solidcraft_geom::brep::Surface::Cylinder { radius, .. } if (radius - 8.0).abs() < 1e-9)),
"{kinds:?}"
);
}
/// After the peg's front top round, the edge where that round runs into the cylinder's wall: its
+105 -11
View File
@@ -22,6 +22,9 @@ use solidcraft_geom::brep::eval::{CurveEval, SurfaceEval};
use super::{R, Unsupported};
/// Halvings one span's extent may take.
const MAX_EXTENT_PIECES: usize = 4096;
const NOT_ANALYTIC: Unsupported = Unsupported::NotYet("revolved surfaces other than planes, cylinders, cones, spheres and tori");
/// `brep` with every revolution and extruded circle replaced by its analytic surface (borrowed
@@ -43,7 +46,12 @@ pub(super) fn analytic(brep: &b::Brep) -> R<Cow<'_, b::Brep>> {
}
}
b::Surface::RationalSpline { u_knots, v_knots, homogeneous } => {
extruded_circle(u_knots, v_knots, homogeneous, f.domain, 1e-9 * size.max(1.0))
let found = extruded_circle(u_knots, v_knots, homogeneous, trimmed(brep, f)?, 1e-9 * size.max(1.0));
// A face's trims run at most one turn round a cylinder (SMLib refuses more).
if found.as_ref().is_some_and(|(_, range)| range[0][1] - range[0][0] > TAU * (1.0 + 1e-9)) {
return Err(Unsupported::Invalid("a cylinder's face whose trims run more than one turn round"));
}
found
}
_ => None,
};
@@ -76,14 +84,16 @@ pub(super) fn analytic(brep: &b::Brep) -> R<Cow<'_, b::Brep>> {
/// A rational spline that is a circular arc swept straight along the arc's axis, as a cylinder
/// with the face's range on it: one direction linear (two rows of control points), the second
/// row the first moved by one vector at right angles to the circle the first row runs on, the
/// weights alike. `None` where it is not, or where its range runs more than one turn round.
fn extruded_circle(u_knots: &[f64], v_knots: &[f64], net: &[Vec<[f64; 4]>], domain: [[f64; 2]; 2], tol: f64) -> Option<(b::Surface, [[f64; 2]; 2])> {
/// weights alike. `None` where it is not. The range is the face's own, its trims' box `trims`
/// ([`trimmed`]), not the spline's: SMLib's spline of a cylinder's wall may run on past the face,
/// more than a turn round (a blend's support surface extended past the face's ends).
fn extruded_circle(u_knots: &[f64], v_knots: &[f64], net: &[Vec<[f64; 4]>], trims: [[f64; 2]; 2], tol: f64) -> Option<(b::Surface, [[f64; 2]; 2])> {
let linear = |k: &[f64]| k.len() == 4 && k[0] == k[1] && k[2] == k[3] && k[1] < k[2];
let (rows, arc_knots, arc_range, line_knots, line_range): ([Vec<[f64; 4]>; 2], &[f64], [f64; 2], &[f64], [f64; 2]) =
if net.iter().all(|r| r.len() == 2) && linear(v_knots) && !linear(u_knots) {
([net.iter().map(|r| r[0]).collect(), net.iter().map(|r| r[1]).collect()], u_knots, domain[0], v_knots, domain[1])
([net.iter().map(|r| r[0]).collect(), net.iter().map(|r| r[1]).collect()], u_knots, trims[0], v_knots, trims[1])
} else if net.len() == 2 && linear(u_knots) && !linear(v_knots) {
([net.first()?.clone(), net.get(1)?.clone()], v_knots, domain[1], u_knots, domain[0])
([net.first()?.clone(), net.get(1)?.clone()], v_knots, trims[1], u_knots, trims[0])
} else {
return None;
};
@@ -117,18 +127,102 @@ fn extruded_circle(u_knots: &[f64], v_knots: &[f64], net: &[Vec<[f64; 4]>], doma
prev = a;
angle = a;
}
// A cylinder's range is at most one turn (SMLib refuses more). A spline whose range runs
// further round (SMLib's own, its parameter range past the face's trims) has no such range:
// it stays the exact spline it is.
if angle.abs() > TAU * (1.0 + 1e-9) {
return None;
}
let height = |s: f64| (s - line_knots[1]) / (line_knots[2] - line_knots[1]) * d.len();
let sorted = |a: f64, b: f64| if a <= b { [a, b] } else { [b, a] };
let frame = b::Frame { origin: arr(center), x_axis: arr(x), y_axis: arr(y) };
Some((b::Surface::Cylinder { frame, radius }, [sorted(0.0, angle), sorted(height(line_range[0]), height(line_range[1]))]))
}
/// The box in its surface's parameters round face `f`'s loops: each use's pcurve over its edge's
/// interval, its least and greatest u and v ([`pcurve_box`]). The range the face takes of its
/// surface, which may run on past it.
fn trimmed(brep: &b::Brep, f: &b::Face) -> R<[[f64; 2]; 2]> {
let mut lo = [f64::INFINITY; 2];
let mut hi = [f64::NEG_INFINITY; 2];
for l in &f.loops {
let b::Loop::Edges { edges } = l else { return Err(Unsupported::Invalid("a vertex loop on a cylinder's face")) };
for u in edges {
let interval = brep.edges.get(u.edge as usize).ok_or(Unsupported::Invalid("edge"))?.interval;
let [ru, rv] = pcurve_box(&u.pcurve, interval).ok_or(Unsupported::Invalid("a pcurve that does not evaluate"))?;
for (k, r) in [ru, rv].into_iter().enumerate() {
lo[k] = lo[k].min(r[0]);
hi[k] = hi[k].max(r[1]);
}
}
}
if !(lo.iter().chain(&hi).all(|x| x.is_finite()) && lo[0] <= hi[0] && lo[1] <= hi[1]) {
return Err(Unsupported::Invalid("a face without trims"));
}
Ok([[lo[0], hi[0]], [lo[1], hi[1]]])
}
/// A pcurve's least and greatest u and v over `interval`, to rounding: on each Bézier span, the
/// greatest value found at a span's points (its ends, then the ends of its halves) bounds the
/// greatest from below, and its control points' (weights positive) from above; halves whose
/// control points can't beat the best found are dropped, the others halved, until the two meet.
fn pcurve_box(c: &b::Curve2, interval: [f64; 2]) -> Option<[[f64; 2]; 2]> {
use solidcraft_geom::bspline::{HPoint, bezier_segment, segment};
let (knots, ctrl): (&[f64], Vec<HPoint>) = match c {
b::Curve2::Spline { knots, points } => (knots, points.iter().map(|p| HPoint { x: p[0], y: p[1], z: 0.0, w: 1.0 }).collect()),
b::Curve2::RationalSpline { knots, homogeneous } => {
(knots, homogeneous.iter().map(|h| HPoint { x: h[0], y: h[1], z: 0.0, w: h[2] }).collect())
}
};
let p = knots.len().checked_sub(ctrl.len() + 1)?;
let [t0, t1] = interval;
let mut ends: Vec<f64> = vec![t0];
ends.extend(knots.iter().copied().filter(|k| *k > t0 && *k < t1));
ends.push(t1);
ends.dedup();
let mut out = [[f64::INFINITY, f64::NEG_INFINITY]; 2];
for w in ends.windows(2) {
let &[a, z] = w else { continue };
let (_, span) = segment(p, knots, &ctrl, a, z).ok()?;
if span.len() != p + 1 {
return None;
}
for (k, r) in out.iter_mut().enumerate() {
let along = |h: &HPoint| if k == 0 { h.x / h.w } else { h.y / h.w };
for sign in [1.0, -1.0] {
let value = |piece: &[HPoint]| -> Option<[f64; 2]> {
let (first, last) = (piece.first()?, piece.last()?);
let at = (sign * along(first)).max(sign * along(last));
let hull = piece.iter().map(|h| sign * along(h)).fold(f64::NEG_INFINITY, f64::max);
Some([at, hull])
};
let [mut best, _] = value(&span)?;
let mut pieces = vec![span.clone()];
let mut work = 0usize;
while let Some(piece) = pieces.pop() {
let [at, hull] = value(&piece)?;
best = best.max(at);
if hull <= best + 64.0 * f64::EPSILON * (1.0 + best.abs()) {
continue;
}
// Out of work (a pcurve running along its extreme to rounding), the piece's
// control points bound it: the box still holds the curve.
work += 1;
if work > MAX_EXTENT_PIECES {
best = hull;
continue;
}
pieces.push(bezier_segment(&piece, 0.0, 0.5).ok()?);
pieces.push(bezier_segment(&piece, 0.5, 1.0).ok()?);
}
if !best.is_finite() {
return None;
}
if sign > 0.0 {
r[1] = r[1].max(best);
} else {
r[0] = r[0].min(-best);
}
}
}
}
Some(out)
}
/// An analytic surface's frame turned by `angle` about its axis: u then counts from there.
fn turn_frame(s: &mut b::Surface, angle: f64) {
if let b::Surface::Cylinder { frame, .. } | b::Surface::Cone { frame, .. } | b::Surface::Sphere { frame, .. } | b::Surface::Torus { frame, .. } =
+1 -1
View File
@@ -52,4 +52,4 @@ every-pocket-rim-edges-r5-as-deep-as-pocket a corner patch where two rim rounds
report-fillet-concave-arc-bar-top-into-cylinder-solver-residues SMLib status 1001: SmFilletNx1Corner cannot make the topology at the arc's end (fillet error 3, corner type 8)
report-fillet-corner-after-round-running-into-cylinder-solver-residues SMLib's check refuses the round: its corner's surface of revolution is close to singular at its boundary (SmSurface test 5)
report-fillet-curvature-three-edges-at-bar-corner SMLib's check refuses the open setback sheet of curvature rounds: a 2D B-spline curve cusp (SmBSplineCurve test 13)
report-fillet-edge-tangent-at-one-end-into-cylinder SMLib's check refuses the round that fades out at the edge's tangent end: internal poles, coincident control points, cusps
report-fillet-edge-tangent-at-one-end-into-cylinder SMLib refuses the round as larger than the faces beside its edges allow (SMLIB_ERR_NO_ROOM): it fades to nothing at the edge's tangent end, on the wall SMLib now gets back as its cylinder