Tapered join on a face: sew a whole face, keep the run-on inside the body (#1534) (#1763)

Thanks @HiroKenEz.
This commit is contained in:
HiroKenEz authored and GitHub committed 2026-10-16 13:26:15 -04:00
1 parent 31b2ddc0cf
commit 19be7d27e7
2 files changed
+113 -2

No files matched your search

+59 -2
View File
@@ -1942,6 +1942,39 @@ fn extend_for_coplanar(st: &ModelState, plane: &Plane, r: &Region2, lo: f64, hi:
(lo2, hi2)
}
/// 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`
/// all round; that grown outline is probed at two depths.
#[allow(clippy::too_many_arguments)]
fn taper_run_on_fits(st: &ModelState, start: &Plane, dir: Vec3, r: &Region2, depth: f64, widen: f64, op: Operation, targets: &[String]) -> bool {
const MAX_SEGMENTS: usize = 2_000;
let meshes: Vec<Arc<Mesh>> = st.bodies.iter().filter(|b| targets.is_empty() || targets.contains(&b.name)).map(|b| b.mesh()).collect();
if meshes.is_empty() || !depth.is_finite() || !widen.is_finite() {
return false;
}
let want = op == Operation::Join;
// Each loop counterclockwise with the side away from the material: right of the outer loop,
// left of a hole.
let loops: Vec<(solidcraft_geom::Loop2, f64)> = std::iter::once((r.outer.ccw(), 1.0)).chain(r.holes.iter().map(|h| (h.ccw(), -1.0))).collect();
let count: usize = loops.iter().map(|(l, _)| l.segs.len()).sum();
if count > MAX_SEGMENTS {
return false;
}
loops.iter().all(|(lp, side)| {
lp.segs.iter().all(|seg| {
[0.0, 0.25, 0.5, 0.75].iter().all(|t| {
let Some(d) = seg.derivative(*t).normalized() else { return true };
let out = Vec2::new(d.y, -d.x) * *side;
[0.5, 1.0].iter().all(|k| {
let p = start.to_world(seg.point_at(*t) + out * (widen * k)) - dir * (depth * k);
meshes.iter().any(|m| m.contains(p)) == want
})
})
})
})
}
/// How high (mm) a tapered tool's sides stand straight where it starts on a face it can't run
/// on into.
const TAPER_FOOT: f64 = 0.01;
@@ -2840,16 +2873,40 @@ fn feature_tools_with_constructor(
// sides standing on a face are curved where they meet it (along text, say),
// and the boolean finds no result there. The sides keep their slope, so
// the profile stays the section at the start.
let roles = crate::lineage::ProfileRoles { sketch: Some(&ss), region: Some(r), caps: caps.clone() };
// A start that is a whole face of the target (a foot under a part's base) is sewn on
// along it, as it lies: run on, the sides would leave the face through its edges.
if on_face
&& *operation == Operation::Join
&& let Ok(exact) = kernel::extrude_tapered_between(&start_plane, r, 0.0, len, 0.0, sign, taper)
&& st
.bodies
.iter()
.filter(|t| targets.is_empty() || targets.iter().any(|x| *x == t.name))
.any(|t| kernel::can_join_touching_faces(&t.body, &exact))
{
growth = growth.max(len * taper.tan().abs());
return Ok(roles.tool(exact, None));
}
let (lo, hi) = if sign < 0.0 { (from - len, from) } else { (from, from + len) };
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo, hi, *operation, &crate::plain(targets));
let (a, b) = if sign < 0.0 { (from - h2, from - l2) } else { (l2 - from, h2 - from) };
let (mut a, b) = if sign < 0.0 { (from - h2, from - l2) } else { (l2 - from, h2 - from) };
// A taper that narrows the tool away from the face widens it as it runs on into it, past
// the profile (where the profile is the whole face, out through the body's sides, #1534):
// then it starts on the face instead.
if a < 0.0 && a * taper.tan() > 0.0 {
let dir = n * sign;
if !taper_run_on_fits(st, &start_plane, dir, r, -a, a * taper.tan(), *operation, &crate::plain(targets)) {
a = 0.0;
}
}
// A start that can't run on (the profile hangs over the face's edge):
// the sides stand straight for a hair first, so the boolean can push the
// face the tool stands on into the body.
let foot = if a >= 0.0 && on_face { TAPER_FOOT.min((b - a) * 0.01) } else { 0.0 };
let body = kernel::extrude_tapered_between(&start_plane, r, a, b, foot, sign, taper)?;
growth = growth.max(a.abs().max(b.abs()) * taper.tan().abs());
Ok(crate::lineage::ProfileRoles { sketch: Some(&ss), region: Some(r), caps: caps.clone() }.tool(body, None))
Ok(roles.tool(body, None))
})
.collect::<Result<_>>()?;
// Profiles apart by more than the tools grow (the letters of a text): the tools