* WIP deep rename (not for push) * WIP2 * WIP3 * WIP4 wxs fix * rustfmt * Rename the remaining identifiers in merged crash-report code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
485 lines
21 KiB
Rust
485 lines
21 KiB
Rust
//! Modifiers' histories (fillets, chamfers, shell, draft, offset, split, delete, surfaces): every
|
|
//! face and edge origin checked against the geometry of the operands (in tests only: the history
|
|
//! itself is recorded, never sampled), and the exact origins asserted where the outcome is known.
|
|
|
|
use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
|
|
|
|
use crate::body::Body;
|
|
use crate::boolean_record::tests::{self, Reach, changes, to_edge, to_face};
|
|
use crate::history::{Change, EdgeOrigin, FaceOrigin, History};
|
|
use crate::{BoolOp, boolean, box_solid, cylinder, extrude};
|
|
|
|
const TOL: f64 = 0.05;
|
|
|
|
const fn face(face: usize) -> FaceOrigin {
|
|
FaceOrigin::Face { operand: 0, face }
|
|
}
|
|
|
|
const fn edge(edge: usize) -> FaceOrigin {
|
|
FaceOrigin::Edge { operand: 0, edge }
|
|
}
|
|
|
|
const fn reach(made: f64) -> Reach {
|
|
Reach { made, moved: 0.0 }
|
|
}
|
|
|
|
/// [`checked`] at this module's mesh tolerance.
|
|
fn checked<'a>(r: &'a Body, operands: &[&Body], at: Reach, generated: &dyn Fn(usize) -> bool) -> &'a History {
|
|
tests::checked(r, operands, TOL, at, generated)
|
|
}
|
|
|
|
/// [`checked`] for a modifier of one operand whose every face comes from something of it.
|
|
fn verified<'a>(r: &'a Body, operand: &Body, at: Reach) -> &'a History {
|
|
checked(r, &[operand], at, &|_| false)
|
|
}
|
|
|
|
/// The faces whose origins are exactly `origins`.
|
|
fn with(h: &History, origins: &[FaceOrigin]) -> Vec<usize> {
|
|
(0..h.faces.len()).filter(|i| h.faces[*i].origins == origins).collect()
|
|
}
|
|
|
|
/// The origins of every face made in place of edges (blends).
|
|
fn blends(h: &History) -> Vec<Vec<FaceOrigin>> {
|
|
h.faces.iter().filter(|f| f.origins.iter().any(|o| matches!(o, FaceOrigin::Edge { .. }))).map(|f| f.origins.clone()).collect()
|
|
}
|
|
|
|
/// The index of the edge of `b` nearest `p`.
|
|
fn edge_at(b: &Body, p: Vec3) -> usize {
|
|
b.nearest_edge(p, TOL).unwrap().unwrap().0
|
|
}
|
|
|
|
/// The index of the face of `b` holding `p`.
|
|
fn face_at(b: &Body, p: Vec3) -> usize {
|
|
let m = b.tessellate(TOL).unwrap();
|
|
(0..b.face_count()).min_by(|x, y| to_face(&m, *x, p).total_cmp(&to_face(&m, *y, p))).unwrap()
|
|
}
|
|
|
|
fn cube() -> Body {
|
|
box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap()
|
|
}
|
|
|
|
fn square(x0: f64, y0: f64, x1: f64, y1: f64) -> Loop2 {
|
|
Loop2::polygon(&[Vec2::new(x0, y0), Vec2::new(x1, y0), Vec2::new(x1, y1), Vec2::new(x0, y1)])
|
|
}
|
|
|
|
/// An L-shaped prism (not convex), 20 high.
|
|
fn ell() -> Body {
|
|
let outer = Loop2::polygon(&[
|
|
Vec2::ZERO,
|
|
Vec2::new(40.0, 0.0),
|
|
Vec2::new(40.0, 15.0),
|
|
Vec2::new(15.0, 15.0),
|
|
Vec2::new(15.0, 30.0),
|
|
Vec2::new(0.0, 30.0),
|
|
]);
|
|
extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0.0, 20.0).unwrap().pop().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_one_edge() {
|
|
let b = cube();
|
|
let at = Vec3::new(20.0, 30.0, 20.0);
|
|
let r = crate::fillet(&b, &[at], 5.0).unwrap();
|
|
let h = verified(&r, &b, reach(5.0));
|
|
assert_eq!(with(h, &[edge(edge_at(&b, at))]).len(), 1);
|
|
// Two faces along the edge and the two at its ends are trimmed, two are untouched.
|
|
assert_eq!(changes(h), [2, 4, 0, 0, 1]);
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_an_edge_a_cut_split() {
|
|
let b = cube();
|
|
// A slot through the back leaves two pieces of the top back edge.
|
|
let notch = box_solid(Vec3::new(15.0, 25.0, -5.0), Vec3::new(25.0, 35.0, 25.0)).unwrap();
|
|
let cut = boolean(&b, ¬ch, BoolOp::Cut).unwrap().unwrap();
|
|
let at = Vec3::new(5.0, 30.0, 20.0);
|
|
let piece = edge_at(&cut, at);
|
|
assert_eq!(cut.history().unwrap().edges[piece].origins, [EdgeOrigin::Edge { operand: 0, edge: edge_at(&b, at) }]);
|
|
let r = crate::fillet(&cut, &[at], 3.0).unwrap();
|
|
let h = verified(&r, &cut, reach(3.0));
|
|
assert_eq!(blends(h), [vec![edge(piece)]]);
|
|
// Through the cut: the blend stands in place of the box's edge, which the cut split.
|
|
let mut through = h.clone();
|
|
through.substitute(0, cut.history().unwrap()).unwrap();
|
|
through.check(&r, &[&b, ¬ch]).unwrap();
|
|
assert_eq!(blends(&through), [vec![edge(edge_at(&b, at))]]);
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_three_edges_at_a_corner() {
|
|
let b = cube();
|
|
let pts = [Vec3::new(20.0, 30.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(40.0, 30.0, 10.0)];
|
|
let r = crate::fillet_radii(&b, &pts.map(|p| (p, 4.0))).unwrap();
|
|
let h = verified(&r, &b, reach(4.0 * 1.5));
|
|
let edges: Vec<usize> = pts.iter().map(|p| edge_at(&b, *p)).collect();
|
|
for e in &edges {
|
|
assert_eq!(with(h, &[edge(*e)]).len(), 1, "{h:?}");
|
|
}
|
|
// The corner patch stands in place of the three edges meeting there.
|
|
let mut corner: Vec<FaceOrigin> = edges.iter().map(|e| edge(*e)).collect();
|
|
corner.sort();
|
|
assert_eq!(with(h, &corner).len(), 1, "{:?}", blends(h));
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_a_whole_loop() {
|
|
let b = cube();
|
|
let pts = [Vec3::new(20.0, 0.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(20.0, 30.0, 20.0), Vec3::new(0.0, 15.0, 20.0)];
|
|
let r = crate::fillet(&b, &pts, 3.0).unwrap();
|
|
let h = verified(&r, &b, reach(3.0 * 1.5));
|
|
let mut made: Vec<FaceOrigin> = blends(h).into_iter().flatten().collect();
|
|
made.sort();
|
|
let mut want: Vec<FaceOrigin> = pts.iter().map(|p| edge(edge_at(&b, *p))).collect();
|
|
want.sort();
|
|
assert_eq!(made, want);
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_every_edge_of_a_box() {
|
|
let b = cube();
|
|
let pts: Vec<Vec3> = b.edges(TOL).unwrap().iter().map(|e| e.mid).collect();
|
|
let r = crate::fillet(&b, &pts, 2.0).unwrap();
|
|
let h = verified(&r, &b, reach(2.0 * 2.0));
|
|
// Six faces shrunk, twelve cylinders, eight corners of three edges each.
|
|
assert_eq!(changes(h), [0, 6, 0, 0, 20]);
|
|
assert_eq!(blends(h).iter().filter(|o| o.len() == 1).count(), 12);
|
|
assert_eq!(blends(h).iter().filter(|o| o.len() == 3).count(), 8);
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_a_rolling_ball_chain() {
|
|
let main = cylinder(Vec3::new(-40.0, 0.0, 0.0), Vec3::X, 15.0, 80.0).unwrap();
|
|
let branch = cylinder(Vec3::ZERO, Vec3::Z, 10.0, 35.0).unwrap();
|
|
let tee = boolean(&main, &branch, BoolOp::Union).unwrap().unwrap();
|
|
let at = Vec3::new(0.0, 10.0, 125f64.sqrt());
|
|
let r = crate::fillet(&tee, &[at], 3.0).unwrap();
|
|
let h = verified(&r, &tee, reach(3.0 * 2.0));
|
|
assert!(!blends(h).is_empty());
|
|
assert!(blends(h).iter().all(|o| o.len() == 1));
|
|
}
|
|
|
|
#[test]
|
|
fn fillet_variable_and_chord() {
|
|
let b = cube();
|
|
let at = Vec3::new(20.0, 30.0, 20.0);
|
|
let r = crate::fillet_variable(&b, at, Vec3::new(0.0, 30.0, 20.0), 2.0, 5.0).unwrap();
|
|
let h = verified(&r, &b, reach(5.0));
|
|
assert_eq!(blends(h), [vec![edge(edge_at(&b, at))]]);
|
|
let r = crate::fillet_chord(&b, &[at, Vec3::new(40.0, 15.0, 0.0)], 4.0).unwrap();
|
|
let h = verified(&r, &b, reach(4.0));
|
|
assert_eq!(blends(h).len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn chamfer_cuts_one_edge() {
|
|
let b = cube();
|
|
let at = Vec3::new(20.0, 30.0, 20.0);
|
|
let r = crate::chamfer(&b, &[at], 4.0).unwrap();
|
|
let h = verified(&r, &b, reach(4.0));
|
|
let chamfer = with(h, &[edge(edge_at(&b, at))]);
|
|
assert_eq!(chamfer.len(), 1, "{h:?}");
|
|
assert_eq!(h.faces[chamfer[0]].change, Change::Generated);
|
|
assert_eq!(r.face_count(), 7);
|
|
}
|
|
|
|
#[test]
|
|
fn chamfer_cutters_mitre_at_a_corner() {
|
|
let b = cube();
|
|
let pts = [Vec3::new(20.0, 30.0, 20.0), Vec3::new(40.0, 15.0, 20.0)];
|
|
let r = crate::chamfer(&b, &pts, 3.0).unwrap();
|
|
let h = verified(&r, &b, reach(3.0 * 1.5));
|
|
for p in pts {
|
|
assert_eq!(with(h, &[edge(edge_at(&b, p))]).len(), 1, "{h:?}");
|
|
}
|
|
// The two chamfers meet along the mitre.
|
|
let (a, c) = (edge(edge_at(&b, pts[0])), edge(edge_at(&b, pts[1])));
|
|
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::meet(a, c)]));
|
|
}
|
|
|
|
#[test]
|
|
fn chamfer_with_two_sides() {
|
|
let b = cube();
|
|
let at = Vec3::new(20.0, 30.0, 20.0);
|
|
let r = crate::chamfer_sides(&b, &[at], 2.0, crate::ChamferSide::Distance(4.0), false).unwrap();
|
|
let h = verified(&r, &b, reach(4.0));
|
|
assert_eq!(blends(h), [vec![edge(edge_at(&b, at))]]);
|
|
}
|
|
|
|
#[test]
|
|
fn delete_a_fillet() {
|
|
let b = cube();
|
|
let r = crate::fillet(&b, &[Vec3::new(20.0, 30.0, 20.0)], 5.0).unwrap();
|
|
let round = with(r.history().unwrap(), &[edge(edge_at(&b, Vec3::new(20.0, 30.0, 20.0)))]);
|
|
let back = crate::delete_faces(&r, &round).unwrap();
|
|
let h = verified(&back, &r, Reach { made: 0.0, moved: 6.0 });
|
|
// Every face is one of the filleted body's, trimmed back out to the sharp edge.
|
|
assert_eq!(h.faces.len(), 6);
|
|
assert!(h.faces.iter().all(|f| f.origins.len() == 1));
|
|
// The faces either side of the round meet again where they met nowhere in its input.
|
|
let (top, back_face) = (face_at(&r, Vec3::new(20.0, 10.0, 20.0)), face_at(&r, Vec3::new(20.0, 30.0, 5.0)));
|
|
let (x, y) = (face(top), face(back_face));
|
|
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::meet(x, y)]));
|
|
}
|
|
|
|
#[test]
|
|
fn shell_lines_the_walls_with_offsets() {
|
|
let b = cube();
|
|
let top = Vec3::new(20.0, 15.0, 20.0);
|
|
let r = crate::shell(&b, &[top], 2.0).unwrap();
|
|
let h = verified(&r, &b, reach(2.0));
|
|
// Five inner walls, each an offset of the face it lines; the top is left as a rim.
|
|
let top = face_at(&b, top);
|
|
for f in (0..6).filter(|f| *f != top) {
|
|
let walls = with(h, &[FaceOrigin::Offset { operand: 0, face: f }]);
|
|
assert_eq!(walls.len(), 1, "{h:?}");
|
|
assert_eq!(h.faces[walls[0]].change, Change::Generated);
|
|
}
|
|
assert!(with(h, &[FaceOrigin::Offset { operand: 0, face: top }]).is_empty());
|
|
assert_eq!(changes(h), [0, 6, 0, 0, 5]);
|
|
}
|
|
|
|
#[test]
|
|
fn shell_of_a_body_that_is_not_convex() {
|
|
let b = ell();
|
|
let r = crate::shell(&b, &[Vec3::new(5.0, 5.0, 20.0)], 2.0).unwrap();
|
|
// An inner wall runs past its face by the thickness at a concave corner.
|
|
let h = verified(&r, &b, reach(2.0 * 1.5));
|
|
let offsets = h.faces.iter().filter(|f| matches!(f.origins.as_slice(), [FaceOrigin::Offset { .. }])).count();
|
|
assert_eq!(offsets, 7, "{h:?}");
|
|
}
|
|
|
|
/// A shell of a block with a tab joined on a side: the cavity Cut sews the walls in pieces
|
|
/// across its slabs and joins them on their common supports, the history carried through.
|
|
#[test]
|
|
fn shell_of_a_stepped_body_joins_its_walls() {
|
|
let stock = box_solid(Vec3::new(-20.0, -15.0, 0.0), Vec3::new(20.0, 15.0, 24.0)).unwrap();
|
|
let tab = box_solid(Vec3::new(-12.0, -25.0, 8.0), Vec3::new(-4.0, -15.0, 16.0)).unwrap();
|
|
let b = boolean(&stock, &tab, BoolOp::Union).unwrap().unwrap();
|
|
let r = crate::shell(&b, &[Vec3::new(0.0, 0.0, 24.0)], 1.0).unwrap();
|
|
let h = verified(&r, &b, reach(1.0 * 1.5));
|
|
// Every face of the body but the open top is lined by one inner wall.
|
|
let offsets = h.faces.iter().filter(|f| matches!(f.origins.as_slice(), [FaceOrigin::Offset { .. }])).count();
|
|
assert_eq!(offsets, b.face_count() - 1, "{h:?}");
|
|
}
|
|
|
|
/// A shell of a painted block: its faces are used as they are (unhealed), keeping the paint's
|
|
/// face order, with the history of that copy. Paint is an authored boundary, so the cavity Cut
|
|
/// keeps the side faces it sews across the rim's level in pieces (Split).
|
|
#[test]
|
|
fn shell_of_a_painted_body() {
|
|
let b = painted_cube();
|
|
let r = crate::shell(&b, &[Vec3::new(20.0, 15.0, 20.0)], 2.0).unwrap();
|
|
let h = verified(&r, &b, reach(2.0));
|
|
assert_eq!(changes(h), [0, 2, 8, 0, 5], "{h:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn draft_tilts_faces() {
|
|
let b = cube();
|
|
let side = Vec3::new(40.0, 15.0, 10.0);
|
|
let r = crate::draft(&b, &[side], &Plane::XY, Vec3::Z, 5f64.to_radians()).unwrap();
|
|
let h = verified(&r, &b, Reach { made: 0.0, moved: 2.0 });
|
|
assert_eq!(changes(h), [0, 6, 0, 0, 0]);
|
|
assert_eq!(with(h, &[face(face_at(&b, side))]).len(), 1);
|
|
// Walls of a body that is not convex.
|
|
let l = ell();
|
|
let wall = Vec3::new(40.0, 7.0, 10.0);
|
|
let r = crate::draft(&l, &[wall], &Plane::XY, Vec3::Z, 5f64.to_radians()).unwrap();
|
|
let h = verified(&r, &l, Reach { made: 0.0, moved: 2.0 });
|
|
assert_eq!(with(h, &[face(face_at(&l, wall))]).len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn offset_moves_a_face() {
|
|
let b = ell();
|
|
let top = Vec3::new(5.0, 5.0, 20.0);
|
|
let r = crate::offset_faces(&b, &[top], 3.0).unwrap();
|
|
let h = verified(&r, &b, Reach { made: 0.0, moved: 3.0 * 1.1 });
|
|
assert_eq!(h.faces.len(), b.face_count());
|
|
assert_eq!(with(h, &[face(face_at(&b, top))]).len(), 1);
|
|
assert!(h.edges.iter().all(|e| matches!(e.origins.as_slice(), [EdgeOrigin::Edge { .. }])));
|
|
}
|
|
|
|
#[test]
|
|
fn split_a_face() {
|
|
let b = cube();
|
|
let top = face_at(&b, Vec3::new(20.0, 15.0, 20.0));
|
|
let plane = Plane::new(Vec3::new(10.0, 0.0, 0.0), Vec3::Y, Vec3::Z).unwrap();
|
|
let (r, map) = crate::split_faces_with_map(&b, &[top], &crate::SplitTool::Plane(plane)).unwrap();
|
|
let h = verified(&r, &b, reach(0.0));
|
|
// Two pieces of the top; the faces whose edges the split cut are whole, as is the rest.
|
|
assert_eq!(with(h, &[face(top)]), map.iter().enumerate().filter(|(_, f)| **f == top).map(|(i, _)| i).collect::<Vec<_>>());
|
|
assert_eq!(changes(h), [5, 0, 2, 0, 0]);
|
|
for (i, f) in map.iter().enumerate() {
|
|
assert_eq!(h.faces[i].origins, [face(*f)]);
|
|
}
|
|
// The split line runs between pieces of one face: it comes from nothing.
|
|
assert_eq!(h.edges.iter().filter(|e| e.origins.is_empty()).count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn split_a_body() {
|
|
let b = cube();
|
|
let plane = Plane::new(Vec3::new(10.0, 0.0, 0.0), Vec3::Y, Vec3::Z).unwrap();
|
|
let parts = crate::split_body(&b, &crate::SplitTool::Plane(plane)).unwrap();
|
|
assert_eq!(parts.len(), 2);
|
|
for p in &parts {
|
|
let h = checked(p, &[&b], reach(0.0), &|i| p.history().is_some_and(|h| h.faces[i].change == Change::Generated));
|
|
// One section face, new; the rest the box's faces or pieces of them.
|
|
assert_eq!(h.faces.iter().filter(|f| f.origins.is_empty()).count(), 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn surfaces_keep_their_faces() {
|
|
let b = cube();
|
|
let top = Vec3::new(20.0, 15.0, 20.0);
|
|
let copy = crate::copy_faces(&b, &[top]).unwrap();
|
|
let h = verified(©, &b, reach(0.0));
|
|
assert_eq!(h.faces[0].origins, [face(face_at(&b, top))]);
|
|
assert_eq!(h.faces[0].change, Change::Kept);
|
|
// Trimmed: the copy, cut back (its other piece is gone).
|
|
let plane = Plane::new(Vec3::new(10.0, 0.0, 0.0), Vec3::Y, Vec3::Z).unwrap();
|
|
let t = crate::trim(©, &crate::SplitTool::Plane(plane), Vec3::new(5.0, 15.0, 20.0)).unwrap();
|
|
let h = verified(&t, ©, reach(0.0));
|
|
assert_eq!(h.faces[0].origins, [face(0)]);
|
|
assert_eq!(h.faces[0].change, Change::Modified);
|
|
// Extended: the face and its edges moved out.
|
|
let x = crate::extend(©, &[Vec3::new(40.0, 15.0, 20.0)], 5.0).unwrap();
|
|
let h = verified(&x, ©, Reach { made: 0.0, moved: 5.0 * 1.1 });
|
|
assert_eq!(h.faces[0].origins, [face(0)]);
|
|
assert!(h.edges.iter().all(|e| e.origins.len() == 1));
|
|
// Patched: a new face on the body's own edges.
|
|
let open = crate::copy_faces(
|
|
&b,
|
|
&[
|
|
Vec3::new(20.0, 15.0, 0.0),
|
|
Vec3::new(20.0, 0.0, 10.0),
|
|
Vec3::new(20.0, 30.0, 10.0),
|
|
Vec3::new(0.0, 15.0, 10.0),
|
|
Vec3::new(40.0, 15.0, 10.0),
|
|
],
|
|
)
|
|
.unwrap();
|
|
let rim = [Vec3::new(20.0, 0.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(20.0, 30.0, 20.0), Vec3::new(0.0, 15.0, 20.0)];
|
|
let lid = crate::patch_edges(&open, &rim).unwrap();
|
|
let h = checked(&lid, &[&open], reach(0.0), &|_| true);
|
|
assert_eq!(h.faces[0].change, Change::Generated);
|
|
assert!(h.edges.iter().all(|e| matches!(e.origins.as_slice(), [EdgeOrigin::Edge { operand: 0, .. }])));
|
|
// Stitched: each face the one it was sewn from, the seam the edges sewn together.
|
|
let closed = crate::stitch(&[&open, &lid], 0.0).unwrap();
|
|
let h = checked(&closed, &[&open, &lid], reach(0.0), &|_| false);
|
|
assert!(h.faces.iter().all(|f| f.change == Change::Kept && f.origins.len() == 1));
|
|
assert_eq!(h.edges.iter().filter(|e| e.origins.len() == 2).count(), 4);
|
|
let patch = crate::patch_region(&Plane::XY, &Region2 { outer: square(0.0, 0.0, 10.0, 10.0), holes: vec![] }).unwrap();
|
|
assert_eq!(patch.history().unwrap().operands, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn thicken_offsets_the_surface() {
|
|
let s = crate::patch_region(&Plane::XY, &Region2 { outer: square(0.0, 0.0, 10.0, 10.0), holes: vec![] }).unwrap();
|
|
let t = crate::thicken(&s, 2.0, false).unwrap();
|
|
let h = verified(&t, &s, reach(2.0));
|
|
assert_eq!(with(h, &[face(0)]).len(), 1);
|
|
assert_eq!(with(h, &[FaceOrigin::Offset { operand: 0, face: 0 }]).len(), 1);
|
|
assert_eq!(blends(h).len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn mirrored_copy_keeps_every_face_and_edge() {
|
|
let b = ell();
|
|
let m = [[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [100.0, 0.0, 0.0, 1.0]];
|
|
let r = crate::transform_matrix(&b, m).unwrap();
|
|
let h = r.history().unwrap();
|
|
h.check(&r, &[&b]).unwrap();
|
|
// Each edge is the mirror image of the one it says it is.
|
|
let (rm, bm) = (r.tessellate(TOL).unwrap(), b.tessellate(TOL).unwrap());
|
|
for (j, e) in h.edges.iter().enumerate() {
|
|
let [EdgeOrigin::Edge { edge, .. }] = e.origins[..] else { panic!("edge {j}: {e:?}") };
|
|
for p in &rm.edges[j] {
|
|
assert!(to_edge(&bm, edge, Vec3::new(100.0 - p.x, p.y, p.z)) < 1e-6, "edge {j} is not the mirror of edge {edge}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The block with a face painted: an authored boundary, so the cuts leave the faces they split
|
|
/// across their slabs in pieces (none of them joined on their common supports).
|
|
fn painted_cube() -> Body {
|
|
cube().with_paint(Some(crate::Paint { opacity: 1., faces: vec![crate::FacePaint { face: 0, color: [1., 0., 0.], opacity: 1. }] }))
|
|
}
|
|
|
|
/// The block `b` with box cuts of each common shape: a notch through the back, a pocket, a
|
|
/// step along one end, a slot across the top, a hole through, a corner bitten out.
|
|
fn box_cuts_of(b: &Body) -> Vec<(&'static str, Body, Body)> {
|
|
[
|
|
("notch", Vec3::new(15.0, 25.0, -5.0), Vec3::new(25.0, 35.0, 25.0)),
|
|
("pocket", Vec3::new(10.0, 10.0, 10.0), Vec3::new(30.0, 20.0, 25.0)),
|
|
("step", Vec3::new(30.0, -5.0, 10.0), Vec3::new(45.0, 35.0, 25.0)),
|
|
("slot", Vec3::new(15.0, -5.0, 10.0), Vec3::new(25.0, 35.0, 25.0)),
|
|
("hole", Vec3::new(15.0, 10.0, -5.0), Vec3::new(25.0, 20.0, 25.0)),
|
|
("bite", Vec3::new(30.0, 20.0, 10.0), Vec3::new(45.0, 35.0, 25.0)),
|
|
]
|
|
.into_iter()
|
|
.map(|(name, lo, hi)| {
|
|
let tool = box_solid(lo, hi).unwrap();
|
|
let cut = boolean(b, &tool, BoolOp::Cut).unwrap().unwrap();
|
|
(name, cut, tool)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Meetings with a side of several origins (a face the modifier's heal merged).
|
|
fn merged_meetings(h: &History) -> usize {
|
|
h.edges.iter().flat_map(|e| &e.origins).filter(|o| matches!(o, EdgeOrigin::Meet { a, b } if a.single().is_none() || b.single().is_none())).count()
|
|
}
|
|
|
|
/// A fillet right after a box cut. The cut joins the walls it sews in pieces across its slabs
|
|
/// on their common supports, so the fillet finds whole faces. Where the block is painted the cut
|
|
/// keeps those pieces (it has more faces), the fillet's heal merges them, and the edges where
|
|
/// the round runs into such a face meet all of it. Composed with the cut, over the block and
|
|
/// the tool.
|
|
#[test]
|
|
fn fillet_after_box_cuts() {
|
|
let at = Vec3::new(5.0, 0.0, 20.0);
|
|
let plain = box_cuts_of(&cube());
|
|
for (b, painted) in [(cube(), false), (painted_cube(), true)] {
|
|
let cuts = if painted { box_cuts_of(&b) } else { plain.clone() };
|
|
for ((name, cut, tool), (_, whole, _)) in cuts.into_iter().zip(&plain) {
|
|
let r = crate::fillet(&cut, &[at], 2.0).unwrap();
|
|
let h = verified(&r, &cut, reach(2.0 * 1.5));
|
|
assert_eq!(blends(h), [vec![edge(edge_at(&cut, at))]], "{name}");
|
|
let pieces = cut.face_count() > whole.face_count();
|
|
assert_eq!(pieces, painted && name != "notch" && name != "hole", "{name} (painted: {painted})");
|
|
assert_eq!(merged_meetings(h) > 0, pieces, "{name} (painted: {painted}): {h:?}");
|
|
let mut through = h.clone();
|
|
through.substitute(0, cut.history().unwrap()).unwrap();
|
|
through.check(&r, &[&b, &tool]).unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A chamfer right after a box cut, its cutter's cut going the prism way along the edge: the
|
|
/// cutter's walls meet the cut body's walls where a segment was split off. The cut body's ends
|
|
/// have their outlines in collinear edges (pocket, step, bite), each matched by the segment
|
|
/// running along it; walls the cut left in pieces across the slabs (step, slot, bite of the
|
|
/// painted block) are followed through the levels where one piece gives way to the next. Where
|
|
/// the cut joined those pieces, the line between two walls stays in edges split at the levels,
|
|
/// and each slab's edge runs along the one over its levels. Composed with the cut, over the
|
|
/// block and the tool.
|
|
#[test]
|
|
fn chamfer_after_box_cuts() {
|
|
let at = Vec3::new(5.0, 0.0, 20.0);
|
|
for (b, painted) in [(cube(), false), (painted_cube(), true)] {
|
|
for (name, cut, tool) in box_cuts_of(&b) {
|
|
let r = crate::chamfer(&cut, &[at], 2.0).unwrap();
|
|
let h = verified(&r, &cut, reach(2.0 * 1.5));
|
|
// On the painted block the chamfer face keeps the boundaries the cut left.
|
|
let faces = with(h, &[edge(edge_at(&cut, at))]).len();
|
|
assert!(if painted { faces >= 1 } else { faces == 1 }, "{name} (painted: {painted}): {h:?}");
|
|
let mut through = h.clone();
|
|
through.substitute(0, cut.history().unwrap()).unwrap();
|
|
through.check(&r, &[&b, &tool]).unwrap();
|
|
}
|
|
}
|
|
}
|