diff --git a/crates/kernel/src/blend_corner.rs b/crates/kernel/src/blend_corner.rs
index 750b46a810..70b852595e 100644
--- a/crates/kernel/src/blend_corner.rs
+++ b/crates/kernel/src/blend_corner.rs
@@ -1,28 +1,11 @@
-//! Fillets with a radius per edge, and the corners where filleted edges meet.
-//!
-//! The edges are straight and convex, and every face at their end vertices is planar with three
-//! edges meeting at each such vertex, including reflex corners of pocket rims. The faces there are rebuilt around the
-//! blends, faces away from them are kept. At a vertex:
-//! - one filleted edge ends on the third face: the blend ends in that face's section of the
-//! cylinder (an exact arc or ellipse);
-//! - two filleted edges, the third sharp: the cylinders trim each other (rolling ball); the
-//! crease runs from where their contact lines cross to the third edge (equal radii) or to the
-//! smaller blend's contact line on the face that ends the larger one;
-//! - three filleted edges: each cylinder stops at a plane section through the points where its
-//! contact lines meet the others', and a corner patch spans the three sections (a sphere for
-//! equal radii, else a rational patch through the exact sections).
-//!
-//! A fillet that ends on an earlier round (of any radius) takes the round off
-//! ([`crate::delete_faces`]) and blends both edges together.
-
-use std::collections::{HashMap, HashSet};
+//! Fillets with a radius per edge. SMLib rounds every picked edge at its own radius in one
+//! blend, with the corners where the rounds meet: an end in the face beyond, two rounds trimming
+//! each other, or a corner patch where three meet.
use solidcraft_geom::Vec3;
-use truck_modeling::{self as mt, builder};
-use crate::blend::{plane_normal, too_large, unsupported, vtx};
-use crate::body::{Body, Solid, p3, v3};
-use crate::{KernelError, Result, guard};
+use crate::body::Body;
+use crate::{KernelError, Result};
/// Fillet each edge nearest a point with its own radius.
pub fn fillet_radii(body: &Body, edges: &[(Vec3, f64)]) -> Result
{
@@ -33,1225 +16,16 @@ pub fn fillet_radii(body: &Body, edges: &[(Vec3, f64)]) -> Result {
if edges.iter().any(|(p, r)| !(p.is_finite() && r.is_finite() && *r > 1e-6 && *r < 1e6)) {
return Err(KernelError::Invalid("fillet size must be positive".into()));
}
- if crate::blend_smlib::selected()? {
- let (points, radii): (Vec, Vec) = edges.iter().copied().unzip();
- return crate::blend_smlib::fillet_each(body, &points, &radii);
- }
- // Each step's result carries its history over `[body]`.
- let counts = crate::record::counts(&[body]);
- let mut cur = crate::boolean_record::healed(body)?;
- let mut sel = edges.to_vec();
- let mut first: Option = None;
- // Earlier rounds at the edges' ends come off and are blended again with the new edges.
- for _ in 0..4 {
- let err = match build(&cur, &sel, true) {
- Ok(b) => return Ok(crate::record::after(b, &cur, &counts)),
- Err(e) => e,
- };
- let rounds = guard("recognise earlier rounds", || rounds_at(&cur, &sel))?;
- if rounds.is_empty() {
- return Err(first.unwrap_or(err));
- }
- first.get_or_insert(err);
- let idx: Vec = rounds.iter().map(|r| r.0).collect();
- sel = guard("map picks across earlier rounds", || surviving_picks(&cur, &sel, &idx))?;
- cur = crate::record::after(crate::delete_faces(&cur, &idx)?, &cur, &counts);
- sel.extend(rounds.iter().map(|r| (r.1, r.2)));
- }
- Err(first.unwrap_or_else(|| unsupported("these fillets")))
-}
-
-/// Local equal-distance chamfers, including pocket rims where adjacent walls have
-/// different slopes. The planar strips trim each other at each selected corner.
-pub(crate) fn chamfer(body: &Body, edges: &[Vec3], distance: f64) -> Result {
- let cur = crate::boolean_record::healed(body)?;
- let selected: Vec<_> = edges.iter().map(|p| (*p, distance)).collect();
- Ok(crate::record::after(build(&cur, &selected, false)?, &cur, &crate::record::counts(&[body])))
-}
-
-/// A face of the shell being rebuilt, or a new face.
-#[derive(Clone, Copy, PartialEq)]
-enum Owner {
- Face(usize),
- Cyl(mt::EdgeID),
- Patch,
-}
-
-/// A filleted edge.
-struct Sel {
- r: f64,
- /// Setback: distance along each face from the edge to the contact line.
- s: f64,
- /// Middle weight of the cross-section arc, sin(α/2) for the faces' interior angle α.
- w: f64,
- fa: usize,
- fb: usize,
- na: Vec3,
- nb: Vec3,
- /// In-face directions away from the edge.
- ta: Vec3,
- tb: Vec3,
- p0: Vec3,
- d: Vec3,
- surf: mt::Surface,
-}
-
-impl Sel {
- fn t(&self, f: usize) -> Vec3 {
- if f == self.fa { self.ta } else { self.tb }
- }
- fn axis_point(&self) -> Vec3 {
- self.p0 + self.ta * self.s - self.na * self.r
- }
-}
-
-/// The edges at a vertex: id, the edge, unit direction away from the vertex.
-struct Around {
- edges: Vec<(mt::EdgeID, Vec3)>,
-}
-
-fn h(p: Vec3, w: f64) -> mt::Vector4 {
- mt::Vector4::new(p.x * w, p.y * w, p.z * w, w)
-}
-
-/// Rational quadratic from `a` to `b` with middle control point `m` of weight `w`.
-fn conic(a: Vec3, m: Vec3, b: Vec3, w: f64) -> mt::NurbsCurve {
- mt::NurbsCurve::new(mt::BSplineCurve::new(mt::KnotVec::bezier_knot(2), vec![h(a, 1.0), h(m, w), h(b, 1.0)]))
-}
-
-fn edge(a: &mt::Vertex, b: &mt::Vertex, c: mt::Curve) -> Result {
- mt::Edge::try_new(a, b, c).map_err(|e| KernelError::Failed(format!("corner edge: {e}")))
-}
-
-/// Point where the contact line at setback `s` (in-face direction `t`) crosses the line from `v`
-/// along `u`.
-fn hit(v: Vec3, u: Vec3, t: Vec3, s: f64) -> Result {
- let k = u.dot(t);
- if k.abs() < 1e-6 {
- return Err(unsupported("a fillet that runs along its neighbouring edge"));
- }
- Ok(v + u * (s / k))
-}
-
-/// Unit direction in which a (curved) edge leaves its end vertex `v`.
-fn leaving(e: &mt::Edge, v: &mt::Vertex) -> Option {
- use mt::{BoundedCurve, ParametricCurve};
- let c = e.curve();
- let (t0, t1) = c.range_tuple();
- let at_front = e.absolute_front() == v;
- let d = c.der(if at_front { t0 } else { t1 });
- let d = Vec3::new(d.x, d.y, d.z);
- (if at_front { d } else { d * -1.0 }).normalized()
-}
-
-/// Point where the contact line at setback `s` (in-face direction `t`) crosses a curved edge
-/// leaving `v`: the first along it from `v`.
-fn crossing(e: &mt::Edge, v: Vec3, t: Vec3, s: f64) -> Result {
- use mt::{BoundedCurve, ParametricCurve};
- let c = e.curve();
- let (t0, t1) = c.range_tuple();
- let from_front = crate::body::from_p3(e.absolute_front().point()).dist(v) <= crate::body::from_p3(e.absolute_back().point()).dist(v);
- let at = |f: f64| crate::body::from_p3(c.subs(if from_front { t0 + (t1 - t0) * f } else { t1 - (t1 - t0) * f }));
- let g = |f: f64| (at(f) - v).dot(t) - s;
- let steps = 256;
- let mut lo = 0.0;
- for i in 1..=steps {
- let hi = i as f64 / steps as f64;
- if g(hi) >= 0.0 {
- let (mut a, mut b) = (lo, hi);
- for _ in 0..60 {
- let m = (a + b) * 0.5;
- if g(m) >= 0.0 {
- b = m;
- } else {
- a = m;
- }
- }
- return Ok(at((a + b) * 0.5));
- }
- lo = hi;
- }
- Err(too_large("a fillet larger than the faces next to it"))
-}
-
-/// The cylinder's (a chamfer's plane's) section by a curved end face `g`, from `a` (on the
-/// contact line with in-face direction `ta`) to `b` (`tb`) round the edge's end `v` (its other
-/// end `far`): points of the blend's cross-sections on the face, interpolated; within tolerance
-/// of both.
-#[allow(clippy::too_many_arguments)]
-fn curved_section(
- sel: &Sel,
- g: &mt::Face,
- (a, ta): (Vec3, Vec3),
- (b, tb): (Vec3, Vec3),
- v: Vec3,
- far: Vec3,
- round: bool,
- size: f64,
-) -> Result {
- use mt::ParametricCurve;
- let tolerance = (size * 1e-5).clamp(1e-5, 0.001);
- let fail = || too_large("a fillet larger than the faces next to it");
- // Position along the edge, and the cross-section there (fraction 0 on `ta`'s side).
- let along = |x: Vec3| (x - sel.p0).dot(sel.d);
- let point = |f: f64, l: f64| -> Vec3 {
- let apex = sel.p0 + sel.d * l;
- let (pa, pb) = (apex + ta * sel.s, apex + tb * sel.s);
- if round { crate::body::from_p3(conic(pa, apex, pb, sel.w).subs(f)) } else { pa + (pb - pa) * f }
- };
- // The face's surface, each search started from the last one's answer (the points come in
- // small steps).
- let surface = g.oriented_surface();
- let last = std::cell::Cell::new(None::<(f64, f64)>);
- let side = |x: Vec3| -> f64 {
- use mt::{ParametricSurface, ParametricSurface3D, SearchNearestParameter};
- let found = last
- .get()
- .and_then(|hint| surface.search_nearest_parameter(p3(x), Some(hint), 20))
- .or_else(|| surface.search_nearest_parameter(p3(x), None, 100));
- found.map_or(f64::NAN, |(u, w)| {
- last.set(Some((u, w)));
- let n = surface.normal(u, w);
- (x - crate::body::from_p3(surface.subs(u, w))).dot(Vec3::new(n.x, n.y, n.z))
- })
- };
- let (la, lb) = (along(a), along(b));
- // Out of the material: toward the edge's end.
- let outward = if along(v) >= along(far) { 1.0 } else { -1.0 };
- let step = ((lb - la).abs().max(sel.s * 0.05)) / 16.0;
- let root = |f: f64| -> Option {
- let guess = la + (lb - la) * f;
- let gv = side(point(f, guess));
- if !gv.is_finite() {
- return None;
- }
- let dir = if gv < 0.0 { outward } else { -outward };
- let mut prev = (guess, gv);
- for i in 1..=4096 {
- let l = guess + dir * step * i as f64;
- let cur = (l, side(point(f, l)));
- if !cur.1.is_finite() {
- return None;
- }
- if (prev.1 < 0.0) != (cur.1 < 0.0) {
- let (mut x, mut y) = (prev, cur);
- for _ in 0..60 {
- let m = (x.0 + y.0) * 0.5;
- let gm = side(point(f, m));
- if (gm < 0.0) == (x.1 < 0.0) {
- x = (m, gm);
- } else {
- y = (m, gm);
- }
- }
- return Some((x.0 + y.0) * 0.5);
- }
- prev = cur;
- }
- None
- };
- // Closer together toward the ends, where the face may only touch the blend.
- let across = |t: f64| (1.0 - (std::f64::consts::PI * t).cos()) * 0.5;
- let mut m = 8;
- loop {
- let mut pts = vec![a];
- for i in 1..m {
- let f = across(i as f64 / m as f64);
- pts.push(point(f, root(f).ok_or_else(fail)?));
- }
- pts.push(b);
- let curve = crate::build::interpolate_cubic(&pts).ok_or_else(|| KernelError::Failed("fillet: end curve".into()))?;
- let accurate = (0..m).all(|i| {
- let t = (i as f64 + 0.5) / m as f64;
- let f = across(t);
- let x = crate::body::from_p3(curve.subs(t));
- root(f).is_some_and(|l| point(f, l).dist(x) <= tolerance) && side(x).abs() <= tolerance
- });
- if accurate {
- return Ok(mt::Curve::BSplineCurve(curve));
- }
- if m >= 128 {
- return Err(KernelError::Failed("fillet: could not fit the blend's end in a curved face".into()));
- }
- m *= 2;
- }
-}
-
-/// Point in a face where the contact lines of two edges from `v` (directions `u1`, `u2`, in-face
-/// directions `t1`, `t2`, setbacks `s1`, `s2`) cross.
-fn meet(v: Vec3, (u1, t1, s1): (Vec3, Vec3, f64), (u2, t2, s2): (Vec3, Vec3, f64)) -> Result {
- let (k1, k2) = (u1.dot(t2), u2.dot(t1));
- if k1.abs() < 1e-6 || k2.abs() < 1e-6 {
- return Err(unsupported("fillets of nearly parallel edges at a corner"));
- }
- Ok(v + u1 * (s2 / k1) + u2 * (s1 / k2))
-}
-
-/// Intersection of the three planes n·x = n·p.
-fn planes3(a: (Vec3, Vec3), b: (Vec3, Vec3), c: (Vec3, Vec3)) -> Result {
- let row = |n: Vec3| [n.x, n.y, n.z];
- crate::polyhedron::solve3_pub([row(a.0), row(b.0), row(c.0)], [a.0.dot(a.1), b.0.dot(b.1), c.0.dot(c.1)])
- .map(|x| Vec3::new(x[0], x[1], x[2]))
- .ok_or_else(|| unsupported("a corner whose planes do not meet in a point"))
-}
-
-/// Flip a surface so that its normal near `at` points along `outward`.
-fn orient(mut surf: mt::Surface, at: Vec3, outward: Vec3) -> mt::Surface {
- use mt::{ParametricSurface3D, SearchNearestParameter};
- if let Some((u, v)) = surf.search_nearest_parameter(p3(at), None, 100) {
- let n = surf.normal(u, v);
- if Vec3::new(n.x, n.y, n.z).dot(outward) < 0.0 {
- surf = mt::Invertible::inverse(&surf);
- }
- }
- surf
-}
-
-/// Edges from `from` to `to` through the pool edges of `owner`.
-fn connect(from: &mt::Vertex, to: &mt::Vertex, pool: &[(mt::Edge, Vec)], owner: Owner) -> Result> {
- let mut out = Vec::new();
- let mut used: HashSet = HashSet::new();
- let mut cur = from.clone();
- for _ in 0..=pool.len() {
- if &cur == to {
- return Ok(out);
- }
- let next = pool.iter().filter(|(e, o)| o.contains(&owner) && !used.contains(&e.id())).find_map(|(e, _)| {
- if e.front() == &cur {
- Some(e.clone())
- } else if e.back() == &cur {
- Some(e.inverse())
- } else {
- None
- }
- });
- let Some(e) = next else { break };
- used.insert(e.id());
- cur = e.back().clone();
- out.push(e);
- }
- Err(KernelError::Failed("fillet corner: a boundary does not close".into()))
-}
-
-fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result {
- let size = cur.size();
- let tol = (size * 2e-3).max(1e-3);
- let solid = cur.deep_copy();
- let all = Body::unique_edges(&solid);
- let mut radius: HashMap = HashMap::new();
- let mut order: Vec = Vec::new();
- for (p, r) in sel_pts {
- let Some((idx, dist)) = cur.nearest_edge(*p, tol)? else { return Err(KernelError::Invalid("the body has no edges".into())) };
- if dist > size * 0.05 + 1e-3 {
- return Err(KernelError::Invalid(format!("no edge near {:?}", [p.x, p.y, p.z])));
- }
- let e = all.get(idx).ok_or_else(|| KernelError::Invalid("edge index".into()))?;
- if radius.insert(e.id(), *r).is_none() {
- order.push(e.id());
- }
- }
- let shells = solid.boundaries();
- let Some(first) = order.first() else { return Err(KernelError::Invalid("no edges selected".into())) };
- let si = shells.iter().position(|sh| sh.edge_iter().any(|e| e.id() == *first)).ok_or_else(|| KernelError::Failed("edge not found".into()))?;
- let shell = shells.get(si).ok_or_else(|| KernelError::Failed("shell".into()))?;
- if order.iter().any(|id| !shell.edge_iter().any(|e| e.id() == *id)) {
- return Err(unsupported("fillets on separate lumps in one go"));
- }
- let faces: Vec = shell.face_iter().cloned().collect();
- let edges: HashMap = shell.edge_iter().map(|e| (e.id(), e)).collect();
- // Faces on each edge, and the faces and edges at each vertex.
- let mut on_edge: HashMap> = HashMap::new();
- let mut at_vertex: HashMap, Vec)> = HashMap::new();
- for (fi, f) in faces.iter().enumerate() {
- for e in f.edge_iter() {
- let ids = on_edge.entry(e.id()).or_default();
- if !ids.contains(&fi) {
- ids.push(fi);
- }
- for v in [e.front(), e.back()] {
- let a = at_vertex.entry(v.id()).or_default();
- a.0.insert(e.id());
- if !a.1.contains(&fi) {
- a.1.push(fi);
- }
- }
- }
- }
- let planes: Vec