Text cut through a part, or joined in relief, in one exact boolean (#1446)
Checked locally on main merged with this: cargo check --workspace, the kernel and doc tests, and all robustness goldens (all_0 to all_3) pass.
This commit is contained in:
1 parent
5fbbc13885
commit
39f6d68a50
3 files changed
+103
-9
No files matched your search
+21
-6
@@ -2827,6 +2827,8 @@ fn feature_tools_with_constructor(
|
||||
let mut untrimmed = extent.to.is_none() && extent.from.is_none() && !extent.through_all;
|
||||
// The footprint of each tool in the sketch: a tool lies in the prism over it.
|
||||
let mut footprints = Vec::with_capacity(regions.len());
|
||||
// Each tool's extent along the normal (they differ where one is run on into a face).
|
||||
let mut extents: Vec<(f64, f64)> = Vec::with_capacity(regions.len());
|
||||
for r in ®ions {
|
||||
if *operation == Operation::Join {
|
||||
let exact = centered_extrude(&ss.plane, r, lo + off, hi + off, Some(&ss))?;
|
||||
@@ -2840,6 +2842,7 @@ fn feature_tools_with_constructor(
|
||||
// the sewable seam with a coincident curved intersection.
|
||||
tools.extend(exact);
|
||||
footprints.push(r.clone());
|
||||
extents.push((lo + off, hi + off));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -2850,15 +2853,27 @@ fn feature_tools_with_constructor(
|
||||
untrimmed &= l2 == lo + off && h2 == hi + off && &r == original;
|
||||
tools.extend(centered_extrude(&ss.plane, &r, l2, h2, Some(&ss))?);
|
||||
footprints.push(r);
|
||||
extents.push((l2, h2));
|
||||
}
|
||||
// A cut by profiles apart in the sketch (the letters of a text): each tool lies in the
|
||||
// prism over its footprint, so the tools are apart too (whatever their heights, and
|
||||
// trimmed or not). The body is then cut once by all of them rather than once per
|
||||
// profile, each cut working on a body grown by the one before.
|
||||
// (Sheet metal keeps its own way of cutting, tool by tool, to keep its flat pattern.)
|
||||
// A cut or a join by profiles apart in the sketch (the letters of a text): each tool
|
||||
// lies in the prism over its footprint, so the tools are apart too (whatever their
|
||||
// heights, and trimmed or not). The body is then cut once by all of them, or joined to
|
||||
// all of them at once, rather than once 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 its own way of cutting, tool by tool, to keep its flat pattern.)
|
||||
let sheet =
|
||||
if targets.is_empty() { !st.sheets.is_empty() } else { targets.iter().any(|t| st.sheets.iter().any(|sheet| &sheet.body == t)) };
|
||||
if *operation == Operation::Cut && !sheet && tools.len() > 1 && regions_apart(&footprints, APART_GAP) {
|
||||
let one_target = if targets.is_empty() { st.bodies.len() == 1 } else { targets.len() == 1 };
|
||||
// A join at once only of tools alike in height (one prism of them all, which the
|
||||
// boolean joins exactly): tools that start at different levels (some run on into the
|
||||
// face, one hanging over its edge) keep joining one by one.
|
||||
let alike = extents.windows(2).all(|w| (w[0].0 - w[1].0).abs() < 1e-9 && (w[0].1 - w[1].1).abs() < 1e-9);
|
||||
let at_once = match operation {
|
||||
Operation::Cut => !sheet,
|
||||
Operation::Join => one_target && !sheet && alike,
|
||||
_ => false,
|
||||
};
|
||||
if at_once && tools.len() > 1 && regions_apart(&footprints, APART_GAP) {
|
||||
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
|
||||
if let Ok(Some(one)) = kernel::join_apart(&bodies) {
|
||||
tools = vec![Traced::step(one, &tools.iter().collect::<Vec<_>>())];
|
||||
|
||||
@@ -1044,3 +1044,35 @@ fn a_line_of_text_is_cut_in_one_exact_boolean() {
|
||||
let off = cut([-6.0, 20.0], 1.0);
|
||||
assert!(off > 0.0 && off < one, "{off} {one}");
|
||||
}
|
||||
|
||||
/// A line of text cut right through a part, and one joined on in relief, are decided in 2D in
|
||||
/// one boolean too. Through, each letter's counter (inside an "e" or an "a") comes out as a
|
||||
/// piece of its own. In relief the letters are joined to the part all at once: one by one, each
|
||||
/// joined to a part grown by the ones before, it took minutes.
|
||||
#[test]
|
||||
fn a_line_of_text_cut_through_or_joined_in_relief_is_one_boolean() {
|
||||
let text = |op: &str, distance: f64| {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane":"XY"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[105,26]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance":40}));
|
||||
let before = volume(&mut s);
|
||||
run(&mut s, "sketch.create", json!({"plane":{"face":[50,13,40]}}));
|
||||
run(&mut s, "sketch.text", json!({"text":"textvraiment tres long","at":[3,9],"height":6.5}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let id = run(&mut s, "solid.extrude", json!({"distance":distance,"operation":op}))["feature"].as_u64().unwrap();
|
||||
let r = s.model.result(id).unwrap();
|
||||
assert!(r.error.is_none() && r.boolean_diagnostics.events.is_empty(), "{op}: {r:?}");
|
||||
(before, volume(&mut s), s.model.state().bodies.len())
|
||||
};
|
||||
let (before, through, pieces) = text("cut", -40.0);
|
||||
let (_, one, _) = text("cut", -1.0);
|
||||
// Through: the letters' area all the way down, with the counters left as pieces.
|
||||
assert!(pieces > 1, "{pieces}");
|
||||
assert!(rel(before - through, 40.0 * (before - one)) < 2e-3, "{} vs {}", before - through, 40.0 * (before - one));
|
||||
// In relief 2 high: twice what a 1 mm cut takes.
|
||||
let (_, relief, pieces) = text("join", 2.0);
|
||||
assert_eq!(pieces, 1);
|
||||
assert!(rel(relief - before, 2.0 * (before - one)) < 2e-3, "{} vs {}", relief - before, 2.0 * (before - one));
|
||||
}
|
||||
@@ -1286,10 +1286,13 @@ fn exact_loop(lp: &Loop2, tags: &[Tag], sources: &[Seg2], near: f64) -> (Vec<Seg
|
||||
j += 1;
|
||||
}
|
||||
let Some(last) = lp.segs.get((first + j) % n) else { return plain() };
|
||||
// A run reaching the curve's end is the curve to its end, exactly: the same curve the
|
||||
// walls over it are read from, which a parameter found by projection (a hair short of 1)
|
||||
// would cut short into a different one.
|
||||
let at = |p: Vec2| {
|
||||
if p.dist(src.start()) < 1e-9 {
|
||||
if p.dist(src.start()) < near {
|
||||
0.0
|
||||
} else if p.dist(src.end()) < 1e-9 {
|
||||
} else if p.dist(src.end()) < near {
|
||||
1.0
|
||||
} else {
|
||||
src.closest_param(p)
|
||||
@@ -1359,6 +1362,29 @@ fn exact_loop(lp: &Loop2, tags: &[Tag], sources: &[Seg2], near: f64) -> (Vec<Seg
|
||||
}
|
||||
}
|
||||
}
|
||||
// The loop closed exactly: where a segment ends a hair from where the next starts (a font's
|
||||
// nearly round curve read as an arc ends a few hundredths of a micron off the curve after
|
||||
// it), the one that is not an arc moves its end onto the arc's (an arc's ends stay on its
|
||||
// circle). Two arcs are left as they meet, as a boolean of lines and arcs leaves them.
|
||||
let m = segs.len();
|
||||
for i in 0..m {
|
||||
let j = (i + 1) % m;
|
||||
let (Some(x), Some(y)) = (segs.get(i).copied(), segs.get(j).copied()) else { continue };
|
||||
let gap = x.end().dist(y.start());
|
||||
if gap == 0.0 || gap > near {
|
||||
continue;
|
||||
}
|
||||
let arc = |s: &Seg2| matches!(s, Seg2::Arc { .. });
|
||||
if !arc(&y) {
|
||||
if let Some(s) = segs.get_mut(j) {
|
||||
*s = seg_with_ends(&y, x.end(), y.end());
|
||||
}
|
||||
} else if !arc(&x)
|
||||
&& let Some(s) = segs.get_mut(i)
|
||||
{
|
||||
*s = seg_with_ends(&x, x.start(), y.start());
|
||||
}
|
||||
}
|
||||
(segs, ts)
|
||||
}
|
||||
|
||||
@@ -1902,6 +1928,27 @@ fn sew_connected_sections(
|
||||
&& s.length() <= 2.0 * tol
|
||||
&& matches!((weld(&ends, &grid, s.start()), weld(&ends, &grid, s.end())), (Some(p), Some(q)) if p == q)
|
||||
};
|
||||
// One curve between two ends, as the walls and the caps beside them each read it: their
|
||||
// sections come from different booleans, which split it from different pieces, so the
|
||||
// control points can differ by a hair, and sewing tells edges apart by them. The first
|
||||
// reading stands for all of them.
|
||||
let chains: std::cell::RefCell<Vec<[Vec3; 4]>> = std::cell::RefCell::new(Vec::new());
|
||||
let same_chain = |c: [Vec3; 4]| -> [Vec3; 4] {
|
||||
let mid = |k: &[Vec3; 4]| (k[0] + (k[1] + k[2]) * 3.0 + k[3]) * 0.125;
|
||||
let mut known = chains.borrow_mut();
|
||||
for k in known.iter() {
|
||||
if mid(k).dist(mid(&c)) <= tol * 10.0 {
|
||||
if k[0] == c[0] && k[3] == c[3] {
|
||||
return *k;
|
||||
}
|
||||
if k[0] == c[3] && k[3] == c[0] {
|
||||
return [k[3], k[2], k[1], k[0]];
|
||||
}
|
||||
}
|
||||
}
|
||||
known.push(c);
|
||||
c
|
||||
};
|
||||
let at = |u: Vec2, z: f64| plane.to_world(u) + d * z;
|
||||
let welded = |u: Vec2, z: f64| at(weld(&ends, &grid, u).unwrap_or(u), z);
|
||||
let edge = |sg: &Seg2, z: f64| -> Option<EdgeSpec> {
|
||||
@@ -1910,7 +1957,7 @@ fn sew_connected_sections(
|
||||
Seg2::Arc { .. } => EdgeSpec::Arc { a: welded(sg.start(), z), b: welded(sg.end(), z), mid: at(sg.point_at(0.5), z) },
|
||||
_ => {
|
||||
let [p0, p1, p2, p3] = cubic_of(sg)?;
|
||||
EdgeSpec::Chain { ctrl: vec![welded(p0, z), at(p1, z), at(p2, z), welded(p3, z)].into() }
|
||||
EdgeSpec::Chain { ctrl: same_chain([welded(p0, z), at(p1, z), at(p2, z), welded(p3, z)]).to_vec().into() }
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user