* Fillets and chamfers meeting at corners on curved faces, on top of #935 What #935 and #926 leave of #851 and the matrix: - Corner build for blends meeting on faces of any surface (curvecorner.rs): the knife-edge tip's round with the arc's or the back edge's, edges picked in any order, a round ending in an earlier round of its radius (as a feature of its own), and chamfers meeting at the arc's corner. Its end curves are fitted by chord length: a uniform parameter through points clustered at the ends doubled back on itself there, and the display planner refused the face. - A straight edge ending at a curved face (the back edge into the knife-edge tip): rolled, or cut by the face in the planar corner build (blend_corner.rs). - A rolling ball turning about a rounded corner of its own radius (a pivot: a piece of sphere or cone). - Seams that are an earlier blend's straight contact (fitted curves) count as straight. - No spline across a joint where the strip's curvature jumps (a line into a small round): each strip ends on the joint's section. - Sections at a chain's vertices on the faces of the edge from it alone. - Every new fillet path passes #949's check for openings. Tests measure volumes through the display plan, as #935's do. Action matrix: 51 more fillets and chamfers pass (sweep, tangent arc chain, sheet-metal hem, bracket holes), none fewer. Robustness seed 33 recorded (its last fillet builds differently; display and measured volumes now agree). Co-Authored-By: bherbruck <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bV2ntwLfUHV4JnCMPSyRi * Corner blend tests: scale, seed 33's volumes, tighter engine bounds From the review of #854: - seed_33_display_plan_and_measure_agree: seed 33's bodies measure the same through the display plan and the measure (2e-5), which the golden's re-record rests on. - the_arc_rounded_after_the_back_edge_matches_both_together: the order that still leaves 0.05 mm³ at the tip, as an ignored test. - corner_blends_hold_at_any_scale: the cases this branch adds on the #851 part, at 0.01x and 100x, take off what they take at full size times k³ (within 5e-6, checked to 1e-4). - The engine tests on the part as drawn measure through the display plan; bounds are about twice what each reads (2e-4 to 3e-3), and the front corner's sets are checked against the kernel's CSG grid. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bV2ntwLfUHV4JnCMPSyRi --------- Co-authored-by: bherbruck <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
13 files changed
+2739
-129
No files matched your search
@@ -729,3 +729,197 @@ fn cutting_a_text_removes_each_letter_whether_its_letters_stand_apart_or_touch()
|
||||
assert_eq!(s.world_state().bodies.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// A rounded part as drawn in the app: a rectangle and a quarter circle tangent to its top
|
||||
/// line, extruded together. The arc (about (62.17, -21), radius 21) ends square at the front
|
||||
/// face and in a knife-edge tip where it runs into the back face.
|
||||
fn knife_edge_part() -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0.0, 0.0], "p1": [41.5, -21.0]}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["l2"], "text_at": [36.5, -8.5], "value": "21 mm"}));
|
||||
run(&mut s, "sketch.line", json!({"infer": true, "points": ["p2", [61.5, -21.0]]}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["l5"], "text_at": [54.5, -26.5], "value": "21"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": "l5.end", "radius": 21.0}));
|
||||
run(&mut s, "sketch.line", json!({"infer": true, "points": ["p3", [62.166666666666664, 0.0]]}));
|
||||
run(&mut s, "sketch.constraint.tangent", json!({"a": "c1", "b": "l6"}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"direction": "positive", "distance": "10 mm", "operation": "new", "profiles": [0, 1], "sketch": 1}));
|
||||
s
|
||||
}
|
||||
|
||||
/// The volume of the session's bodies through the display plan (the measure's mesh cuts across
|
||||
/// the concave arc). It holds to about 1e-3 of what a blend removes here; the bounds below are
|
||||
/// about twice what each test reads.
|
||||
fn planned(s: &Session) -> f64 {
|
||||
s.model.state().bodies.iter().map(|b| b.body.display_plan(2e-4).unwrap().evaluate().measure().volume).sum()
|
||||
}
|
||||
|
||||
/// The middle of the part's arc.
|
||||
const KNIFE_ARC_MID: [f64; 2] = [62.166666666666664 - 21.0 * std::f64::consts::FRAC_1_SQRT_2, -21.0 + 21.0 * std::f64::consts::FRAC_1_SQRT_2];
|
||||
|
||||
/// Simpson's rule over the round's corner (2 × 2 less a quarter disc) at distance ρ = 21 + x
|
||||
/// from the arc's axis, times `length(ρ)`.
|
||||
fn round_along(length: impl Fn(f64) -> f64) -> f64 {
|
||||
let (r, n) = (2.0f64, 2000);
|
||||
let h = r / n as f64;
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let x = i as f64 * h;
|
||||
let w = if i == 0 || i == n { 1.0 } else { [2.0, 4.0][i % 2] };
|
||||
w * (r - (r * r - (r - x) * (r - x)).max(0.0).sqrt()) * length(21.0 + x)
|
||||
})
|
||||
.sum::<f64>()
|
||||
* h
|
||||
/ 3.0
|
||||
}
|
||||
|
||||
/// Its edges are stored the other way round from their curves, which once chained the arc
|
||||
/// with the front edge across their corner. Rounding the arc at the bottom and the top removes
|
||||
/// the round's section along it, cut off at the tip by the back face's plane: ρ asin(21 / ρ).
|
||||
#[test]
|
||||
fn fillet_an_arc_that_runs_into_a_knife_edge_tip() {
|
||||
let mut s = knife_edge_part();
|
||||
let before = planned(&s);
|
||||
let [x, y] = KNIFE_ARC_MID;
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[x, y, 0.0]], "radius": 2}));
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[x, y, 10.0]], "radius": 2}));
|
||||
let removed = round_along(|rho| rho * (21.0 / rho).asin());
|
||||
let after = planned(&s);
|
||||
assert!(rel(before - after, 2.0 * removed) < 2e-3, "removed {} vs {}", before - after, 2.0 * removed);
|
||||
}
|
||||
|
||||
/// The part's front corner (the front face into the arc) rounded with 2 first, then the bottom
|
||||
/// run (front edge, the corner round's arc, the arc) with 2 as well: a round on a round of the
|
||||
/// same radius. Along the front edge a straight round; at the corner the ball turns about one
|
||||
/// point, a piece of sphere (θ r³ / 6 over the corner's θ); along the arc the torus section from
|
||||
/// the corner round to the tip.
|
||||
#[test]
|
||||
fn fillet_a_run_over_a_corner_rounded_with_the_same_radius() {
|
||||
let mut s = knife_edge_part();
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[41.1667, -21.0, 5.0]], "radius": 2}));
|
||||
let before = planned(&s);
|
||||
let [x, y] = KNIFE_ARC_MID;
|
||||
// The corner round: about (62.17 - √(23² - 2²), -19), its arc's middle 2 from there.
|
||||
let cx = 62.166666666666664 - (23.0f64 * 23.0 - 4.0).sqrt();
|
||||
let joint = 2.0f64.atan2((23.0f64 * 23.0 - 4.0).sqrt());
|
||||
let theta = std::f64::consts::FRAC_PI_2 - joint;
|
||||
let corner_mid = [cx + 2.0 * (theta / 2.0).sin(), -19.0 - 2.0 * (theta / 2.0).cos()];
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[x, y, 0.0], [corner_mid[0], corner_mid[1], 0.0], [20.0, -21.0, 0.0]], "radius": 2}));
|
||||
let removed = (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * cx + theta * 8.0 / 6.0 + round_along(|rho| rho * ((21.0 / rho).asin() - joint));
|
||||
let after = planned(&s);
|
||||
assert!(rel(before - after, removed) < 1e-3, "removed {} vs {removed}", before - after);
|
||||
}
|
||||
|
||||
/// Straight edges of the part ending at its arc wall: the long front edge at the top (the wall
|
||||
/// cuts the round off at x = 62.17 − √(21² − d²), d from the front face) and the back edge at
|
||||
/// the top, which runs into the knife-edge tip (x = 62.17 − √(21² − (21 − d)²), d from the back
|
||||
/// face).
|
||||
#[test]
|
||||
fn fillet_straight_edges_that_end_at_the_arc_wall() {
|
||||
let cx = 62.166666666666664;
|
||||
for (edge, cut) in [([20.0, -21.0, 10.0], 0.0), ([30.0, 0.0, 10.0], 21.0)] {
|
||||
let mut s = knife_edge_part();
|
||||
let before = planned(&s);
|
||||
run(&mut s, "solid.fillet", json!({"edges": [edge], "radius": 2}));
|
||||
let removed = round_along(|rho| {
|
||||
let d = rho - 21.0;
|
||||
cx - (441.0 - (cut - d) * (cut - d)).sqrt()
|
||||
});
|
||||
let after = planned(&s);
|
||||
assert!(rel(before - after, removed) < 3e-3, "{edge:?}: removed {} vs {removed}", before - after);
|
||||
}
|
||||
}
|
||||
|
||||
/// The vertical knife edge at the tip (the arc and the back face meeting at no angle) rounds to
|
||||
/// the ball 2 from both, about 13 back from the tip: the cusp beyond the ball, on the tip's side
|
||||
/// of its contacts, all the way up.
|
||||
#[test]
|
||||
fn fillet_the_knife_edge_at_the_tip() {
|
||||
let cx = 62.166666666666664;
|
||||
let (kx, ky) = (cx - (23.0f64 * 23.0 - 19.0 * 19.0).sqrt(), -2.0);
|
||||
let (hx, hy) = (0.002, 0.0005);
|
||||
let mut area = 0.0;
|
||||
let mut x = kx + hx / 2.0;
|
||||
while x < cx {
|
||||
let lo = -21.0 + (441.0 - (x - cx) * (x - cx)).max(0.0).sqrt();
|
||||
for j in 0..((0.0 - lo) / hy).ceil() as usize {
|
||||
let (y0, y1) = (lo + j as f64 * hy, (lo + (j + 1) as f64 * hy).min(0.0));
|
||||
let y = (y0 + y1) * 0.5;
|
||||
let a = (y - ky).atan2(x - kx);
|
||||
if (x - kx).hypot(y - ky) > 2.0 && a >= (-21.0 - ky).atan2(cx - kx) {
|
||||
area += (y1 - y0) * hx;
|
||||
}
|
||||
}
|
||||
x += hx;
|
||||
}
|
||||
let mut s = knife_edge_part();
|
||||
let before = planned(&s);
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[cx, 0.0, 5.0]], "radius": 2}));
|
||||
let after = planned(&s);
|
||||
assert!(rel(before - after, area * 10.0) < 5e-4, "removed {} vs {}", before - after, area * 10.0);
|
||||
let mut s = knife_edge_part();
|
||||
run(&mut s, "solid.chamfer", json!({"edges": [[cx, 0.0, 5.0]], "distance": 2}));
|
||||
assert!(before - planned(&s) > before - after, "the chamfer takes the ball's bulge off too");
|
||||
}
|
||||
|
||||
/// Rounds of the edges at the arc's top front corner, two or all three together (r 2): they meet
|
||||
/// in creases (with all three, a piece of sphere fills the corner). Each set takes off its rounds
|
||||
/// alone, less what each alone takes in a box about the corner, plus what they take there
|
||||
/// together: the CSG grid of the kernel's `rounds_meeting_at_a_corner_of_an_arc_wall`
|
||||
/// (curvecorner_tests.rs, 0.02 mm cells), within its accuracy.
|
||||
#[test]
|
||||
fn fillet_edges_meeting_at_the_front_corner_of_the_arc() {
|
||||
// In the box about the corner: what the long edge's, the arc's and the upright's rounds take
|
||||
// alone, and what two or all three take together.
|
||||
let corner = [2.719176, 3.015864, 1.863];
|
||||
let together = [(vec![0, 2], 3.886936), (vec![0, 1], 4.971256), (vec![1, 2], 4.19864), (vec![0, 1, 2], 6.3332)];
|
||||
let [x, y] = KNIFE_ARC_MID;
|
||||
let (long, arc, upright) = ([20.0, -21.0, 10.0], [x, y, 10.0], [41.1667, -21.0, 5.0]);
|
||||
let off = |edges: Vec<[f64; 3]>| {
|
||||
let mut s = knife_edge_part();
|
||||
let before = planned(&s);
|
||||
run(&mut s, "solid.fillet", json!({"edges": edges, "radius": 2}));
|
||||
before - planned(&s)
|
||||
};
|
||||
let edges = [long, arc, upright];
|
||||
let alone = edges.map(|e| off(vec![e]));
|
||||
for (which, both) in together {
|
||||
let want = which.iter().map(|i| alone[*i] - corner[*i]).sum::<f64>() + both;
|
||||
let got = off(which.iter().map(|i| edges[*i]).collect());
|
||||
assert!((got - want).abs() < 0.03, "{which:?}: removed {got} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The long top front edge rounded, then the small front-left upright as a fillet of its own
|
||||
/// (as in the app): the new round ends in the earlier one, taking off what both take together.
|
||||
#[test]
|
||||
fn fillet_the_long_edge_then_the_small_left_edge() {
|
||||
let mut s = knife_edge_part();
|
||||
let before = planned(&s);
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[20.0, -21.0, 10.0]], "radius": 2}));
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[0.0, -21.0, 5.0]], "radius": 2}));
|
||||
let one_by_one = before - planned(&s);
|
||||
let mut s = knife_edge_part();
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[20.0, -21.0, 10.0], [0.0, -21.0, 5.0]], "radius": 2}));
|
||||
let together = before - planned(&s);
|
||||
// Within the display meshes' accuracy (the two are built of different faces).
|
||||
assert!(rel(one_by_one, together) < 2e-4, "one by one {one_by_one} vs together {together}");
|
||||
}
|
||||
|
||||
/// The long top front edge and the front-left vertical edge rounded together (r 2): the long
|
||||
/// round ends at the arc wall, the two meet at the top-left corner (the top-left edge sharp).
|
||||
/// Each removes its own round (the long one cut off by the wall), less what both would take at
|
||||
/// the corner: in its 2 mm cube, what lies outside both quarter circles, ∫ (2 − √(4 − t²))² dt
|
||||
/// over 0..2 = 16 − 8/3 − 4π.
|
||||
#[test]
|
||||
fn fillet_two_edges_meeting_at_a_corner_one_running_into_the_arc_wall() {
|
||||
let mut s = knife_edge_part();
|
||||
let before = planned(&s);
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[20.0, -21.0, 10.0], [0.0, -21.0, 5.0]], "radius": 2}));
|
||||
let long = round_along(|rho| 62.166666666666664 - (441.0 - (rho - 21.0) * (rho - 21.0)).sqrt());
|
||||
let upright = (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 10.0;
|
||||
let removed = long + upright - (16.0 - 8.0 / 3.0 - 4.0 * std::f64::consts::PI);
|
||||
let after = planned(&s);
|
||||
assert!(rel(before - after, removed) < 1e-3, "removed {} vs {removed}", before - after);
|
||||
}
|
||||
Reference in new issue
Block a user