The loft's top lies in the pocket floor. truck's boolean cannot take coincident faces, and the exact polygon fallback (planar_boolean) only takes bodies whose faces are all planar, so the hole's cylinder ruled out the only path that handles this cut. planar_boolean now first tries a sliced form: when an axis-aligned slab holds the tool and none of the body's curved faces, the body is split on the slab's planes (planes clear of the body's own faces), the planar middle piece takes the exact boolean, and the pieces outside are glued back on along the cut faces (the middle piece is healed first so its edges match theirs). Pieces in several parts are refused rather than glued into one shell. split_by_plane's half-space code is now ops::half_space, which this uses. Tests: loft_cut_beside_a_round_hole replays examples/regressions/loft_cut_beside_round_hole.json and checks the exact volume (slab - pocket - prismatoid - hole; fails on main). With the hole made after the loft, the body is the same (volume, area, 19 faces, 42 edges). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
2.7 KiB
Rust
67 lines
2.7 KiB
Rust
//! Booleans that failed, panicked or hung, replayed from the scripts in
|
||
//! `examples/regressions/` (each script's description says what went wrong).
|
||
|
||
use serde_json::{Value, json};
|
||
|
||
use crate::Session;
|
||
|
||
fn replay(script: &str) -> Session {
|
||
let mut s = Session::default();
|
||
let script: Value = serde_json::from_str(script).unwrap();
|
||
s.run_script(&script).unwrap();
|
||
s
|
||
}
|
||
|
||
fn volume(s: &Session, body: &str) -> f64 {
|
||
s.model.state().body(body).map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(0.0)
|
||
}
|
||
|
||
/// 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) {
|
||
let slab = 900.0 * 480.0 * 160.0;
|
||
let loft = 4.0 / 6.0 * (556.68 * 99.0 + 80.0 * 80.0 + 4.0 * (556.68 + 80.0) / 2.0 * (99.0 + 80.0) / 2.0);
|
||
(slab - 557.68 * 100.0 * 120.0, loft)
|
||
}
|
||
|
||
#[test]
|
||
fn loft_cut_beside_a_round_hole() {
|
||
let s = replay(include_str!("../../../examples/regressions/loft_cut_beside_round_hole.json"));
|
||
let (pocketed, loft) = slab_pocket_loft();
|
||
let hole = std::f64::consts::PI * 17.5 * 17.5 * 25.0;
|
||
let want = pocketed - loft - hole;
|
||
let got = volume(&s, "Body1");
|
||
assert!((got - want).abs() < 1e-6 * want, "{got} vs {want}");
|
||
}
|
||
|
||
#[test]
|
||
fn shallow_loft_cut_from_the_top_face() {
|
||
let s = replay(include_str!("../../../examples/regressions/shallow_loft_cut_from_top_face.json"));
|
||
let (_, loft) = slab_pocket_loft();
|
||
let want = 900.0 * 480.0 * 160.0 - loft;
|
||
let got = volume(&s, "Body1");
|
||
assert!((got - want).abs() < 1e-9 * want, "{got} vs {want}");
|
||
}
|
||
|
||
#[test]
|
||
fn oblique_cut_through_a_small_arc_matches_a_split() {
|
||
let scripts = [
|
||
include_str!("../../../examples/regressions/oblique_cut_of_cylinder.json"),
|
||
include_str!("../../../examples/regressions/oblique_cut_of_cylinder_hang.json"),
|
||
];
|
||
// The same bar split by the prism's tilted top: the piece above it is the cut's result.
|
||
let mut split = Session::default();
|
||
let script: Value = serde_json::from_str(scripts[0]).unwrap();
|
||
let steps = script.get("commands").and_then(Value::as_array).unwrap();
|
||
split.run_script(&Value::Array(steps[..5].to_vec())).unwrap();
|
||
let whole = volume(&split, "Tool");
|
||
split.execute("solid.split_body", &json!({"body": "Tool", "plane": {"origin": [0, -472, 853.91], "normal": [0, 15.44, 40]}})).unwrap();
|
||
let above = volume(&split, "Tool");
|
||
assert!(above < whole - 10.0, "{above} of {whole}");
|
||
for script in scripts {
|
||
let s = replay(script);
|
||
let got = volume(&s, "Tool");
|
||
assert!((got - above).abs() < 1e-6 * above, "{got} vs {above}");
|
||
}
|
||
}
|