Taper any profile, sketch text included (#1014)

A tapered extrude now offsets spline and conic curves as well as lines and arcs, so sketch text and any other profile can take a taper, and the join of tapered letters onto a face finds its boolean result.
This commit is contained in:
phanoob authored and GitHub committed 2026-10-14 09:47:44 -04:00
1 parent d114c58540
commit 4dbc0efff9
6 files changed
+619 -136

No files matched your search

+61 -3
View File
@@ -1789,6 +1789,26 @@ fn extend_for_coplanar(st: &ModelState, plane: &Plane, r: &Region2, lo: f64, hi:
(lo2, hi2)
}
/// 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;
/// Whether a face of the targets (all bodies when none are named) lies in the plane at height
/// `h` along `plane`'s normal.
fn face_level_at(st: &ModelState, plane: &Plane, h: f64, targets: &[String]) -> bool {
let n = plane.normal();
st.bodies.iter().filter(|b| targets.is_empty() || targets.contains(&b.name)).any(|b| {
let m = b.mesh();
let tol = (b.body.size() * 1e-7).max(1e-9);
m.triangles.iter().any(|t| {
m.tri(t).is_some_and(|[a, b, c]| {
(b - a).cross(c - a).normalized().is_some_and(|normal| normal.dot(n).abs() > 1.0 - 1e-9)
&& ((a - plane.origin).dot(n) - h).abs() < tol
})
})
})
}
/// The body a face-based feature applies to: by name, or the one whose surface is nearest the
/// first point.
fn body_at(st: &ModelState, body: &Option<String>, points: &[Vec3]) -> Result<usize> {
@@ -2582,13 +2602,51 @@ fn feature_tools_with_constructor(
Some(crate::lineage::extrude_cap_role(from)),
Some(crate::lineage::extrude_cap_role(if sign < 0.0 { from - len } else { from + len })),
];
return regions
// How far a tool grows past its profile at either end, at most.
let mut growth: f64 = 0.0;
let tools: Vec<Traced> = regions
.iter()
.map(|r| {
let body = kernel::extrude_tapered(&start_plane, r, len, sign, taper)?;
// An end on a face of the target runs on into it, as a prism's does: tapered
// 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 (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, targets);
let (a, b) = if sign < 0.0 { (from - h2, from - l2) } else { (l2 - from, h2 - from) };
// 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 && matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, from, targets)
{
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))
})
.collect();
.collect::<Result<_>>()?;
// Profiles apart by more than the tools grow (the letters of a text): the tools
// are apart too, and go into the body as one, in one boolean rather than one per
// profile, each on a body grown or cut by the ones before. (A join meeting several
// bodies keeps joining them tool by tool; sheet metal keeps cutting tool by tool.)
let sheet =
if targets.is_empty() { !st.sheets.is_empty() } else { targets.iter().any(|t| st.sheets.iter().any(|sheet| &sheet.body == t)) };
let one_target = if targets.is_empty() { st.bodies.len() == 1 } else { targets.len() == 1 };
let at_once = match operation {
Operation::Cut => !sheet,
Operation::Join => one_target && !sheet,
_ => false,
};
if at_once && tools.len() > 1 && regions_apart(&regions, APART_GAP + 2.0 * growth) {
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
if let Ok(Some(one)) = kernel::join_apart(&bodies) {
return Ok(vec![Traced::step(one, &tools.iter().collect::<Vec<_>>())]);
}
}
return Ok(tools);
}
let off = from
+ match &extent.start_offset {