mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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, .. } =
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user