Thanks @fkirill.
This commit is contained in:
1 parent
9be8bb5e6b
commit
f480898158
3 files changed
+179
No files matched your search
@@ -2141,6 +2141,46 @@ fn extend_for_coplanar(sur: &Surroundings, plane: &Plane, r: &Region2, lo: f64,
|
||||
(lo2, hi2)
|
||||
}
|
||||
|
||||
/// Remove ends of a join tool already wholly inside its targets. A tab extending
|
||||
/// a rib otherwise shares the rib's side faces through its buried portion; on a
|
||||
/// chamfered base those coincident faces can defeat every boolean retry (#2059).
|
||||
/// Only straight profiles are trimmed, after checking the entire swept material
|
||||
/// against the targets' faces, with the same clearance used when extending ends.
|
||||
fn trim_embedded_join_ends(sur: &Surroundings, plane: &Plane, r: &Region2, lo: f64, hi: f64) -> (f64, f64) {
|
||||
if sur.op != Operation::Join
|
||||
|| !std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs).all(|s| matches!(s, solidcraft_geom::Seg2::Line { .. }))
|
||||
{
|
||||
return (lo, hi);
|
||||
}
|
||||
let pieces = r.triangulate(1e-6);
|
||||
let covered: f64 = pieces.iter().map(|[a, b, c]| (*b - *a).cross(*c - *a).abs() * 0.5).sum();
|
||||
if pieces.is_empty() || (covered - r.area()).abs() > 1e-7 * r.area() {
|
||||
return (lo, hi);
|
||||
}
|
||||
let n = plane.normal();
|
||||
let mut levels: Vec<f64> = sur
|
||||
.bodies
|
||||
.iter()
|
||||
.flat_map(|(_, mesh, _)| {
|
||||
mesh.triangles.iter().filter_map(|t| {
|
||||
let [a, b, c] = mesh.tri(t)?;
|
||||
let normal = (b - a).cross(c - a).normalized()?;
|
||||
(normal.dot(n).abs() > 1.0 - 1e-9).then(|| (a - plane.origin).dot(n))
|
||||
})
|
||||
})
|
||||
.filter(|h| *h > lo + sur.skin && *h < hi - sur.skin)
|
||||
.collect();
|
||||
levels.sort_by(f64::total_cmp);
|
||||
levels.dedup_by(|a, b| (*a - *b).abs() < sur.skin);
|
||||
if levels.len() > 256 {
|
||||
return (lo, hi);
|
||||
}
|
||||
let start = levels.iter().rev().copied().find(|h| sur.harmless(plane, &pieces, -sur.skin, lo + sur.skin, h - sur.skin)).unwrap_or(lo);
|
||||
let end =
|
||||
levels.iter().copied().find(|h| *h > start + sur.skin && sur.harmless(plane, &pieces, -sur.skin, h + sur.skin, hi - sur.skin)).unwrap_or(hi);
|
||||
(start, end)
|
||||
}
|
||||
|
||||
/// Whether a tapered tool, run on by `depth` behind the plane it starts in (`dir` the way it
|
||||
/// extrudes), stays where running on changes nothing: in the targets for a join, out of them for
|
||||
/// a cut. Its sides keep their slope, so the section there is the profile grown by up to `widen`
|
||||
@@ -3180,6 +3220,23 @@ fn feature_tools_with_constructor(
|
||||
let mut extents: Vec<(f64, f64)> = Vec::with_capacity(regions.len());
|
||||
let plain_targets: Vec<String> = crate::plain(targets);
|
||||
let surroundings = Surroundings::of(st, *operation, &plain_targets);
|
||||
// Trimming may discard a tool's material, so only bodies this feature
|
||||
// actually joins can establish that the discarded end is already filled.
|
||||
let participants = if plain_targets.is_empty() { &f.participants } else { &plain_targets };
|
||||
let trim_targets: Vec<String> = st
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|b| {
|
||||
if participants.is_empty() {
|
||||
b.component == f.component || b.feature == f.id
|
||||
} else {
|
||||
participants.iter().any(|t| t == "*" || t == &b.name)
|
||||
}
|
||||
})
|
||||
.map(|b| b.name.clone())
|
||||
.collect();
|
||||
let join_surroundings =
|
||||
if *operation == Operation::Join && !trim_targets.is_empty() { Surroundings::of(st, *operation, &trim_targets) } else { None };
|
||||
for r in ®ions {
|
||||
if *operation == Operation::Join {
|
||||
let exact = centered_extrude(&ss.plane, r, lo + off, hi + off, Some(&ss))?;
|
||||
@@ -3198,6 +3255,12 @@ fn feature_tools_with_constructor(
|
||||
}
|
||||
}
|
||||
let (l2, h2) = surroundings.as_ref().map_or((lo + off, hi + off), |sur| extend_for_coplanar(sur, &ss.plane, r, lo + off, hi + off));
|
||||
let (l2, h2) = join_surroundings.as_ref().map_or((l2, h2), |sur| {
|
||||
// Trim only material in the requested extent. A run-on added
|
||||
// above to resolve a touching cap must keep doing its job.
|
||||
let (l, h) = trim_embedded_join_ends(sur, &ss.plane, r, lo + off, hi + off);
|
||||
(if l > lo + off { l } else { l2 }, if h < hi + off { h } else { h2 })
|
||||
});
|
||||
let probe = |p: Vec2| (1..4).map(|k| ss.plane.to_world(p) + n * (lo + off + (hi - lo) * k as f64 / 4.0)).collect::<Vec<_>>();
|
||||
let original = r;
|
||||
let grown = |gains: &dyn Fn(&[[Vec2; 3]]) -> bool| match &surroundings {
|
||||
@@ -5734,6 +5797,27 @@ mod body_name_tests {
|
||||
mod coplanar_growth_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn buried_join_ends_stop_at_material_boundaries() {
|
||||
let block = kernel::box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 8.0)).unwrap();
|
||||
let profile = rect(2.0, 2.0, 4.0, 4.0);
|
||||
let st = state(vec![block.clone()]);
|
||||
let sur = Surroundings::of(&st, Operation::Join, &[]).unwrap();
|
||||
assert_eq!(trim_embedded_join_ends(&sur, &Plane::XY, &profile, 0.0, 16.0), (8.0, 16.0));
|
||||
assert_eq!(trim_embedded_join_ends(&sur, &Plane::XY, &profile, -8.0, 8.0), (-8.0, 0.0));
|
||||
// A thin sealed void between the old start and the rib's top must still
|
||||
// be filled by the join. The remaining tool must start before that void.
|
||||
let void = kernel::box_solid(Vec3::new(1.0, 1.0, 3.01), Vec3::new(5.0, 5.0, 3.02)).unwrap();
|
||||
let hollow = kernel::boolean(&block, &void, BoolOp::Cut).unwrap().unwrap();
|
||||
let st = state(vec![hollow]);
|
||||
let sur = Surroundings::of(&st, Operation::Join, &[]).unwrap();
|
||||
let (lo, hi) = trim_embedded_join_ends(&sur, &Plane::XY, &profile, 0.0, 16.0);
|
||||
assert!(lo <= 3.01, "trimming past {lo} would discard material the join adds in the void");
|
||||
assert_eq!(hi, 16.0);
|
||||
let cut = Surroundings::of(&st, Operation::Cut, &[]).unwrap();
|
||||
assert_eq!(trim_embedded_join_ends(&cut, &Plane::XY, &profile, 0.0, 16.0), (0.0, 16.0));
|
||||
}
|
||||
|
||||
fn state(bodies: Vec<Body>) -> ModelState {
|
||||
ModelState { bodies: bodies.into_iter().enumerate().map(|(i, b)| ModelBody::new(format!("Body{i}"), b, 0)).collect(), ..Default::default() }
|
||||
}
|
||||
|
||||
@@ -45,6 +45,64 @@ fn volume(s: &Session, body: &str) -> f64 {
|
||||
s.model.state().body(body).map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// #2059: tabs exactly as wide as their supporting ribs share the ribs' side
|
||||
/// planes. The base's remote conical chamfer must not prevent their join.
|
||||
#[test]
|
||||
fn eighteen_tabs_join_the_ribs_of_a_chamfered_base() {
|
||||
let s = replay(include_str!("../../../examples/regressions/tabs_on_chamfered_ribbed_base.json"));
|
||||
let check = |s: &Session| {
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{:?}", s.model.results);
|
||||
assert_eq!(s.model.state().bodies.len(), 1);
|
||||
let state = s.model.state();
|
||||
let body = &state.body("Base").unwrap().body;
|
||||
assert_eq!(body.shell_count(), 1);
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
let mesh = body.tessellate(1e-4).unwrap();
|
||||
// R40 x 4 disc less its 1 x 1 chamfer, six 32 x 2 ribs
|
||||
// standing 4 above it, and eighteen 1 x 2 tabs another 8 higher.
|
||||
let expected = std::f64::consts::PI * (6400.0 - 40.0 + 1.0 / 3.0) + 1536.0 + 288.0;
|
||||
let got = mesh.measure().volume;
|
||||
assert!((got - expected).abs() < 0.1, "{got} vs {expected}");
|
||||
println!("#2059 volume {got:.6} / {expected:.6} mm³; one valid shell, eighteen tabs");
|
||||
for k in 0..6 {
|
||||
let angle = k as f64 * std::f64::consts::TAU / 6.0;
|
||||
let p = |x: f64, y: f64, z| solidcraft_geom::Vec3::new(x * angle.cos() - y * angle.sin(), x * angle.sin() + y * angle.cos(), z);
|
||||
for x in [15.5, 25.5, 35.5] {
|
||||
assert!(mesh.contains(p(x, 0.0, 12.0)), "tab {k} at {x}");
|
||||
assert!(!mesh.contains(p(x, 1.2, 12.0)), "tab {k} grew sideways at {x}");
|
||||
}
|
||||
assert!(mesh.contains(p(20.0, 0.0, 6.0)), "rib {k}");
|
||||
assert!(mesh.contains(p(39.5, 0.0, 2.0)), "rim {k}");
|
||||
assert!(!mesh.contains(p(39.75, 0.0, 3.75)), "chamfer {k}");
|
||||
}
|
||||
assert!((mesh.bounds().max.z - 16.0).abs() < 1e-8);
|
||||
};
|
||||
check(&s);
|
||||
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
check(&reopened);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buried_join_end_ignores_bodies_in_other_components() {
|
||||
for own_body in [false, true] {
|
||||
let mut s = Session::default();
|
||||
let run = |s: &mut Session, command: &str, params: Value| s.execute(command, ¶ms).unwrap();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 8, "body_name": "Other"}));
|
||||
run(&mut s, "component.create", json!({"name": "Child"}));
|
||||
if own_body {
|
||||
run(&mut s, "solid.box", json!({"corner": [50, 0, 0], "length": 10, "width": 10, "height": 8, "body_name": "Child"}));
|
||||
}
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [2, 2], "p1": [4, 4]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 16, "operation": "join", "body_name": "Tab"}));
|
||||
let got = volume(&s, if own_body { "Child" } else { "Tab" });
|
||||
let expected = 64.0 + if own_body { 800.0 } else { 0.0 };
|
||||
assert!((got - expected).abs() < 1e-6, "{got}");
|
||||
assert!((volume(&s, "Other") - 800.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
/// Slab less a 557.68 × 100 × 120 pocket and the loft below its floor (556.68 × 99 down 4 mm to
|
||||
/// 80 × 80; prismatoid: h/6 · (A_top + A_bottom + 4 A_mid)).
|
||||
fn slab_pocket_loft() -> (f64, f64) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"description": "Reduced reproduction for #2059, constructed from the public SolidCraft screenshot: eighteen 1 x 2 mm tabs extend six ribs on a chamfered round base. The tabs share the ribs' side planes; the remote conical chamfer made the join fail with Union: no result. This is an original analytic example, not the reporter's unavailable design.",
|
||||
"commands": [
|
||||
{"command": "solid.cylinder", "params": {"radius": 40, "height": 4, "body_name": "Base"}},
|
||||
{"command": "solid.chamfer", "params": {"edges": [[40, 0, 4]], "distance": 1}},
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "Ribs"}},
|
||||
{"command": "sketch.line", "params": {"points": [[5.0, -1.0], [37.0, -1.0], [37.0, 1.0], [5.0, 1.0]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[3.366025404, 3.830127019], [19.366025404, 31.54293994], [17.633974596, 32.54293994], [1.633974596, 4.830127019]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-1.633974596, 4.830127019], [-17.633974596, 32.54293994], [-19.366025404, 31.54293994], [-3.366025404, 3.830127019]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-5.0, 1.0], [-37.0, 1.0], [-37.0, -1.0], [-5.0, -1.0]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-3.366025404, -3.830127019], [-19.366025404, -31.54293994], [-17.633974596, -32.54293994], [-1.633974596, -4.830127019]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[1.633974596, -4.830127019], [17.633974596, -32.54293994], [19.366025404, -31.54293994], [3.366025404, -3.830127019]], "closed": true}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"sketch": "Ribs", "distance": 8, "operation": "join"}},
|
||||
{"command": "sketch.create", "params": {"plane": {"origin": [0, 0, 0], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": "Tabs"}},
|
||||
{"command": "sketch.line", "params": {"points": [[15.0, -1.0], [16.0, -1.0], [16.0, 1.0], [15.0, 1.0]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[25.0, -1.0], [26.0, -1.0], [26.0, 1.0], [25.0, 1.0]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[35.0, -1.0], [36.0, -1.0], [36.0, 1.0], [35.0, 1.0]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[8.366025404, 12.490381057], [8.866025404, 13.356406461], [7.133974596, 14.356406461], [6.633974596, 13.490381057]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[13.366025404, 21.150635095], [13.866025404, 22.016660498], [12.133974596, 23.016660498], [11.633974596, 22.150635095]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[18.366025404, 29.810889132], [18.866025404, 30.676914536], [17.133974596, 31.676914536], [16.633974596, 30.810889132]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-6.633974596, 13.490381057], [-7.133974596, 14.356406461], [-8.866025404, 13.356406461], [-8.366025404, 12.490381057]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-11.633974596, 22.150635095], [-12.133974596, 23.016660498], [-13.866025404, 22.016660498], [-13.366025404, 21.150635095]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-16.633974596, 30.810889132], [-17.133974596, 31.676914536], [-18.866025404, 30.676914536], [-18.366025404, 29.810889132]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-15.0, 1.0], [-16.0, 1.0], [-16.0, -1.0], [-15.0, -1.0]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-25.0, 1.0], [-26.0, 1.0], [-26.0, -1.0], [-25.0, -1.0]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-35.0, 1.0], [-36.0, 1.0], [-36.0, -1.0], [-35.0, -1.0]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-8.366025404, -12.490381057], [-8.866025404, -13.356406461], [-7.133974596, -14.356406461], [-6.633974596, -13.490381057]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-13.366025404, -21.150635095], [-13.866025404, -22.016660498], [-12.133974596, -23.016660498], [-11.633974596, -22.150635095]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[-18.366025404, -29.810889132], [-18.866025404, -30.676914536], [-17.133974596, -31.676914536], [-16.633974596, -30.810889132]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[6.633974596, -13.490381057], [7.133974596, -14.356406461], [8.866025404, -13.356406461], [8.366025404, -12.490381057]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[11.633974596, -22.150635095], [12.133974596, -23.016660498], [13.866025404, -22.016660498], [13.366025404, -21.150635095]], "closed": true}},
|
||||
{"command": "sketch.line", "params": {"points": [[16.633974596, -30.810889132], [17.133974596, -31.676914536], [18.866025404, -30.676914536], [18.366025404, -29.810889132]], "closed": true}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"sketch": "Tabs", "distance": 16, "operation": "join"}}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user