Fillet: a setback too short for its corner is refused naming the least that builds (smlib_blend_fit_size)

SMLib's setback corner takes a G1 fill only once certified convex, and refuses a setback too
short for that as SMLIB_ERR_NO_ROOM with SMLIB_FIT_SETBACK_AT_LEAST, the smallest setback that
builds. The kernel reads it (solidcraft-smlib binds smlib_blend_fit_size) and names it; the
Fillet dialog says it in plain words. Other room refusals keep their words. The r5 box corner
set back 5.5 is refused naming 5.962, which builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
jyviko
2026-11-01 21:52:54 -05:00
committed by jyviko
parent 17de3569fd
commit 5136898b9d
10 changed files with 132 additions and 12 deletions
@@ -0,0 +1,23 @@
{
"en": {
"ui.fillet_error_setback_short": "The setback is too short for this corner's patch: it builds from {least} on. Try a longer Setback."
},
"ar": {
"ui.fillet_error_setback_short": "مسافة الإرجاع قصيرة جدًا على رقعة هذه الزاوية: تُبنى ابتداءً من {least}. جرّب مسافة إرجاع أطول."
},
"de": {
"ui.fillet_error_setback_short": "Der Rücksprung ist für das Eckstück zu kurz: Es lässt sich ab {least} bauen. Versuchen Sie einen längeren Rücksprung."
},
"he": {
"ui.fillet_error_setback_short": "הנסיגה קצרה מדי עבור הטלאי של פינה זו: הוא נבנה החל מ-{least}. נסו נסיגה ארוכה יותר."
},
"ja": {
"ui.fillet_error_setback_short": "このコーナーのパッチにはセットバックが短すぎます。{least} 以上で作れます。より長いセットバックを試してください。"
},
"ko": {
"ui.fillet_error_setback_short": "이 코너의 패치에 비해 셋백이 너무 짧습니다. {least}부터 만들 수 있습니다. 더 긴 셋백을 시도해 보세요."
},
"zh": {
"ui.fillet_error_setback_short": "对于此角点的面片,后退距离太短:从 {least} 起可以构建。请尝试更长的后退距离。"
}
}
+4
View File
@@ -224,6 +224,10 @@ pub fn fillet_removing(body: &Body, edges: &[Vec3], radius: f64) -> Result<(Body
Ok((closed(body, b)?, gone))
}
/// What a refusal of a setback too short for its corner says before the least that builds (in
/// mm): SMLib certifies the corner's fill convex only from that setback on ([`fillet_setback`]).
pub const SETBACK_AT_LEAST: &str = "the smallest setback that builds is ";
/// [`fillet`] with corner setbacks: each `(at, distance)` sets back the corner at the vertex
/// nearest `at`, a vertex where every edge is rounded, so its patch starts `distance` along
/// each of its edges from the vertex (one distance for all of a corner's edges: SMLib keeps one
+9 -1
View File
@@ -418,7 +418,15 @@ mod imp {
"not supported yet: the {what} meets a corner of this kind, which SMLib's blends do not handle yet (where its edges \
run on tangentially into each other, or meet in a way no blend corner builds)"
)),
SmError::NoRoom => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")),
// A setback too short for its corner's fill (SMLib certifies it convex only from
// its least on) names that least; any other is a blend too large for its room.
SmError::NoRoom => match solidcraft_smlib::blend_fit() {
Some(solidcraft_smlib::Fit::SetbackAtLeast(least)) => KernelError::Invalid(format!(
"the setback is too short for its corner: {}{least:.4} mm (the corner's patch is convex only from there)",
crate::SETBACK_AT_LEAST
)),
_ => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")),
},
e => failed(what, "operation", e),
})?;
// Checked here unless the evaluation's validation mode leaves it to the feature's end
+14 -3
View File
@@ -44,8 +44,10 @@ fn a_setback_off_the_rounded_corner_is_refused() {
}
/// A setback that does not reach past where the corner's rounds meet without one (on faces square
/// to each other, the radius) is refused before SMLib runs, naming that least setback; a setback
/// just past it builds.
/// to each other, the radius) is refused before SMLib runs, naming that least setback. Past it, a
/// setback still too short for SMLib's corner fill (certified convex only from a setback on,
/// smlib_blend_fit_size's SMLIB_FIT_SETBACK_AT_LEAST) is refused naming the smallest that builds,
/// at most twice the one asked for (the certificate scales the setbacks up to 2); that one builds.
#[test]
fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
for s in [2.0, 5.0] {
@@ -54,7 +56,16 @@ fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
other => panic!("setback {s}: {:?}", other.map(|b| b.face_count())),
}
}
let out = corner_case(Some(5.5)).unwrap();
let least = match corner_case(Some(5.5)) {
Err(KernelError::Invalid(why)) => {
let named = why.split_once(SETBACK_AT_LEAST).and_then(|(_, rest)| rest.split_once(" mm")).and_then(|(n, _)| n.parse::<f64>().ok());
named.unwrap_or_else(|| panic!("no least setback named: {why}"))
}
other => panic!("setback 5.5: {:?}", other.map(|b| b.face_count())),
};
eprintln!("setback 5.5 refused: the smallest that builds is {least}");
assert!(least > 5.5 && least <= 11.0, "{least}");
let out = corner_case(Some(least)).unwrap();
assert!(out.validity().is_empty(), "{:?}", out.validity());
// The least the refusal names, for a form to show before it runs; none at no vertex.
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
+2 -2
View File
@@ -156,8 +156,8 @@ mod validity;
pub mod watch;
pub use blend::{
ChamferSide, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_g2_with_g2_corners,
fillet_least_setback, fillet_max, fillet_max_each, fillet_removing, fillet_setback, tangent_chain,
ChamferSide, SETBACK_AT_LEAST, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners,
fillet_g2_with_g2_corners, fillet_least_setback, fillet_max, fillet_max_each, fillet_removing, fillet_setback, tangent_chain,
};
pub use blend_corner::fillet_radii;
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
+20 -1
View File
@@ -1,5 +1,5 @@
//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_boolean_union_all`, `smlib_boolean2d`, `smlib_fillet`,
//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_dropping`, `smlib_blend_setback`, `smlib_blend_setback_open`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_dropping`, `smlib_blend_setback`, `smlib_blend_setback_open`, `smlib_blend_fit_size`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
//! `smlib_thicken`, `smlib_draft`, `smlib_merge_faces`, `smlib_brep_solid_count`, `smlib_imprint`; the fork's `capi/src/smlib_ops.cpp`), declared by
//! hand. Part of the FFI boundary, like its parent: called only through
//! [`crate::ops`].
@@ -31,6 +31,11 @@ pub const BLEND_CURVATURE: i32 = 1;
pub const BLEND_CHORD: i32 = 2;
pub const BLEND_CHAMFER: i32 = 3;
pub const FIT_SIZE: i32 = 0;
pub const FIT_SETBACK: i32 = 1;
pub const FIT_SCALE: i32 = 2;
pub const FIT_SETBACK_AT_LEAST: i32 = 3;
/// Which selection operation: the C functions share one signature.
#[derive(Clone, Copy)]
pub enum Pick {
@@ -95,6 +100,7 @@ unsafe extern "C" {
out: *mut *mut RawBrep,
out_failed_point: *mut i64,
) -> i32;
fn smlib_blend_fit_size(out_size: *mut f64, out_what: *mut i32) -> i32;
fn smlib_blend_dropping(
brep: *const RawBrep,
kind: i32,
@@ -251,6 +257,19 @@ pub fn pick_op(kind: Pick, b: &Handle, points: &[[f64; 3]], tolerance: f64, size
})
}
/// `smlib_blend_fit_size`: the size that builds, and what it sizes (`FIT_*`), for the last blend
/// call SMLib refused as too large or a setback as too short; `None` where it names none.
#[cfg(feature = "native")]
pub fn blend_fit_size() -> Option<(f64, i32)> {
let (mut size, mut what) = (0.0f64, -1i32);
(unsafe { smlib_blend_fit_size(&mut size, &mut what) } == OK).then_some((size, what))
}
#[cfg(not(feature = "native"))]
pub fn blend_fit_size() -> Option<(f64, i32)> {
None
}
#[cfg(feature = "native")]
pub fn thicken(b: &Handle, thickness: f64, symmetric: bool) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_thicken(b.raw(), thickness, i32::from(symmetric), out) })
+1 -1
View File
@@ -32,7 +32,7 @@ pub mod trim;
pub use check::{CheckLevel, CheckReport};
pub use measure::{MEASURE_ACCURACY, MassProperties, PointClass};
pub use ops::{Blend, BooleanOp, ChamferSecond, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS};
pub use ops::{Blend, BooleanOp, ChamferSecond, Fit, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS, blend_fit};
/// Why an SMLib call failed.
#[derive(Clone, Debug, PartialEq)]
+29
View File
@@ -8,6 +8,35 @@
use crate::{Brep, Error, Result, error, ffi, init};
/// What size builds a blend SMLib refused ([`blend_fit`]): from the refusal's own cause, never by
/// building again.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Fit {
/// The largest size (a round's radius, a chamfer's distance, a chord's width) that builds.
Size(f64),
/// The largest setback of a set-back corner that fits (the one asked for is too long).
Setback(f64),
/// The factor (< 1) a two-distance chamfer's distances are both scaled by to build.
Scale(f64),
/// The smallest setback that builds the corner (the one asked for is too short: its fill is
/// certified convex only from there).
SetbackAtLeast(f64),
}
/// The size that builds the last blend this process's SMLib refused as too large
/// ([`Error::NoRoom`]) or whose setback it refused as too short; `None` where it names no single
/// size. Read it right after the refused call: SMLib keeps only the last one.
pub fn blend_fit() -> Option<Fit> {
let (size, what) = ffi::ops::blend_fit_size()?;
match what {
ffi::ops::FIT_SIZE => Some(Fit::Size(size)),
ffi::ops::FIT_SETBACK => Some(Fit::Setback(size)),
ffi::ops::FIT_SCALE => Some(Fit::Scale(size)),
ffi::ops::FIT_SETBACK_AT_LEAST => Some(Fit::SetbackAtLeast(size)),
_ => None,
}
}
/// How [`Brep::boolean`] combines two bodies.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BooleanOp {
+10 -4
View File
@@ -2665,7 +2665,7 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut
#[derive(Debug, PartialEq)]
pub(crate) enum FilletRefusal {
/// In these plain words.
Plain(&'static str),
Plain(String),
/// Not again: the Setback field already flags the value and names the least it takes.
InField,
}
@@ -2680,6 +2680,12 @@ pub(crate) fn fillet_plain_error(error: &str, setback_in_field: bool) -> Option<
if setback_in_field && error.contains("the setback must be more than") {
return Some(FilletRefusal::InField);
}
// A setback too short for its corner's patch: the least that builds, as the kernel names it
if let Some((_, rest)) = error.split_once(solidcraft_engine::kernel::SETBACK_AT_LEAST)
&& let Some(least) = rest.split_once(" mm").map(|(n, _)| n.trim()).filter(|n| n.parse::<f64>().is_ok())
{
return Some(FilletRefusal::Plain(crate::i18n::format("ui.fillet_error_setback_short", &[("least", &format!("{least} mm"))])));
}
let too_large = error.contains("larger than the faces beside its edges allow");
let corner = error.contains("setback") || error.contains("G2 corner") || error.contains("corner patch");
let kernel = error.contains("SMLib")
@@ -2688,11 +2694,11 @@ pub(crate) fn fillet_plain_error(error: &str, setback_in_field: bool) -> Option<
|| error.contains("kernel internal error")
|| error.contains("the rounds would leave openings");
if too_large {
Some(FilletRefusal::Plain(tr("ui.fillet_error_too_large")))
Some(FilletRefusal::Plain(tr("ui.fillet_error_too_large").to_string()))
} else if corner && kernel {
Some(FilletRefusal::Plain(tr("ui.fillet_error_corner")))
Some(FilletRefusal::Plain(tr("ui.fillet_error_corner").to_string()))
} else if kernel {
Some(FilletRefusal::Plain(tr("ui.fillet_error_failed")))
Some(FilletRefusal::Plain(tr("ui.fillet_error_failed").to_string()))
} else {
None
}
+20
View File
@@ -1416,6 +1416,26 @@ mod tests {
/// A smooth-patch fillet reopens with its corner and setback, and OK writes them as saved; a
/// setback list the dialog cannot show as one distance is kept as it is.
/// A setback too short for SMLib's corner fill (past the least the field flags) is refused by
/// the kernel naming the smallest that builds; the dialog says it in plain words, that length
/// in them.
#[test]
fn a_setback_too_short_for_the_corner_says_the_least_that_builds() {
let refused = format!(
"invalid parameters for `solid.fillet`: the setback is too short for its corner: {}5.9620 mm (the corner's patch is convex only from there)",
solidcraft_engine::kernel::SETBACK_AT_LEAST
);
match crate::dialogs::fillet_plain_error(&refused, true) {
Some(crate::dialogs::FilletRefusal::Plain(plain)) => assert!(plain.contains("5.9620 mm"), "{plain}"),
other => panic!("{other:?}"),
}
// A blend too large for its faces keeps its own words.
assert_eq!(
crate::dialogs::fillet_plain_error("the fillet is larger than the faces beside its edges allow", true),
Some(crate::dialogs::FilletRefusal::Plain(crate::i18n::tr("ui.fillet_error_too_large").to_string()))
);
}
#[test]
fn a_saved_smooth_patch_reopens_with_its_setback() {
for (setbacks, field) in [