* 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 <[email protected]> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
312 lines
15 KiB
Rust
312 lines
15 KiB
Rust
use crate::{FaceSource, extrude, transform_matrix};
|
|
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
|
|
|
fn assert_exact_planes(body: &crate::Body) {
|
|
let mesh = body.tessellate(0.001).unwrap();
|
|
for (index, _) in body.face_sources().unwrap().iter().enumerate() {
|
|
let Some(plane) = body.face_plane(index) else { continue };
|
|
let mut count = 0;
|
|
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
|
if *face as usize != index {
|
|
continue;
|
|
}
|
|
for point in mesh.tri(triangle).unwrap() {
|
|
assert!((point - plane.origin).dot(plane.normal()).abs() < 1e-7);
|
|
count += 1;
|
|
}
|
|
}
|
|
assert!(count > 0);
|
|
let normal = body.faces(0.001).unwrap()[index].plane_normal.unwrap();
|
|
assert!(normal.dot(plane.normal()) > 0.999999);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn box_certificates_are_six_construction_roles_with_dimension_stable_axes() {
|
|
for (a, b) in [
|
|
(Vec3::new(2., 3., 4.), Vec3::new(12., 23., 34.)),
|
|
(Vec3::new(12., 23., 34.), Vec3::new(2., 3., 4.)),
|
|
(Vec3::new(2., -8., 4.), Vec3::new(22., 43., 54.)),
|
|
] {
|
|
let body = crate::box_solid(a, b).unwrap().with_box_certificate(42).unwrap();
|
|
let mut roles = std::collections::HashSet::new();
|
|
for (index, source) in body.face_sources().unwrap().iter().enumerate() {
|
|
let Some(FaceSource::Box { producer, axis, high, plane }) = source else { panic!("uncertified box face") };
|
|
assert_eq!(*producer, Some(42));
|
|
assert!(roles.insert((*axis, *high)));
|
|
let unit = [Vec3::X, Vec3::Y, Vec3::Z][*axis as usize];
|
|
let bound = if *high { a.max(b) } else { a.min(b) };
|
|
assert_eq!(plane.origin, unit * bound.dot(unit));
|
|
assert!(plane.normal().dist(unit * if *high { 1. } else { -1. }) < 1e-12);
|
|
assert_eq!(body.face_plane(index), Some(*plane));
|
|
}
|
|
assert_eq!(roles.len(), 6);
|
|
assert_exact_planes(&body);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cap_roles_follow_exact_offset_planes_and_canonical_profile_memberships() {
|
|
let outer = Loop2::polygon(&[Vec2::new(-10., -10.), Vec2::new(10., -10.), Vec2::new(10., 10.), Vec2::new(-10., 10.)]);
|
|
let h1 = Loop2::circle(Vec2::new(-4., 0.), 1.);
|
|
let h2 = Loop2::circle(Vec2::new(4., 0.), 2.);
|
|
let plane = Plane::new(Vec3::new(30., 40., 50.), Vec3::Y, Vec3::Z).unwrap();
|
|
for (start, end) in [(-9., -2.), (7., -3.), (2., 11.)] {
|
|
for holes in [vec![h1.clone(), h2.clone()], vec![h2.clone().reversed(), h1.clone().reversed()]] {
|
|
let region = Region2 { outer: outer.clone().reversed(), holes };
|
|
let raw = extrude(&plane, std::slice::from_ref(®ion), start, end).unwrap().remove(0);
|
|
let signatures: Vec<_> = raw
|
|
.face_sources()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|source| match source {
|
|
Some(FaceSource::ExtrudeCap { .. }) => {
|
|
Some((vec!["l4".into(), "l1".into(), "l3".into(), "l2".into()], vec![vec!["c2".into()], vec!["c1".into()]]))
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
let body = raw.with_extrude_cap_certificates(42, 11, &signatures).unwrap();
|
|
let mut caps = std::collections::HashSet::new();
|
|
for source in body.face_sources().unwrap().iter().flatten() {
|
|
if let FaceSource::CertifiedExtrudeCap { producer, sketch, high, outer, holes, plane: cap } = source {
|
|
assert_eq!((*producer, *sketch), (42, 11));
|
|
assert!(caps.insert(*high));
|
|
assert_eq!(outer, &["l1", "l2", "l3", "l4"]);
|
|
assert_eq!(holes, &[vec!["c1"], vec!["c2"]]);
|
|
assert!(cap.origin.dist(plane.origin + plane.normal() * if *high { start.max(end) } else { start.min(end) }) < 1e-10);
|
|
assert!(cap.normal().dot(plane.normal()) * if *high { 1. } else { -1. } > 0.999999);
|
|
}
|
|
}
|
|
assert_eq!(caps.len(), 2);
|
|
assert_exact_planes(&body);
|
|
let expected = (400. - 5. * std::f64::consts::PI) * (end - start).abs();
|
|
assert!((body.tessellate(1e-4).unwrap().measure().volume - expected).abs() < 0.03);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn construction_planes_follow_reflected_and_sheared_affine_images() {
|
|
let body = crate::box_solid(Vec3::new(2., 3., 4.), Vec3::new(12., 23., 34.)).unwrap().with_box_certificate(42).unwrap();
|
|
for m in [
|
|
[[0., 2., 0., 0.], [-1., 0., 0., 0.], [0., 0., 3., 0.], [25., 30., 40., 1.]],
|
|
[[-1., 0., 0., 0.], [0.4, 2., 0., 0.], [0., 0.3, 1., 0.], [8., 9., 10., 1.]],
|
|
] {
|
|
let placed = transform_matrix(&body, m).unwrap();
|
|
for (original, transformed) in body.face_sources().unwrap().iter().zip(placed.face_sources().unwrap()) {
|
|
let (
|
|
Some(FaceSource::Box { producer: a, axis: aa, high: ah, plane: ap }),
|
|
Some(FaceSource::Box { producer: b, axis: ba, high: bh, plane: bp }),
|
|
) = (original, transformed)
|
|
else {
|
|
panic!("box identity changed")
|
|
};
|
|
assert_eq!((a, aa, ah), (b, ba, bh));
|
|
let p = ap.origin;
|
|
let expected = Vec3::new(
|
|
m[0][0] * p.x + m[1][0] * p.y + m[2][0] * p.z + m[3][0],
|
|
m[0][1] * p.x + m[1][1] * p.y + m[2][1] * p.z + m[3][1],
|
|
m[0][2] * p.x + m[1][2] * p.y + m[2][2] * p.z + m[3][2],
|
|
);
|
|
assert!(bp.origin.dist(expected) < 1e-10);
|
|
}
|
|
assert_exact_planes(&placed);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn material_changes_preserve_only_unchanged_planar_support_certificates() {
|
|
let base = crate::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap().with_box_certificate(42).unwrap();
|
|
let tool = crate::box_solid(Vec3::new(5., -1., -1.), Vec3::new(11., 11., 11.)).unwrap();
|
|
let cut = crate::boolean(&base, &tool, crate::BoolOp::Cut).unwrap().unwrap();
|
|
assert!((cut.tessellate(1e-4).unwrap().measure().volume - 500.).abs() < 1e-8);
|
|
// Unchanged uniquely certified supports retain their role; a newly cut
|
|
// support must never inherit the removed opposite wall's source identity.
|
|
let mut retained = 0;
|
|
let mut new_wall = 0;
|
|
for index in 0..cut.face_count() {
|
|
let actual = cut.planar_face(index).unwrap();
|
|
let source = cut.face_sources().and_then(|s| s.get(index)).and_then(Option::as_ref);
|
|
if let Some(FaceSource::Box { producer: Some(42), axis, high, .. }) = source {
|
|
assert!(!(*axis == 0 && *high));
|
|
let canonical = cut.face_plane(index).unwrap();
|
|
assert!(actual.normal().dot(canonical.normal()) > 1. - 1e-10);
|
|
assert!(actual.height(canonical.origin).abs() < 1e-10);
|
|
retained += 1;
|
|
}
|
|
if actual.normal().x > 0.9 {
|
|
assert!((actual.origin.x - 5.).abs() < 1e-10);
|
|
assert!(!matches!(source, Some(FaceSource::Box { producer: Some(_), .. })));
|
|
new_wall += 1;
|
|
}
|
|
}
|
|
assert_eq!((retained, new_wall), (5, 1));
|
|
}
|
|
|
|
#[test]
|
|
fn canonical_revolve_axis_is_per_profile_and_independent_of_input_axis_sign() {
|
|
for x in [-4., 4.] {
|
|
let region = Region2 {
|
|
outer: Loop2::polygon(&[Vec2::new(x - 1., 0.), Vec2::new(x + 1., 0.), Vec2::new(x + 1., 2.), Vec2::new(x - 1., 2.)]),
|
|
holes: vec![],
|
|
};
|
|
let axis = crate::canonical_revolve_axis(&Plane::XY, ®ion, Vec2::default(), Vec2::Y).unwrap();
|
|
assert_eq!(axis, crate::canonical_revolve_axis(&Plane::XY, ®ion, Vec2::default(), -Vec2::Y).unwrap());
|
|
assert!(axis.cross(Plane::XY.to_world(region.centroid())).dot(Vec3::Z) > 0.);
|
|
assert_eq!(axis, Vec3::Y * if x < 0. { 1. } else { -1. });
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn linear_side_frame_stays_fixed_when_endpoints_and_depth_change() {
|
|
for (lo, hi, start, end) in [(3., 23., 0., 7.), (-9., 43., -3., 19.)] {
|
|
let region =
|
|
Region2 { outer: Loop2::polygon(&[Vec2::new(2., lo), Vec2::new(12., lo), Vec2::new(12., hi), Vec2::new(2., hi)]), holes: vec![] };
|
|
let body = extrude(&Plane::XY, &[region], start, end).unwrap().remove(0);
|
|
let plane = body
|
|
.face_sources()
|
|
.unwrap()
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(index, source)| match source {
|
|
Some(FaceSource::ExtrudeSide { segment: Seg2::Line { a, b }, .. }) if a.x == 2. && b.x == 2. => body.face_plane(index),
|
|
_ => None,
|
|
})
|
|
.unwrap();
|
|
assert_eq!(plane.origin, Vec3::X * 2.);
|
|
assert_eq!(plane.x, -Vec3::Y);
|
|
assert_eq!(plane.normal(), -Vec3::X);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reflected_hole_walls_and_low_caps_keep_exact_outward_planes() {
|
|
let outer = Loop2::polygon(&[Vec2::new(-10., -10.), Vec2::new(10., -10.), Vec2::new(10., 10.), Vec2::new(-10., 10.)]);
|
|
let hole = Loop2::polygon(&[Vec2::new(-3., -2.), Vec2::new(3., -2.), Vec2::new(3., 2.), Vec2::new(-3., 2.)]);
|
|
let body = extrude(&Plane::XY, &[Region2 { outer, holes: vec![hole] }], -4., 7.).unwrap().remove(0);
|
|
let reflected = transform_matrix(&body, [[-1., 0., 0., 0.], [0., 2., 0., 0.], [0.2, 0., 1., 0.], [30., 40., 50., 1.]]).unwrap();
|
|
assert_exact_planes(&reflected);
|
|
assert_eq!(
|
|
reflected.face_sources().unwrap().iter().filter(|source| matches!(source, Some(FaceSource::ExtrudeSide { plane: Some(_), .. }))).count(),
|
|
8
|
|
);
|
|
assert_eq!(
|
|
reflected.face_sources().unwrap().iter().filter(|source| matches!(source, Some(FaceSource::ExtrudeCap { high: false, .. }))).count(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn side_basis_and_attached_coordinates_stay_continuous_through_vertical() {
|
|
let mut previous = None;
|
|
for epsilon in [1e-6, -1e-6] {
|
|
let region =
|
|
Region2 { outer: Loop2::polygon(&[Vec2::ZERO, Vec2::new(epsilon, 10.), Vec2::new(-5., 10.), Vec2::new(-5., 0.)]), holes: vec![] };
|
|
let body = extrude(&Plane::XY, &[region], 0., 5.).unwrap().remove(0);
|
|
let plane = body
|
|
.face_sources()
|
|
.unwrap()
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(index, source)| match source {
|
|
Some(FaceSource::ExtrudeSide { segment: Seg2::Line { a, b }, .. }) if a.dist(Vec2::ZERO) < 1e-12 && b.y == 10. && b.x == epsilon => {
|
|
body.face_plane(index)
|
|
}
|
|
_ => None,
|
|
})
|
|
.unwrap();
|
|
assert!(plane.x.dot(Vec3::Y) > 0.999999);
|
|
assert_eq!(plane.y, Vec3::Z);
|
|
let attached = plane.to_world(Vec2::new(3., 2.));
|
|
assert!(attached.dist(Vec3::new(epsilon * 0.3, 3., 2.)) < 1e-10);
|
|
if let Some(old) = previous {
|
|
assert!(attached.dist(old) < 1e-6);
|
|
}
|
|
previous = Some(attached);
|
|
assert_exact_planes(&body);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn constructed_annulus_sides_follow_exact_source_edges() {
|
|
let ring = Region2 { outer: Loop2::circle(Vec2::default(), 10.), holes: vec![Loop2::circle(Vec2::default(), 9.).reversed()] };
|
|
for (start, end) in [(0., 7.), (-7., 0.), (-7., 7.)] {
|
|
let bodies = extrude(&Plane::XY, std::slice::from_ref(&ring), start, end).unwrap();
|
|
let body = &bodies[0];
|
|
let sources = body.face_sources().unwrap();
|
|
assert_eq!(sources.len(), body.face_count());
|
|
let mesh = body.tessellate(0.001).unwrap();
|
|
let mut inner = 0;
|
|
let mut outer = 0;
|
|
for (index, source) in sources.iter().enumerate() {
|
|
let Some(FaceSource::ExtrudeSide { segment: Seg2::Arc { radius, .. }, .. }) = source else { continue };
|
|
if (*radius - 10.).abs() < 1e-12 {
|
|
outer += 1;
|
|
} else {
|
|
assert!((*radius - 9.).abs() < 1e-12);
|
|
inner += 1;
|
|
}
|
|
let mut vertices = 0;
|
|
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
|
if *face as usize != index {
|
|
continue;
|
|
}
|
|
for point in mesh.tri(triangle).unwrap() {
|
|
assert!((point.x.hypot(point.y) - radius).abs() < 1e-8, "face {index}, source {radius}, {point:?}");
|
|
vertices += 1;
|
|
}
|
|
}
|
|
assert!(vertices > 0);
|
|
}
|
|
assert_eq!((outer, inner), (2, 2), "full circles split into two actual edges each");
|
|
assert_eq!(sources.iter().filter(|source| matches!(source, Some(FaceSource::ExtrudeCap { .. }))).count(), 2);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn constructed_side_identity_survives_affine_placement_without_recognition() {
|
|
let region = Region2 { outer: Loop2::circle(Vec2::default(), 10.), holes: vec![] };
|
|
let bodies = extrude(&Plane::named("XY").unwrap(), &[region], 0., 7.).unwrap();
|
|
let source = &bodies[0];
|
|
let placed = transform_matrix(source, [[0., 1., 0., 0.], [-1., 0., 0., 0.], [0., 0., 1., 0.], [25., 30., 40., 1.]]).unwrap();
|
|
for (original, placed) in source.face_sources().unwrap().iter().zip(placed.face_sources().unwrap()) {
|
|
match (original, placed) {
|
|
(Some(FaceSource::ExtrudeSide { segment: a, .. }), Some(FaceSource::ExtrudeSide { segment: b, .. })) => assert_eq!(a, b),
|
|
(Some(FaceSource::ExtrudeCap { high: a, region: ar, .. }), Some(FaceSource::ExtrudeCap { high: b, region: br, .. })) => {
|
|
assert_eq!(a, b);
|
|
assert_eq!(ar, br)
|
|
}
|
|
_ => panic!("source role changed"),
|
|
}
|
|
}
|
|
assert!((placed.size() - source.size()).abs() < 1e-8);
|
|
assert!(placed.faces(0.001).unwrap().iter().all(|face| face.centroid.dist(Vec3::new(25., 30., 43.5)) < 11.));
|
|
}
|
|
|
|
#[test]
|
|
fn constructed_large_arc_split_and_reversal_preserve_one_source_segment() {
|
|
let arc = Seg2::Arc { center: Vec2::default(), radius: 10., start: 0., sweep: std::f64::consts::PI * 5. / 3. };
|
|
let outline = Loop2 { segs: vec![arc, Seg2::Line { a: arc.end(), b: arc.start() }] };
|
|
for outline in [outline.clone(), outline.reversed()] {
|
|
let body = extrude(&Plane::XY, &[Region2 { outer: outline, holes: vec![] }], 0., 7.).unwrap().remove(0);
|
|
let sources = body.face_sources().unwrap();
|
|
let arcs: Vec<_> = sources
|
|
.iter()
|
|
.filter_map(|source| match source {
|
|
Some(FaceSource::ExtrudeSide { segment: Seg2::Arc { radius, sweep, .. }, .. }) => Some((radius, sweep)),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(arcs.len(), 2, "large input arc explicitly creates two actual boundary edges");
|
|
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.tessellate(0.001).unwrap();
|
|
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
|
if matches!(sources[*face as usize], Some(FaceSource::ExtrudeSide { segment: Seg2::Arc { .. }, .. })) {
|
|
assert!(mesh.tri(triangle).unwrap().iter().all(|point| (point.x.hypot(point.y) - 10.).abs() < 1e-7));
|
|
}
|
|
}
|
|
}
|
|
}
|