mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
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:
+76
-26
@@ -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)]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user