Blends: a smooth edge and an unfinished tangent chain are refused saying why; SMLib's unsupported corner is named

A smooth edge (its faces meet tangentially) has nothing to round, and an edge
that runs on tangentially into one left out has no end SMLib can blend (it
rounds a tangent chain as one sequence and failed building the face there,
status 1001): both are refused before SMLib with that reason. SMLib's new
SMLIB_ERR_CORNER_UNSUPPORTED (a vertex no blend corner handles) is
Error::CornerUnsupported and says so instead of a bare status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 18:29:10 -04:00
committed by Irakli Kourtis
parent ef93b18eda
commit 229739a704
4 changed files with 110 additions and 26 deletions
+76 -26
View File
@@ -627,20 +627,58 @@ pub fn chamfer_sides(body: &Body, edges: &[Vec3], d1: f64, side: ChamferSide, fl
/// a round cannot pass from one side of the faces to the other, so it ends at that vertex.
/// Picks that find no edge are refused.
pub fn tangent_chain(body: &Body, picks: &[Vec3]) -> Result<Vec<Vec3>> {
/// Edges meeting at an angle below this continue one another.
const TANGENT_DEGREES: f64 = 0.5;
if picks.is_empty() || picks.len() > 1000 || picks.iter().any(|p| !p.is_finite()) {
return Err(KernelError::Invalid("select 1…1000 edges by finite points".into()));
}
let rec = Faces::of(body)?;
let (seeds, hidden) = crate::blend_smlib::picked_edges(body, picks)?;
let mut chain: Vec<usize> = Vec::new();
let mut picked: Vec<usize> = Vec::new();
for e in seeds {
if !chain.contains(&e) {
chain.push(e);
if !picked.contains(&e) {
picked.push(e);
}
}
let picked = chain.len();
let chain = chain_of(&rec, &picked, &hidden);
let mut out = picks.to_vec();
for e in chain.into_iter().skip(picked.len()) {
out.push(edge_middle(&rec, e).ok_or_else(|| KernelError::Failed("a point on a chain edge".into()))?);
}
Ok(out)
}
/// Edges meeting at an angle below this continue one another; faces meeting at an angle below
/// it meet tangentially.
const TANGENT_DEGREES: f64 = 0.5;
/// The point halfway along edge `e`.
fn edge_middle(rec: &Faces, e: usize) -> Option<Vec3> {
let edge = rec.rec.edges.get(e)?;
CurveEval::new(edge.parameters()).ok().and_then(|c| c.point(0.5 * (edge.interval[0] + edge.interval[1])))
}
/// How far from a face's surface a point may lie for its normal there.
fn normal_reach(rec: &Faces) -> f64 {
rec.rec.tolerance.max(1e-7) * 10.0
}
/// Whether edge `e`'s two faces meet tangentially (within [`TANGENT_DEGREES`]) at its start: a
/// smooth edge, with no corner to blend.
fn smooth_join(rec: &Faces, e: usize) -> bool {
let cos = TANGENT_DEGREES.to_radians().cos();
let accept = normal_reach(rec);
let faces: Vec<u32> = rec.faces_of(e).into_iter().collect();
let (Some(&a), Some(&b), Some((p, _))) = (faces.first(), faces.get(1), rec.ends(e)) else { return false };
match (rec.normal_at(a as usize, p, accept), rec.normal_at(b as usize, p, accept)) {
(Some(na), Some(nb)) => faces.len() == 2 && na.dot(nb) > cos,
_ => false,
}
}
/// `seeds` (edges in the record's order, each once), then every other edge that continues one
/// of them tangentially, as [`tangent_chain`] follows them; `hidden` marks the edges a pick
/// passes over.
fn chain_of(rec: &Faces, seeds: &[usize], hidden: &[bool]) -> Vec<usize> {
let mut chain = seeds.to_vec();
// The unit direction edge `e` leaves its end `v` in.
let leaving = |e: usize, v: u32| -> Option<Vec3> {
let [a, b] = rec.rec.edges.get(e)?.vertices;
@@ -650,16 +688,7 @@ pub fn tangent_chain(body: &Body, picks: &[Vec3]) -> Result<Vec<Vec3>> {
if a == v { rec.tangent(e, false) } else { rec.tangent(e, true).map(|t| t * -1.0) }
};
let cos = TANGENT_DEGREES.to_radians().cos();
let accept = rec.rec.tolerance.max(1e-7) * 10.0;
// Whether edge `e`'s two faces meet tangentially at its start.
let smooth = |e: usize| -> bool {
let faces: Vec<u32> = rec.faces_of(e).into_iter().collect();
let (Some(&a), Some(&b), Some((p, _))) = (faces.first(), faces.get(1), rec.ends(e)) else { return false };
match (rec.normal_at(a as usize, p, accept), rec.normal_at(b as usize, p, accept)) {
(Some(na), Some(nb)) => faces.len() == 2 && na.dot(nb) > cos,
_ => false,
}
};
let accept = normal_reach(rec);
// Whether edge `e` is convex at its end `v` (its faces meet at less than 180° inside the
// body): the direction into its second face, square to it, points against its first face's
// outward normal. A face lies to the left of its loop seen from its surface's natural normal.
@@ -686,7 +715,7 @@ pub fn tangent_chain(body: &Body, picks: &[Vec3]) -> Result<Vec<Vec3>> {
let Some(d) = leaving(e, v) else { continue };
for (f, other) in rec.rec.edges.iter().enumerate() {
let passed_over = hidden.get(f).copied().unwrap_or(false);
if passed_over || chain.contains(&f) || other.uses.len() < 2 || !other.vertices.contains(&v) || smooth(f) {
if passed_over || chain.contains(&f) || other.uses.len() < 2 || !other.vertices.contains(&v) || smooth_join(rec, f) {
continue;
}
let turns_over = matches!((convex(e, v), convex(f, v)), (Some(a), Some(b)) if a != b);
@@ -696,16 +725,37 @@ pub fn tangent_chain(body: &Body, picks: &[Vec3]) -> Result<Vec<Vec3>> {
}
}
}
let mut out = picks.to_vec();
for e in chain.into_iter().skip(picked) {
let edge = rec.rec.edges.get(e).ok_or_else(|| KernelError::Failed("a chain edge".into()))?;
let mid = CurveEval::new(edge.parameters())
.ok()
.and_then(|c| c.point(0.5 * (edge.interval[0] + edge.interval[1])))
.ok_or_else(|| KernelError::Failed("a point on a chain edge".into()))?;
out.push(mid);
chain
}
/// Refuses blending exactly `edges` (the record's edges `picks` took, in order) where SMLib has
/// no blend to build: an edge whose faces meet tangentially (a smooth edge, nothing to round),
/// and an edge that runs on tangentially into an edge left out (SMLib rounds a tangent chain
/// as one sequence; it has no end for a blend at a vertex where its edge continues smoothly,
/// and fails building the blend's face there). [`tangent_chain`] gives the whole chain.
pub(crate) fn refuse_unblendable(body: &Body, picks: &[Vec3], edges: &[usize], hidden: &[bool]) -> Result<()> {
let rec = Faces::of(body)?;
let at = |i: usize| picks.get(i).map(|p| format!("({:.3}, {:.3}, {:.3})", p.x, p.y, p.z)).unwrap_or_default();
for (i, &e) in edges.iter().enumerate() {
if smooth_join(&rec, e) {
return Err(KernelError::Invalid(format!("the edge at {} is smooth: its faces meet tangentially, so there is nothing to round", at(i))));
}
}
Ok(out)
let mut seeds: Vec<usize> = Vec::new();
for &e in edges {
if !seeds.contains(&e) {
seeds.push(e);
}
}
let chain = chain_of(&rec, &seeds, hidden);
if let Some(&next) = chain.get(seeds.len()) {
let p = edge_middle(&rec, next).map(|p| format!(" ({:.3}, {:.3}, {:.3})", p.x, p.y, p.z)).unwrap_or_default();
return Err(KernelError::Invalid(format!(
"a picked edge continues tangentially into the edge at{p}, which is not blended with it: a blend cannot end where its \
edge runs on smoothly, so blend its tangent chain too (tangent_chain: true)"
)));
}
Ok(())
}
#[cfg(test)]
+29
View File
@@ -191,6 +191,7 @@ mod imp {
body.require_brep(what)?;
sizes_ok(what, sizes)?;
let picked = resolve(body, edges)?;
crate::blend::refuse_unblendable(body, edges, &picked.edges, &picked.hidden)?;
let kind = match kind {
Kind::Round => Blend::Round,
Kind::Curvature => Blend::Curvature,
@@ -214,6 +215,7 @@ mod imp {
return Err(KernelError::Invalid(format!("{what}: each corner is a finite point with a positive, finite setback")));
}
let picked = resolve(body, edges)?;
crate::blend::refuse_unblendable(body, edges, &picked.edges, &picked.hidden)?;
let corners = corner_vertices(body, setbacks.iter().map(|(p, _)| *p))?;
let distances: Vec<f64> = setbacks.iter().map(|(_, d)| *d).collect();
let tol = pick_tolerance(body);
@@ -234,6 +236,7 @@ mod imp {
return Err(KernelError::Invalid(format!("{what}: each corner is a finite point with a positive, finite setback")));
}
let picked = resolve(body, edges)?;
crate::blend::refuse_unblendable(body, edges, &picked.edges, &picked.hidden)?;
let corners = corner_vertices(body, setbacks.iter().map(|(p, _)| *p))?;
let distances: Vec<f64> = setbacks.iter().map(|(_, d)| *d).collect();
let at: Vec<Vec3> = setbacks.iter().map(|(p, _)| *p).collect();
@@ -281,6 +284,7 @@ mod imp {
body.require_brep("chamfer")?;
sizes_ok("chamfer", &vec![d1; edges.len()])?;
let picked = resolve(body, edges)?;
crate::blend::refuse_unblendable(body, edges, &picked.edges, &picked.hidden)?;
let second = match side {
crate::ChamferSide::Distance(d) => ChamferSecond::Distance(d),
crate::ChamferSide::Angle(a) => ChamferSecond::Angle(a),
@@ -296,6 +300,7 @@ mod imp {
return Err(KernelError::Invalid("the start point must be finite".into()));
}
let picked = resolve(body, &[edge_at])?;
crate::blend::refuse_unblendable(body, &[edge_at], &picked.edges, &picked.hidden)?;
let [at] = picked.points[..] else { return Err(KernelError::Invalid("one edge".into())) };
let tol = pick_tolerance(body);
run(body, "variable fillet", &picked.edges, |b| {
@@ -334,6 +339,10 @@ mod imp {
None => failed(what, "operation", SmError::CornerRefused { corner }),
},
SmError::NoEffect => KernelError::Failed(format!("SMLib made no change: the {what} reported success but built no face")),
SmError::CornerUnsupported => KernelError::Invalid(format!(
"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")),
e => failed(what, "operation", e),
})?;
@@ -594,4 +603,24 @@ mod tests {
assert!(imp::blend(&b, "fillet", Kind::Round, &[Vec3::new(f64::NAN, 20.0, 20.0)], &[1.0]).is_err());
assert!(imp::blend(&b, "fillet", Kind::Round, &[], &[]).is_err());
}
/// A smooth edge (a round's seam with the face it runs into) has nothing to round, and an
/// edge that runs on tangentially into one left out has no end SMLib can blend: both are
/// refused before SMLib, saying so. Rounded with its tangent chain the edge builds.
#[test]
fn smooth_edges_and_unfinished_tangent_chains_are_refused_saying_why() {
let b = cube();
let rounded = fillet(&b, &[Vec3::new(10.0, 20.0, 20.0)], 3.0).unwrap();
// the seam where the r3 round meets the top, 3 in from the old edge
let seam = imp::blend(&rounded, "fillet", Kind::Round, &[Vec3::new(10.0, 17.0, 20.0)], &[1.0]).unwrap_err().to_string();
assert!(seam.contains("is smooth: its faces meet tangentially, so there is nothing to round"), "{seam}");
// the end face's top edge runs on tangentially into the round's end arc
let top = Vec3::new(0.0, 8.0, 20.0);
let alone = imp::blend(&rounded, "fillet", Kind::Round, &[top], &[1.0]).unwrap_err().to_string();
assert!(alone.contains("continues tangentially"), "{alone}");
let chain = crate::tangent_chain(&rounded, &[top]).unwrap();
assert!(chain.len() > 1, "{chain:?}");
let whole = fillet(&rounded, &chain, 1.0).unwrap();
assert!(volume(&whole) < volume(&rounded));
}
}
+1
View File
@@ -20,6 +20,7 @@ pub const ERR_NO_CUT: i32 = -1104;
pub const ERR_UNSUPPORTED: i32 = -1105;
pub const ERR_NO_EFFECT: i32 = -1107;
pub const ERR_WALLS_TOUCH: i32 = -1110;
pub const ERR_CORNER_UNSUPPORTED: i32 = -1111;
pub const BOOLEAN_UNION: i32 = 0;
pub const BOOLEAN_SUBTRACT: i32 = 1;
+4
View File
@@ -58,6 +58,8 @@ pub enum Error {
WallsTouch,
/// SMLib cannot build this exactly yet (a variable radius whose law has a corner).
Unsupported,
/// A blend's edges meet at a vertex of a kind no SMLib blend corner handles yet.
CornerUnsupported,
/// SMLib does not set back the corner of corner point `corner`: SMLib sets back a corner
/// only where every edge meeting it is blended and they are all convex or all concave.
CornerRefused { corner: usize },
@@ -92,6 +94,7 @@ impl std::fmt::Display for Error {
Error::NoRoom => write!(f, "there is no room for it: an offset leaves none inside the body, or a blend is larger than its faces allow"),
Error::WallsTouch => write!(f, "the offset's walls would meet along a line, which leaves no solid there"),
Error::Unsupported => write!(f, "not supported yet: SMLib cannot build this exactly"),
Error::CornerUnsupported => write!(f, "not supported yet: a corner of this kind is not supported yet by SMLib's blends"),
Error::CornerRefused { corner } => write!(
f,
"not supported yet: SMLib does not set back corner {}: it sets back a corner only where every edge meeting it is blended, all convex or all concave",
@@ -129,6 +132,7 @@ fn error(s: i32) -> Error {
ffi::ops::ERR_NO_ROOM => Error::NoRoom,
ffi::ops::ERR_WALLS_TOUCH => Error::WallsTouch,
ffi::ops::ERR_UNSUPPORTED => Error::Unsupported,
ffi::ops::ERR_CORNER_UNSUPPORTED => Error::CornerUnsupported,
ffi::ops::ERR_NO_EFFECT => Error::NoEffect,
ffi::trim::ERR_NOT_EXACT => Error::NotExact,
ffi::ERR_EXCEPTION => Error::Exception,