mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
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:
@@ -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} 起可以构建。请尝试更长的后退距离。"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user