Thanks @erkexzcx.
This commit is contained in:
1 parent
b20fd3d9d6
commit
5c0351b1f4
4 files changed
+106
-13
No files matched your search
@@ -316,6 +316,7 @@ mod loft_spacing;
|
||||
mod mirror_planes;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native_cad_import;
|
||||
mod nudge_slivers;
|
||||
mod path_scope;
|
||||
mod pleat_join;
|
||||
mod primitive_points;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! #1488: a loft joined into a slab, flush with the slab's outer face and its bottom, takes the
|
||||
//! boolean's last retry (the tool nudged a fraction of a micron). That leaves sliver faces
|
||||
//! along the flush faces, one of which reads as a face whose boundary crosses itself; every
|
||||
//! later round on the body was then refused for it ("the rounds would leave openings"), even
|
||||
//! one 36 mm away. A round is now refused only for crossed faces it makes.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::run;
|
||||
use crate::Session;
|
||||
|
||||
/// The issue's slab `width` thick with a window through it and the pleat loft joined into it.
|
||||
fn slab_with_pleat(width: f64) -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"corner": [-60, 0, 0], "length": 120, "width": width, "height": 95, "body_name": "Wall"}));
|
||||
run(
|
||||
&mut s,
|
||||
"solid.box",
|
||||
json!({"corner": [-24, -0.5, -1], "length": 48, "width": width + 1.0, "height": 91, "operation": "cut", "targets": ["Wall"]}),
|
||||
);
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Bottom"}));
|
||||
run(
|
||||
&mut s,
|
||||
"sketch.line",
|
||||
json!({"closed": true, "infer": false, "connect": false, "points": [
|
||||
[-26, 3], [-24.76384, 3], [-16.0568, 14.97217], [-8, 4.90117], [0, 14.90117], [8, 4.90117], [16.0568, 14.97217], [24.76384, 3], [26, 3],
|
||||
[26, 0], [23.23616, 0], [15.9432, 10.02783], [8, 0.09883], [0, 10.09883], [-8, 0.09883], [-15.9432, 10.02783], [-23.23616, 0], [-26, 0]]}),
|
||||
);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 90.5, "name": "Top"}));
|
||||
run(
|
||||
&mut s,
|
||||
"sketch.line",
|
||||
json!({"closed": true, "infer": false, "connect": false, "points": [
|
||||
[-26, 3], [-24, 3], [-16, 3], [-8, 3], [0, 3], [8, 3], [16, 3], [24, 3], [26, 3],
|
||||
[26, 0], [24, 0], [16, 0], [8, 0], [0, 0], [-8, 0], [-16, 0], [-24, 0], [-26, 0]]}),
|
||||
);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(
|
||||
&mut s,
|
||||
"solid.loft",
|
||||
json!({"sections": [{"sketch": "Bottom"}, {"sketch": "Top"}], "operation": "join", "targets": ["Wall"], "name": "Pleat"}),
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
fn wall(s: &Session) -> solidcraft_kernel::Body {
|
||||
s.model.state().body("Wall").map(|b| b.body.clone()).unwrap()
|
||||
}
|
||||
|
||||
/// The R1 round on the slab's end edge at x = −60 works after the join, as it does on the 3 mm
|
||||
/// slab (and on the 3.5 mm one before the loft), and adds no crossed face of its own.
|
||||
#[test]
|
||||
fn a_round_far_from_the_joined_loft_works() {
|
||||
for (width, pick) in [(3.5, [-60.0, 1.75, 95.0]), (3.0, [-60.0, 1.5, 95.0])] {
|
||||
let mut s = slab_with_pleat(width);
|
||||
let before = wall(&s);
|
||||
let r = s.execute("solid.fillet", &json!({"body": "Wall", "edges": [pick], "radius": 1}));
|
||||
assert!(r.is_ok(), "{width} mm: {r:?}");
|
||||
let after = wall(&s);
|
||||
assert_eq!(after.face_count(), before.face_count() + 1, "{width} mm: one round face");
|
||||
assert_eq!(after.crossed_faces().unwrap(), before.crossed_faces().unwrap(), "{width} mm");
|
||||
// The round lies along the picked edge: the end face's top corner is gone.
|
||||
let m = solidcraft_kernel::measure(&after).unwrap();
|
||||
assert!((m.bbox.min.x + 60.0).abs() < 1e-6 && (m.bbox.max.z - 95.0).abs() < 1e-6, "{width} mm: {:?}", m.bbox);
|
||||
}
|
||||
}
|
||||
|
||||
/// A round that does cross faces is still refused, joined loft or not (#871's check stands).
|
||||
#[test]
|
||||
fn rounds_that_overlap_are_still_refused() {
|
||||
let mut s = slab_with_pleat(3.5);
|
||||
// Both long top edges of the 3.5 mm slab with a radius over half its thickness.
|
||||
let r = s.execute("solid.fillet", &json!({"body": "Wall", "edges": [[-40, 0, 95], [-40, 3.5, 95]], "radius": 2}));
|
||||
assert!(r.is_err(), "{r:?}");
|
||||
}
|
||||
+17
-10
@@ -1121,7 +1121,7 @@ pub fn fillet_g2(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
let (made, rec) = guard("G2 fillet", || blend_one(&solid, &edge, size, radius, Shape::Curvature))?;
|
||||
cur = record::after(record::rebuilt(made, &[&solid], &rec)?, &cur, &counts);
|
||||
}
|
||||
closed(cur)
|
||||
closed(body, cur)
|
||||
}
|
||||
|
||||
/// Most edges whose other orders are tried when the order given is refused.
|
||||
@@ -1804,9 +1804,16 @@ fn loop_split(cur: &Body, pts: &[Vec3]) -> Result<Option<(Vec<Vec<Vec3>>, Vec<Ve
|
||||
/// A fillet's result, refused when a face's boundary crosses itself (#871): rounds that
|
||||
/// overlap where a wall is thinner than both leave such faces, which the topology checks
|
||||
/// don't see but which show as holes in the model. A fillet either closes the solid or is
|
||||
/// refused.
|
||||
fn closed(b: Body) -> Result<Body> {
|
||||
match b.crossed_face_loops()? {
|
||||
/// refused. Faces of `input` that crossed already and that the fillet carried over unchanged
|
||||
/// (a sliver an earlier boolean left far from the rounds, #1488) are not the fillet's doing.
|
||||
fn closed(input: &Body, b: Body) -> Result<Body> {
|
||||
let after = b.crossed_faces()?;
|
||||
if after.is_empty() {
|
||||
return Ok(b);
|
||||
}
|
||||
let before = input.crossed_faces()?;
|
||||
let tol = (input.size().max(b.size()) * 1e-6).max(1e-9);
|
||||
match after.iter().filter(|p| !before.iter().any(|q| q.dist(**p) <= tol)).count() {
|
||||
0 => Ok(b),
|
||||
_ => Err(KernelError::Failed(
|
||||
"fillet: the rounds would leave openings in the surface (they overlap where a wall or a face is narrower than both)".into(),
|
||||
@@ -1832,17 +1839,17 @@ pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
fn fillet_built(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
// Corners the local blends refuse (an earlier round at an edge's end): radius per edge.
|
||||
blend(body, edges, radius, Shape::Round, "fillet")
|
||||
.and_then(closed)
|
||||
.and_then(|b| closed(body, b))
|
||||
.or_else(|e| {
|
||||
let sel: Vec<(Vec3, f64)> = edges.iter().map(|p| (*p, radius)).collect();
|
||||
// The local blend's refusal, unless only the corner build found the radius too large.
|
||||
crate::blend_corner::fillet_radii(body, &sel)
|
||||
.and_then(closed)
|
||||
.and_then(|b| closed(body, b))
|
||||
.map_err(|e2| keep_size(e, e2))
|
||||
// Three edges at a vertex of any faces end in a sphere.
|
||||
.or_else(|e| {
|
||||
if edges.len() >= 3 {
|
||||
crate::blend_vertex::fillet_ball_vertices(body, edges, radius).and_then(closed).map_err(|_| e)
|
||||
crate::blend_vertex::fillet_ball_vertices(body, edges, radius).and_then(|b| closed(body, b)).map_err(|_| e)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
@@ -1850,9 +1857,9 @@ fn fillet_built(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
})
|
||||
// Straight edges ending at a curved face that isn't an earlier round's: a rolling ball
|
||||
// trimmed by the face. Refused, the refusal above stands.
|
||||
.or_else(|e| blend_with(body, edges, radius, Shape::Round, "fillet", true).and_then(closed).map_err(|_| e))
|
||||
.or_else(|e| blend_with(body, edges, radius, Shape::Round, "fillet", true).and_then(|b| closed(body, b)).map_err(|_| e))
|
||||
// Blends meeting at corners on curved faces: all at once.
|
||||
.or_else(|e| corners(body, edges, radius, Shape::Round, "fillet").and_then(closed).map_err(|e2| keep_size(e, e2)))
|
||||
.or_else(|e| corners(body, edges, radius, Shape::Round, "fillet").and_then(|b| closed(body, b)).map_err(|e2| keep_size(e, e2)))
|
||||
}
|
||||
|
||||
/// The first refusal, unless only the second found the blend too large (a smaller one may fit).
|
||||
@@ -1970,7 +1977,7 @@ pub fn fillet_each(body: &Body, edges: &[Vec3], radii: &[f64]) -> Result<Body> {
|
||||
}
|
||||
Ok(b)
|
||||
};
|
||||
if let Ok(b) = crate::blend_corner::fillet_radii(body, &sel).and_then(closed).and_then(fit) {
|
||||
if let Ok(b) = crate::blend_corner::fillet_radii(body, &sel).and_then(|b| closed(body, b)).and_then(fit) {
|
||||
return Ok(b);
|
||||
}
|
||||
let mut by_radius: Vec<(f64, Vec<Vec3>)> = Vec::new();
|
||||
|
||||
@@ -1165,15 +1165,21 @@ impl Body {
|
||||
/// (rounds that overlap where a wall is thinner than both, #871). Boundaries that only
|
||||
/// touch (at a vertex they share, or within the size's millionth) don't count.
|
||||
pub fn crossed_face_loops(&self) -> Result<usize> {
|
||||
Ok(self.crossed_faces()?.len())
|
||||
}
|
||||
|
||||
/// The faces [`Body::crossed_face_loops`] counts, each as the mean of its boundary's
|
||||
/// points: the same for a face an operation carried over unchanged.
|
||||
pub fn crossed_faces(&self) -> Result<Vec<Vec3>> {
|
||||
use mt::{BoundedCurve, ParameterDivision1D};
|
||||
if self.is_mesh() {
|
||||
return Ok(0);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
const MAX_SEGMENTS: usize = 20_000;
|
||||
let size = self.size();
|
||||
let tol = (size * 1e-6).max(1e-9);
|
||||
let chord = (size * 1e-3).max(1e-4);
|
||||
let mut crossed = 0;
|
||||
let mut crossed = Vec::new();
|
||||
for f in self.solid.face_iter() {
|
||||
let mt::Surface::Plane(pl) = f.oriented_surface() else { continue };
|
||||
let o = from_p3(pl.origin());
|
||||
@@ -1226,7 +1232,10 @@ impl Body {
|
||||
loops.iter().enumerate().flat_map(|(l, segs)| segs.iter().map(move |s| (l, s))).collect();
|
||||
let bad = all.iter().enumerate().any(|(i, (_, s))| all.iter().skip(i + 1).any(|(_, t)| cross(s, t)));
|
||||
if bad {
|
||||
crossed += 1;
|
||||
let ends: Vec<(f64, f64)> = all.iter().map(|(_, s)| s.0).collect();
|
||||
let k = ends.len().max(1) as f64;
|
||||
let (su, sv) = ends.iter().fold((0.0, 0.0), |acc, p| (acc.0 + p.0, acc.1 + p.1));
|
||||
crossed.push(o + u * (su / k) + v * (sv / k));
|
||||
}
|
||||
}
|
||||
Ok(crossed)
|
||||
|
||||
Reference in new issue
Block a user