Class-A surfacing 3/3: blend curves, curved-edge fillets, the G2 corner and continuity analysis (#1642)

Thanks @midasdf.
This commit is contained in:
midasdf authored and GitHub committed 2026-10-16 06:26:27 -04:00
1 parent 3126962cd4
commit bbd4f29ebb
43 files changed
+4753 -132

No files matched your search

+71 -16
View File
@@ -205,19 +205,35 @@ pub fn fillet_variable_points(body: &Body, edge_at: Vec3, start: Vec3, points: &
}
/// Fillet one straight edge with the radius `spline` along it, its positions from the end
/// nearest `start`. One end's radius may be zero.
/// nearest `start`. One end's radius may be zero. A curved edge, or one between curved faces, is
/// rolled by a ball of the law's radius instead ([`curved_variable`]).
pub fn fillet_variable_spline(body: &Body, edge_at: Vec3, start: Vec3, spline: &RadiusSpline) -> Result<Body> {
body.require_brep("fillet")?;
spline.check()?;
let edge = crate::surfaces::edge_near(body, edge_at).ok_or_else(|| KernelError::Invalid(format!("no edge at {edge_at:?}")))?;
guard("variable fillet", || {
let s = EdgeSetup::new(body, &edge)?;
let Ok(s) = EdgeSetup::new(body, &edge) else {
return curved_variable(body, &edge, start, spline);
};
// Positions from the end nearest `start` (the edge keeps its own direction).
let law = if s.p1.dist(start) < s.p0.dist(start) { spline.reversed() } else { spline.clone() };
build(body, &s, &edge, &law)
})
}
/// The radius law along a curved edge: a ball whose radius is the law's at each station rolls
/// along the edge's smooth chain (the same sections, contacts and trims as the straight-edge case,
/// sized by the law there). A law that has run to zero at an end closes the blend to the edge's
/// end point, as the straight-edge fillet does.
fn curved_variable(body: &Body, edge: &mt::Edge, start: Vec3, law: &RadiusSpline) -> Result<Body> {
// Positions from the end nearest `start` (the chain runs along the edge's own direction).
let (p0, p1) = (vtx(edge.absolute_front()), vtx(edge.absolute_back()));
let law = if p1.dist(start) < p0.dist(start) { law.reversed() } else { law.clone() };
let chain = crate::blend::smooth_chain(&body.solid, edge).unwrap_or_else(|| vec![edge.id()]);
let (made, rec) = guard("variable fillet", || crate::curveblend::curve_blend_law(&body.solid, &chain, body.size(), &law, true))?;
crate::record::rebuilt(made, &[&body.solid], &rec)
}
/// A straight edge between two planar faces as the variable blend sees it: the shell it is in
/// and that shell's faces, the frame at the edge, and its ends (front first).
struct EdgeSetup<'a> {
@@ -570,8 +586,11 @@ pub fn fillet_tapered(body: &Body, edges: &[Vec3], radius: f64) -> Result<(Body,
Ok((cur, tapers))
}
/// Chord-length fillet: each edge (straight, between planar faces) rounded so that the blend
/// is `chord` wide across; edges with the same face angle share a radius.
/// Chord-length fillet: each edge rounded so that the blend is `chord` wide across, from contact
/// to contact at every station. A straight edge between planar faces is built exactly, the radius
/// `chord / (2 cos(φ/2))` from its face angle and edges sharing that angle blended together; a
/// curved edge, or one between curved faces (or a chain of selected edges running on into one),
/// is rolled by a ball whose radius gives the same width, which follows the face angle along it.
pub fn fillet_chord(body: &Body, edges: &[Vec3], chord: f64) -> Result<Body> {
body.require_brep("fillet")?;
if !(chord.is_finite() && chord > 1e-6 && chord < 1e6) {
@@ -580,19 +599,44 @@ pub fn fillet_chord(body: &Body, edges: &[Vec3], chord: f64) -> Result<Body> {
if edges.is_empty() || edges.len() > 1000 {
return Err(KernelError::Invalid("select 1…1000 edges".into()));
}
// Radius per edge from its face angle.
let mut groups: Vec<(f64, Vec<Vec3>)> = Vec::new();
for p in edges {
let e = crate::surfaces::edge_near(body, *p).ok_or_else(|| KernelError::Invalid(format!("no edge at {p:?}")))?;
let shell =
body.solid.boundaries().iter().find(|sh| sh.edge_iter().any(|x| x.id() == e.id())).ok_or_else(|| KernelError::Failed("edge".into()))?;
let faces: Vec<&mt::Face> = shell.face_iter().collect();
let fr = edge_frame(&faces, &e)?;
let r = chord_radius(chord, fr.phi);
match groups.iter_mut().find(|(gr, _)| (gr - r).abs() <= 1e-9 * r) {
Some((_, v)) => v.push(*p),
None => groups.push((r, vec![*p])),
let found: Vec<mt::Edge> = edges
.iter()
.map(|p| crate::surfaces::edge_near(body, *p).ok_or_else(|| KernelError::Invalid(format!("no edge at {p:?}"))))
.collect::<Result<_>>()?;
// The exact radius of a straight edge between planar faces; None where the rolling ball has to
// take the edge (a curved edge, or one between curved faces).
let exact_radius = |e: &mt::Edge| -> Option<f64> {
if !matches!(e.curve(), mt::Curve::Line(_)) {
return None;
}
let shell = body.solid.boundaries().iter().find(|sh| sh.edge_iter().any(|x| x.id() == e.id()))?;
let faces: Vec<&mt::Face> = shell.face_iter().collect();
Some(chord_radius(chord, edge_frame(&faces, e).ok()?.phi))
};
// A straight edge running on smoothly into a selected curved one goes with it: the ball rolls
// along the chain, so blending the parts apart would leave a step at their joint.
let meets = |a: &mt::Edge, b: &mt::Edge| [a.front(), a.back()].into_iter().any(|v| v == b.front() || v == b.back());
let mixed = |e: &mt::Edge| found.iter().any(|f| f.id() != e.id() && exact_radius(f).is_none() && meets(e, f));
// Radius per edge from its face angle; the rest roll a ball along their chain.
let mut groups: Vec<(f64, Vec<Vec3>)> = Vec::new();
let mut rolled: Vec<Vec<Vec3>> = Vec::new();
let mut done: Vec<mt::EdgeID> = Vec::new();
for (p, e) in edges.iter().zip(&found) {
if let Some(r) = exact_radius(e).filter(|_| !mixed(e)) {
match groups.iter_mut().find(|(gr, _)| (gr - r).abs() <= 1e-9 * r) {
Some((_, v)) => v.push(*p),
None => groups.push((r, vec![*p])),
}
continue;
}
if done.contains(&e.id()) {
continue;
}
// The whole smooth chain (as the constant-radius path rolls it): a ball cannot stop
// where the edge runs on, so the chain's other edges are blended with it.
let chain = crate::blend::smooth_chain(&body.solid, e).unwrap_or_else(|| vec![e.id()]);
done.extend(chain.iter().copied());
rolled.push(edges.iter().zip(&found).filter(|(_, f)| chain.contains(&f.id())).map(|(p, _)| *p).collect());
}
// Each step's result carries its history over `[body]`.
let counts = crate::record::counts(&[body]);
@@ -600,6 +644,17 @@ pub fn fillet_chord(body: &Body, edges: &[Vec3], chord: f64) -> Result<Body> {
for (r, pts) in groups {
cur = crate::record::after(crate::fillet(&cur, &pts, r)?, &cur, &counts);
}
for picks in rolled {
// The chain as the current body has it: the blends before it rebuilt the faces it runs
// through, and a rebuilt face carries its edges again.
let edges: Vec<mt::Edge> = picks.iter().filter_map(|p| crate::surfaces::edge_near(&cur, *p)).collect();
let Some(first) = edges.first() else { continue };
let keep = |x: &mt::Edge| edges.iter().any(|e| e.id() == x.id());
let chain = crate::blend::smooth_chain_within(&cur.solid, first, &keep).unwrap_or_else(|| vec![first.id()]);
let (made, rec) = crate::guard("chord fillet", || crate::curveblend::curve_blend_chord(&cur.solid, &chain, cur.size(), chord, true))?;
let step = crate::record::rebuilt(made, &[&cur.solid], &rec)?;
cur = crate::record::after(step, &cur, &counts);
}
Ok(cur)
}