feat: fillet fit option reduces a radius too large for its edges (#388)
* feat(kernel): typed TooLarge refusal and fillet_max search Blend refusals caused only by the size (larger than the neighbouring faces, taller than the walls, longer than the next edge, larger than a corner radius) now return KernelError::TooLarge, displayed as before. fillet_max finds the largest radius below a request at which the fillet of the edges works, to three significant digits, by building candidates; it is an explicit call (up to 16 fillets), not run on every refusal. A corner edge that doesn't run square to the edge is now reported as such instead of as a blend larger than the neighbouring faces. Refs #293 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44 * feat: fillet fit option reduces a radius too large for its edges solid.fillet takes fit: exact (the default, as before: a radius too large for the geometry around the edges is refused) or reduce: the radius is reduced to the largest that works for all the picked edges (kernel fillet_max, three significant digits, every candidate built). The feature stores the requested radius, so it refits on every evaluation; when it reduces, the feature's warning says so and the command returns radius_applied. Pattern copies of the fillet use the same rule. The file format writes fit only when it is reduce. The Fillet dialog gets a Radius Fit row (Exact / Reduce to Fit, constant radius) and, when the preview reduced the radius, an Applied Radius line; editing the feature keeps the choice. One radius for all edges for now; per-edge radii and taper follow. Refs #293 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
This commit is contained in:
14 files changed
+316
-23
No files matched your search
@@ -27,6 +27,11 @@ pub(crate) fn unsupported(msg: &str) -> KernelError {
|
||||
KernelError::Failed(format!("not supported yet: {msg}"))
|
||||
}
|
||||
|
||||
/// A blend too large for the faces around it.
|
||||
pub(crate) fn too_large(msg: &str) -> KernelError {
|
||||
KernelError::TooLarge(msg.into())
|
||||
}
|
||||
|
||||
pub(crate) fn vtx(v: &mt::Vertex) -> Vec3 {
|
||||
from_p3(v.point())
|
||||
}
|
||||
@@ -286,7 +291,7 @@ fn blend_geometry(
|
||||
// A rail ending before the old corner extends the neighbouring rim edge away from its
|
||||
// other vertex: valid only within the trimmed side face.
|
||||
if along >= len - tol || (along < 0.0 && !faces.get(fi).is_some_and(|f| extension_fits(f, p, q, tol))) {
|
||||
return Err(unsupported("the blend is larger than the neighbouring faces"));
|
||||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
}
|
||||
// Across two end faces: where the blend's cylinder crosses the edge between them, on the
|
||||
@@ -332,7 +337,7 @@ fn blend_geometry(
|
||||
return Err(unsupported("both ends touch the same face"));
|
||||
}
|
||||
if (end1.a - end0.a).dot(d) <= tol || (end1.b - end0.b).dot(d) <= tol {
|
||||
return Err(unsupported("the blend is larger than the neighbouring faces"));
|
||||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
// A side face whose corner edges are consumed at both ends is used up: its remaining edge,
|
||||
// parallel to the old edge at the setback, becomes the rail and the face is removed.
|
||||
@@ -616,7 +621,10 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
.collect();
|
||||
guard(what, || crate::curveblend::curve_blend(&solid, &ids, size, r, shape == Shape::Round))
|
||||
.map(|o| (o, if on_chain.is_empty() { vec![j] } else { on_chain }))
|
||||
.map_err(|e2| KernelError::Failed(format!("{e2} ({e})")))
|
||||
.map_err(|e2| match e2 {
|
||||
KernelError::TooLarge(m) => KernelError::TooLarge(format!("{m} ({e})")),
|
||||
e2 => KernelError::Failed(format!("{e2} ({e})")),
|
||||
})
|
||||
}
|
||||
None => Err(e),
|
||||
},
|
||||
@@ -893,7 +901,9 @@ fn loop_split(cur: &Body, pts: &[Vec3]) -> Result<Option<(Vec<Vec<Vec3>>, Vec<Ve
|
||||
Ok((!groups.is_empty()).then_some((groups, rest)))
|
||||
}
|
||||
|
||||
/// Constant-radius fillet of the edges nearest to the given points.
|
||||
/// Constant-radius fillet of the edges nearest to the given points. A radius too large for the
|
||||
/// faces around the edges is refused with [`KernelError::TooLarge`] ([`fillet_max`] finds the
|
||||
/// largest that works).
|
||||
pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
body.require_brep("fillet")?;
|
||||
// Corners the local blends refuse (an earlier round at an edge's end): radius per edge.
|
||||
@@ -903,6 +913,41 @@ pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The step of the radius search: three significant digits of the requested radius.
|
||||
fn radius_step(radius: f64) -> f64 {
|
||||
10f64.powf(radius.log10().floor() - 3.0)
|
||||
}
|
||||
|
||||
/// The largest radius below `radius` (to three significant digits) at which the fillet of
|
||||
/// these edges works, with its result; `None` when none does. Each candidate is a real fillet
|
||||
/// of the edges, so the radius returned is one that was built; the search runs up to 16 of
|
||||
/// them, so it is for an explicit request, not every refusal.
|
||||
pub fn fillet_max(body: &Body, edges: &[Vec3], radius: f64) -> Option<(f64, Body)> {
|
||||
if !(radius.is_finite() && radius > 1e-6) {
|
||||
return None;
|
||||
}
|
||||
let q = radius_step(radius);
|
||||
// Radii n·q for n in [0, hi): hi fails (it is the request, or above it), lo works.
|
||||
let mut hi = (radius / q).ceil() as u64;
|
||||
let mut lo = 0u64;
|
||||
let mut best: Option<Body> = None;
|
||||
// log2(10⁴) steps reach the step size; the cap keeps a slow body from stalling.
|
||||
for _ in 0..16 {
|
||||
if hi - lo <= 1 {
|
||||
break;
|
||||
}
|
||||
let mid = lo + (hi - lo) / 2;
|
||||
match fillet(body, edges, mid as f64 * q) {
|
||||
Ok(b) => {
|
||||
lo = mid;
|
||||
best = Some(b);
|
||||
}
|
||||
Err(_) => hi = mid,
|
||||
}
|
||||
}
|
||||
best.map(|b| (lo as f64 * q, b))
|
||||
}
|
||||
|
||||
/// Equal-distance chamfer of the edges nearest to the given points. Each edge is cut with a
|
||||
/// prism whose only face inside the body is the chamfer plane (so neighbouring chamfers meet in
|
||||
/// a mitre); the local operation is the fallback.
|
||||
|
||||
Reference in new issue
Block a user