Tests: SMLib's faces, one per swept segment, closed along a seam, replace truck's halves

Truck split what it swept: a big arc into two faces, a full circle into halves, a turn into
two or three parts, a sphere into six patches, a torus into six; it numbered boxes' faces in its
own order and sewed stepped cuts slab by slab. SMLib sweeps each profile segment into one face,
closes a full cylinder, sphere or torus along one seam edge, and cuts without slabs. The record,
face source, helix, display mesh, measure and boolean history tests still expected truck's
pieces. Each expectation now follows the geometry, checked by the tests' own independent
measures: every face's origin against the profile segment its mesh lies over (unchanged),
faces picked by where they lie instead of by truck's face numbers, and the merged counts by the
kernel-cases ladder's rule (each whole curve once, and each cylinder's seam): a cylinder 3
faces, 3 edges, 2 vertices; a ball cut from a slab 7, 14, 10. The truck-only stepped-prism
check (walls coalesced across slabs) becomes the property it stood for: the stock's walls stay
whole, one face each, Kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 22:16:55 -04:00
committed by Irakli Kourtis
parent 3c680d21a9
commit 30ceb44e6d
6 changed files with 82 additions and 84 deletions
+36 -34
View File
@@ -188,15 +188,16 @@ fn faces_of(h: &History) -> Vec<(Change, Vec<FaceOrigin>)> {
}
/// A boolean with a sphere (no prism, no coincident faces): a box with a corner taken
/// off by a sphere. The three faces at the corner get a notch, the sphere's patches inside the
/// box bound it, meeting the box's faces along new edges.
/// off by a sphere. The three faces at the corner get a notch, the sphere's one face (SMLib's
/// sphere is one periodic face) bounds it inside the box, meeting each notched face along a new
/// edge.
#[test]
fn box_minus_sphere() {
let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
let b = crate::sphere(Vec3::new(10.0, 10.0, 10.0), 4.0).unwrap();
let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap();
let h = verified(&r, &[&a, &b], 0.02);
assert_eq!(changes(h), [3, 6, 0, 0, 0], "{}", show(h));
assert_eq!(changes(h), [3, 4, 0, 0, 0], "{}", show(h));
let from_b: BTreeSet<usize> = h
.faces
.iter()
@@ -206,9 +207,9 @@ fn box_minus_sphere() {
_ => None,
})
.collect();
assert_eq!(from_b.len(), 3);
// Each notched box face meets two of the sphere's patches.
assert_eq!(meets(h).len(), 6, "{}", show(h));
assert_eq!(from_b.len(), 1);
// Each notched box face meets the sphere.
assert_eq!(meets(h).len(), 3, "{}", show(h));
assert!(meets(h).iter().all(|(x, y)| matches!((x, y), (FaceOrigin::Face { operand: 0, .. }, FaceOrigin::Face { operand: 1, .. }))));
}
@@ -231,9 +232,10 @@ fn box_union_box_prism() {
})
.collect();
origins.sort();
// All of a's faces, and b's but the end inside a.
// All of a's faces, and b's but the end inside a (its face at x = 5).
let mut want: Vec<FaceOrigin> = (0..6).map(|i| face(0, i)).chain((0..6).map(|i| face(1, i))).collect();
let inside = face(1, 4);
let inside =
face(1, b.faces(1e-3).unwrap().iter().position(|f| (f.centroid.x - 5.0).abs() < 1e-6 && f.plane_normal.is_some_and(|n| n.x < -0.9)).unwrap());
want.retain(|o| *o != inside);
assert_eq!(origins, want, "{}", show(h));
// b's four sides meet a's face at x = 10.
@@ -243,22 +245,22 @@ fn box_union_box_prism() {
assert!(h.edges.iter().all(|e| e.origins.len() == 1));
}
/// A through hole: the box's sides are kept, top and bottom get the hole, the cylinder's two
/// half walls line it, meeting top and bottom along its rims, and meet each other along the
/// cylinder's own seams.
/// A through hole: the box's sides are kept, top and bottom get the hole, the cylinder's wall
/// (one periodic face) lines it, meeting top and bottom along its rims, and closes on itself
/// along the cylinder's own seam.
#[test]
fn box_minus_cylinder_prism() {
let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
let b = crate::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 12.0).unwrap();
let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap();
let h = verified(&r, &[&a, &b], 0.01);
assert_eq!(changes(h), [4, 4, 0, 0, 0], "{}", show(h));
let walls = from(h, face(1, 1)).len() + from(h, face(1, 2)).len();
assert_eq!(walls, 2, "{}", show(h));
// Rims: each wall meets top and bottom.
assert_eq!(meets(h).len(), 4, "{}", show(h));
assert_eq!(changes(h), [4, 3, 0, 0, 0], "{}", show(h));
let side = (0..b.face_count()).find(|i| b.planar_face(*i).is_none()).unwrap();
assert_eq!(from(h, face(1, side)).len(), 1, "{}", show(h));
// Rims: the wall meets top and bottom.
assert_eq!(meets(h).len(), 2, "{}", show(h));
let seams = h.edges.iter().filter(|e| matches!(e.origins.as_slice(), [EdgeOrigin::Edge { operand: 1, .. }])).count();
assert_eq!(seams, 2);
assert_eq!(seams, 1);
// Every other edge is one of the box's twelve.
let boxed_edges: BTreeSet<_> = h.edges.iter().flat_map(|e| &e.origins).filter(|o| matches!(o, EdgeOrigin::Edge { operand: 0, .. })).collect();
assert_eq!(boxed_edges.len(), 12);
@@ -320,9 +322,9 @@ fn a_cut_of_two_pieces_composes_the_pieces() {
let b = crate::sphere(Vec3::new(15.0, 5.0, 10.0), 7.0).unwrap();
let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap();
let h = verified(&r, &[&a, &b], 0.02);
// Both pieces are bitten: the sphere's patches inside each line its bite.
let bites: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 1, .. })).collect();
assert!(bites.len() >= 2, "{}", show(h));
// Both pieces are bitten: a piece of the sphere's face lines each bite.
let bites = h.faces.iter().filter(|f| matches!(f.origins.as_slice(), [FaceOrigin::Face { operand: 1, .. }])).count();
assert!(bites >= 2, "{}", show(h));
let targets: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 0, .. })).collect();
assert_eq!(targets.len(), 12);
}
@@ -341,10 +343,10 @@ fn faceted_tool() {
assert_eq!(h.faces.iter().filter(|f| matches!(f.origins.first(), Some(FaceOrigin::Face { operand: 1, .. }))).count(), 3);
}
/// A partial-depth cut of stepped prisms: walls over one segment through successive slabs are
/// sewn as one face (coalesced), which comes from the operand walls under it in every slab.
/// A partial-depth cut of a prism by a narrower one: the stock's walls run whole from 0 to 20,
/// one face each past the cutter's floor, untouched (Kept).
#[test]
fn coalesced_stepped_walls() {
fn stepped_cut_keeps_the_stock_walls_whole() {
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
for curved in [true, false] {
let (outer, inner) = if curved {
@@ -357,20 +359,19 @@ fn coalesced_stepped_walls() {
let cutter = crate::extrude(&Plane::XY, &[Region2 { outer: inner, holes: vec![] }], 2.0, 22.0).unwrap().pop().unwrap();
let r = boolean(&stock, &cutter, BoolOp::Cut).unwrap().unwrap();
let h = verified(&r, &[&stock, &cutter], 0.01);
// The stock's walls run whole from 0 to 20, one face each, past the cutter's floor.
let walls: Vec<usize> = (0..r.face_count()).filter(|i| r.face_was_coalesced(*i)).collect();
assert_eq!(walls.len(), if curved { 2 } else { 4 }, "{}", show(h));
let walls: Vec<usize> =
stock.faces(1e-3).unwrap().iter().enumerate().filter(|(_, f)| f.plane_normal.is_none_or(|n| n.z.abs() < 0.5)).map(|(i, _)| i).collect();
assert_eq!(walls.len(), if curved { 1 } else { 4 });
for i in walls {
assert!(matches!(h.faces[i].origins.as_slice(), [FaceOrigin::Face { operand: 0, .. }]), "{}", show(h));
assert_eq!(h.faces[i].change, Change::Modified, "{}", show(h));
let pieces = from(h, face(0, i));
assert_eq!(pieces.len(), 1, "{}", show(h));
assert_eq!(h.faces[pieces[0]].change, Change::Kept, "{}", show(h));
}
}
}
/// A step cut into a box: the stepped prism path sews the box's face 3 in pieces at the
/// slab stations, and the joining of generated walls on their common supports makes it one face
/// again; the joined body keeps the sewn body's history composed through the join. A certified
/// box (whose planar regularization would join the same pieces) gets the same history.
/// A step cut into a box: the box's top (z = 7.5) loses the step's corner, one face still
/// (Modified). A certified box gets the same history as a plain one.
#[test]
fn regularized_step_cut() {
let (pa, pb) = (boxed(0.0, 0.0, 0.0, 30.0, 35.0, 7.5), boxed(0.0, 30.0, 2.5, 22.5, 50.0, 25.0));
@@ -379,9 +380,10 @@ fn regularized_step_cut() {
let h = verified(&r, &[&a, &b], 0.01);
let plain = boolean(&pa, &pb, BoolOp::Cut).unwrap().unwrap();
let ph = verified(&plain, &[&pa, &pb], 0.01);
let joined = from(ph, face(0, 3));
let top = pa.faces(1e-3).unwrap().iter().position(|f| (f.centroid.z - 7.5).abs() < 1e-9).unwrap();
let joined = from(ph, face(0, top));
assert_eq!(joined.len(), 1, "{}", show(ph));
assert_eq!(faces_of(ph)[joined[0]], (Change::Modified, vec![face(0, 3)]));
assert_eq!(faces_of(ph)[joined[0]], (Change::Modified, vec![face(0, top)]));
assert_eq!(faces_of(h), faces_of(ph), "{}", show(h));
}
+2 -1
View File
@@ -378,7 +378,8 @@ fn a_cylinder_wall_split_along_its_seam_meshes_round() {
let outer: Vec<bool> = (0..cup.face_count())
.map(|i| matches!(face_surface(&cup, i), Some(FaceSurface::Cylinder { radius, .. }) if (radius - 50.0).abs() < 1e-6))
.collect();
assert_eq!(outer.iter().filter(|o| **o).count(), 2);
// One periodic face (SMLib sweeps the one circle segment once), its seam split at z = 2.
assert_eq!(outer.iter().filter(|o| **o).count(), 1);
for tol in [display_tol(&cup), 0.1, 0.01] {
let m = cup.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
assert_eq!(open_sides(&m, 1e-6), 0);
+2 -2
View File
@@ -272,7 +272,7 @@ fn constructed_annulus_sides_follow_exact_source_edges() {
}
assert!(vertices > 0);
}
assert_eq!((outer, inner), (2, 2), "full circles split into two actual edges each");
assert_eq!((outer, inner), (1, 1), "each full circle sweeps one side face");
assert_eq!(sources.iter().filter(|source| matches!(source, Some(FaceSource::ExtrudeCap { .. }))).count(), 2);
}
}
@@ -311,7 +311,7 @@ fn constructed_large_arc_split_and_reversal_preserve_one_source_segment() {
_ => None,
})
.collect();
assert_eq!(arcs.len(), 2, "large input arc explicitly creates two actual boundary edges");
assert_eq!(arcs.len(), 1, "the large input arc sweeps one side face");
assert!(arcs.iter().all(|(radius, sweep)| (**radius - 10.).abs() < 1e-12 && (sweep.abs() - std::f64::consts::PI * 5. / 3.).abs() < 1e-12));
assert_eq!(sources.iter().filter(|source| matches!(source, Some(FaceSource::ExtrudeSide { segment: Seg2::Line { .. }, .. }))).count(), 1);
let mesh = body.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
+2 -1
View File
@@ -147,7 +147,8 @@ mod tests {
let region = Region2 { outer: Loop2::circle(Vec2::new(20.0, 0.0), 2.0), holes: vec![] };
let b = sweep_helix(&pl, &region, Vec3::ZERO, Vec3::Z, 8.0, 3.0).unwrap();
let v = crate::measure(&b).unwrap().volume;
assert_eq!(b.face_count(), 4, "two smooth walls and two caps");
// SMLib sweeps the circle (one segment) as one smooth wall.
assert_eq!(b.face_count(), 3, "one smooth wall and two caps");
let want = std::f64::consts::PI * 4.0 * std::f64::consts::TAU * 20.0 * 3.0;
assert!((v - want).abs() / want < 1e-3, "{v} vs {want}");
assert!(sweep_helix(&pl, &region, Vec3::ZERO, Vec3::Z, 3.0, 3.0).is_err(), "pitch below the section");
+32 -43
View File
@@ -141,24 +141,30 @@ fn extrude_sides_follow_profile_segments_in_any_order() {
// The certificates are produced as before.
assert!(b.face_sources().unwrap().iter().all(Option::is_some));
}
// The +x side is whichever segment of the given loop runs along x = 40.
// The +x side is whichever segment of the given loop runs along x = 40 (the hole's wall at
// x = 10 faces +x too).
let r = &variants[2];
let b = crate::extrude(&Plane::XY, std::slice::from_ref(r), 0.0, 10.0).unwrap().pop().unwrap();
let k = r.outer.segs.iter().position(|s| s.start().x == 40.0 && s.end().x == 40.0).unwrap();
let side = b.faces(1e-3).unwrap().iter().position(|f| f.plane_normal.is_some_and(|n| n.dist(Vec3::X) < 1e-9)).unwrap();
let side = b
.faces(1e-3)
.unwrap()
.iter()
.position(|f| f.plane_normal.is_some_and(|n| n.dist(Vec3::X) < 1e-9) && (f.centroid.x - 40.0).abs() < 1e-6)
.unwrap();
assert_eq!(origins(&b)[side], [P(k)]);
}
/// A big arc is built as two edges: both faces come from it.
/// A big arc sweeps one face, as every segment does.
#[test]
fn extrude_a_big_arc_gives_two_faces_of_one_segment() {
fn extrude_a_big_arc_gives_one_face_of_one_segment() {
let (a, b) = (Vec2::new(10.0 * (0.3f64).cos(), -10.0 * (0.3f64).sin()), Vec2::new(10.0 * (0.3f64).cos(), 10.0 * (0.3f64).sin()));
let arc = Seg2::Arc { center: Vec2::ZERO, radius: 10.0, start: 0.3, sweep: TAU - 0.6 };
let r = Region2 { outer: Loop2 { segs: vec![arc, Seg2::Line { a, b }] }, holes: vec![] };
let body = crate::extrude(&Plane::XY, std::slice::from_ref(&r), 0.0, 5.0).unwrap().pop().unwrap();
let got = origins(&body);
assert_eq!(got, expected_extrusion(&body, &Plane::XY, &r, 5.0));
assert_eq!(got.iter().filter(|o| **o == [P(0)]).count(), 2);
assert_eq!(got.iter().filter(|o| **o == [P(0)]).count(), 1);
assert_eq!(got.iter().filter(|o| **o == [P(1)]).count(), 1);
}
@@ -177,18 +183,18 @@ fn primitives_report_their_construction() {
assert_eq!(o, &[want]);
}
assert!(b.face_sources().unwrap().iter().all(|s| matches!(s, Some(crate::FaceSource::Box { .. }))));
// Cylinder: a circle of two arcs, extruded.
// Cylinder: a circle (one arc), extruded.
let c = crate::cylinder(Vec3::ZERO, Vec3::Z, 5.0, 8.0).unwrap();
let r = Region2 { outer: Loop2::circle_from(Vec2::ZERO, 5.0, crate::build::SEAM_ANGLE), holes: vec![] };
let frame = Plane::from_normal(Vec3::ZERO, Vec3::Z).unwrap();
let want = expected_extrusion(&c, &frame, &r, 8.0);
assert_eq!(origins(&c), want);
// Sphere: six patches, all from the arc of the half disc it revolves.
// Sphere: one face, from the arc of the half disc it revolves.
let s = crate::sphere(Vec3::ZERO, 5.0).unwrap();
assert_eq!(origins(&s), vec![vec![P(0)]; 6]);
// Torus: a circle re-split for its seam, turned in three: each face from both arcs.
assert_eq!(origins(&s), vec![vec![P(0)]]);
// Torus: one face, from the circle (segment 0) it turns.
let t = crate::torus(Vec3::ZERO, 10.0, 2.0).unwrap();
assert_eq!(origins(&t), vec![vec![P(0), P(1)]; 6]);
assert_eq!(origins(&t), vec![vec![P(0)]]);
}
/// The plane the revolve tests sketch in (x across, y up the world z axis, the revolve axis).
@@ -209,31 +215,28 @@ fn expected_revolution(b: &Body, r: &Region2) -> Vec<Vec<FaceOrigin>> {
#[test]
fn revolve_partial_and_full() {
let r = Region2 { outer: rect(10.0, 0.0, 20.0, 5.0), holes: vec![rect(12.0, 1.0, 14.0, 2.0).reversed()] };
// Under half a turn truck sweeps in two parts, beyond it in three; a full turn has no caps,
// and its planar faces (from the segments square to the axis) are whole (#726), as a partial
// SMLib turns each segment into one face whatever the angle; a full turn has no caps, and
// its planar faces (from the segments square to the axis) are whole (#726), as a partial
// turn's are when it is final (`revolve_whole`).
let segs: Vec<Seg2> = r.outer.segs.iter().chain(r.holes.iter().flat_map(|h| &h.segs)).cloned().collect();
for whole in [false, true] {
for (angle, parts, caps) in [(FRAC_PI_2, 2, 2), (1.5 * PI, 3, 2), (TAU, 3, 0)] {
for (angle, caps) in [(FRAC_PI_2, 2), (1.5 * PI, 2), (TAU, 0)] {
let revolve = if whole { crate::revolve_whole } else { crate::revolve };
let b = revolve(&SIDE, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, angle).unwrap().pop().unwrap();
let got = origins(&b);
assert_eq!(got, expected_revolution(&b, &r), "angle {angle}, whole {whole}");
for (k, s) in segs.iter().enumerate() {
let square = matches!(s, Seg2::Line { a, b } if (a.y - b.y).abs() < 1e-12);
let want = if (angle == TAU || whole) && square { 1 } else { parts };
assert_eq!(got.iter().filter(|o| **o == [P(k)]).count(), want, "segment {k} at angle {angle}, whole {whole}");
for k in 0..segs.len() {
assert_eq!(got.iter().filter(|o| **o == [P(k)]).count(), 1, "segment {k} at angle {angle}, whole {whole}");
}
assert_eq!(got.iter().filter(|o| **o == [START] || **o == [END]).count(), caps);
}
}
}
/// Loops given clockwise, a hole that is a full circle of three arcs: extrude numbers every
/// face by the segment as given; revolve re-seams the circle into two half circles, each from
/// every given arc it runs over.
/// Loops given clockwise, a hole that is a full circle of three arcs: extrude and revolve
/// number every face by the segment as given, one face each.
#[test]
fn clockwise_loops_and_a_reseamed_circle_keep_their_numbers() {
fn clockwise_loops_and_a_circle_of_arcs_keep_their_numbers() {
let (c, rad) = (Vec2::new(20.0, 10.0), 4.0);
let arcs = [(0.3, 2.0), (2.3, 2.5), (4.8, TAU - 4.5)];
let ccw = Loop2 { segs: arcs.iter().map(|&(start, sweep)| Seg2::Arc { center: c, radius: rad, start, sweep }).collect() };
@@ -245,28 +248,14 @@ fn clockwise_loops_and_a_reseamed_circle_keep_their_numbers() {
for k in 0..7 {
assert_eq!(got.iter().filter(|o| **o == [P(k)]).count(), 1, "segment {k}");
}
// Revolved, the circle restarts at 0.4142 rad: the half from there runs over the first two
// arcs (6, 5), the other half over all three (it ends past the first arc's start).
let first_half = |q: Vec2| (q - c).angle().rem_euclid(TAU);
// Revolved, each arc turns into a face of its own, as the rectangle's sides do.
for angle in [FRAC_PI_2, TAU] {
let b = crate::revolve(&SIDE, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, angle).unwrap().pop().unwrap();
let got = origins(&b);
let want = expected_revolution(&b, &r);
let (_, centres) = mesh_points(&b);
let mut halves = [0, 0];
for ((g, w), centres) in got.iter().zip(&want).zip(&centres) {
let flat: Vec<Vec2> = centres.iter().map(|p| Vec2::new(p.x.hypot(p.y), p.z)).collect();
if flat.iter().all(|q| (q.dist(c) - rad).abs() < 0.1) {
let first = flat.iter().all(|q| (0.4142 - 1e-3..=0.4142 + PI + 1e-3).contains(&first_half(*q)));
assert_eq!(g, &if first { vec![P(5), P(6)] } else { vec![P(4), P(5), P(6)] }, "angle {angle}");
halves[usize::from(!first)] += 1;
} else {
assert_eq!(g, w, "angle {angle}");
}
assert_eq!(got, expected_revolution(&b, &r), "angle {angle}");
for k in 0..7 {
assert_eq!(got.iter().filter(|o| **o == [P(k)]).count(), 1, "segment {k} at angle {angle}");
}
// Truck turns each half in as many parts as each rectangle side.
let parts = got.iter().filter(|o| **o == [P(0)]).count();
assert_eq!(halves, [parts, parts], "angle {angle}");
}
}
@@ -278,11 +267,11 @@ fn revolve_with_an_edge_on_the_axis() {
let got = origins(&partial);
assert_eq!(got, expected_revolution(&partial, &r));
assert_eq!(got.len(), 5);
// A full turn: the radial segments 0 and 2 sweep discs, segment 1 the cylinder (three parts).
// A full turn: the radial segments 0 and 2 sweep discs, segment 1 the cylinder (one face).
let full = crate::revolve(&SIDE, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, TAU).unwrap().pop().unwrap();
let got = origins(&full);
assert_eq!(got, expected_revolution(&full, &r));
assert_eq!(got.iter().filter(|o| **o == [P(1)]).count(), 3);
assert_eq!(got.iter().filter(|o| **o == [P(1)]).count(), 1);
assert_eq!(got.iter().filter(|o| **o == [P(0)] || **o == [P(2)]).count(), 2);
// A cone: the slanted segment ends on the axis (the apex), the base disc.
let tri = Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(0.0, 8.0)]), holes: vec![] };
@@ -292,7 +281,7 @@ fn revolve_with_an_edge_on_the_axis() {
let turned = Region2 { outer: rotated(&tri.outer.reversed(), 1), holes: vec![] };
let cone = crate::revolve(&SIDE, std::slice::from_ref(&turned), Vec2::ZERO, Vec2::Y, TAU).unwrap().pop().unwrap();
assert_eq!(origins(&cone), expected_revolution(&cone, &turned));
// A half disc: a sphere, every patch from the arc (segment 1 here).
// A half disc: a sphere, its one face from the arc (segment 1 here).
let half = Region2 {
outer: Loop2 {
segs: vec![
@@ -303,7 +292,7 @@ fn revolve_with_an_edge_on_the_axis() {
holes: vec![],
};
let ball = crate::revolve(&SIDE, std::slice::from_ref(&half), Vec2::ZERO, Vec2::Y, TAU).unwrap().pop().unwrap();
assert_eq!(origins(&ball), vec![vec![P(1)]; 6]);
assert_eq!(origins(&ball), vec![vec![P(1)]]);
}
#[test]
+8 -3
View File
@@ -31,7 +31,9 @@ fn cylinder_measures_match_analytic() {
let a = 2.0 * PI * 225.0 + 2.0 * PI * 15.0 * 50.0;
assert!(rel(m.volume, v) < 5e-4, "{} vs {v}", m.volume);
assert!(rel(m.area, a) < 5e-4, "{} vs {a}", m.area);
assert_eq!((m.merged.faces, m.merged.edges, m.merged.vertices), (3, 2, 2), "{:?}", m.merged);
// Two closed circles and the side's seam (the ladder's count: each whole curve once, and each
// cylinder's seam); a vertex on each circle.
assert_eq!((m.merged.faces, m.merged.edges, m.merged.vertices), (3, 3, 2), "{:?}", m.merged);
assert_eq!(m.merged.face_types.get("cylinder"), Some(&1));
}
@@ -1001,13 +1003,16 @@ fn sphere_booleans() {
let s = sphere(Vec3::ZERO, 15.0).unwrap();
let m = measure(&s).unwrap();
assert!(rel(m.volume, 4.0 / 3.0 * PI * 3375.0) < 1e-3, "{}", m.volume);
assert_eq!((m.merged.faces, m.merged.edges), (1, 0));
// One spherical face, closed along its seam from pole to pole.
assert_eq!((m.merged.faces, m.merged.edges), (1, 1));
// A ball cut out of the top face of a slab (oracle 14).
let b = box_solid(Vec3::new(-25.0, -25.0, -20.0), Vec3::new(25.0, 25.0, 0.0)).unwrap();
let c = boolean(&b, &s, BoolOp::Cut).unwrap().unwrap();
let m = measure(&c).unwrap();
assert!(rel(m.volume, 50.0 * 50.0 * 20.0 - 2.0 / 3.0 * PI * 3375.0) < 1e-3, "{}", m.volume);
assert_eq!((m.merged.faces, m.merged.edges, m.merged.vertices), (7, 13, 9));
// The box's 6 faces and the sphere's; its 12 edges, the rim circle and the sphere's seam down
// to its pole; its 8 corners, the rim's vertex and the pole.
assert_eq!((m.merged.faces, m.merged.edges, m.merged.vertices), (7, 14, 10));
}
#[test]