use std::collections::BTreeMap;
use arris::geom::{FrameLaw, NurbsCurve, Path, PathSegment, Profile, ProfileLoop, ProfileSegment};
use arris::io::step::ReadOptions;
use arris::math::{Axis, Control, Frame, Point2, Point3, Vec3};
use arris::topo::Model;
use arris_debug::unmetered::{
cut, extrude, fillet, mass_properties, primitive_box, primitive_cylinder, revolve, step_read,
tessellate,
};
use sha2::{Digest, Sha256};
const DIGESTS: &str = include_str!("determinism/digests.json");
fn sha256(bytes: &[u8]) -> String {
Sha256::digest(bytes)
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
fn record(m: &Model, body: arris::topo::Body, prov: &arris::topo::Provenance) -> serde_json::Value {
let bytes = arris::io::body::write(m, body, prov).expect("a built body writes");
let hex = sha256(&bytes);
let mp = mass_properties(m, body).unwrap_or_else(|e| panic!("{e:?}"));
serde_json::json!({
"body_sha256": hex,
"volume": format!("{:?}", mp.volume),
"area": format!("{:?}", mp.area),
})
}
fn involute(rb: f64, t: f64, angle: f64, mirror: bool) -> Point2 {
let (st, ct) = (libm::sin(t), libm::cos(t));
let (x, y) = (rb * (ct + t * st), rb * (st - t * ct));
let y = if mirror { -y } else { y };
let (sa, ca) = (libm::sin(angle), libm::cos(angle));
Point2::new(ca * x - sa * y, sa * x + ca * y)
}
fn polar(r: f64, angle: f64) -> Point2 {
Point2::new(r * libm::cos(angle), r * libm::sin(angle))
}
fn gear_profile() -> Profile {
const TEETH: usize = 20;
const FLANK_ARCS: usize = 4;
let module = 1.0;
let pitch = module * TEETH as f64 / 2.0;
let alpha = 20.0_f64.to_radians();
let rb = pitch * libm::cos(alpha);
let (ra, rf) = (pitch + module, pitch - 1.25 * module);
let t_max = libm::sqrt((ra / rb) * (ra / rb) - 1.0);
let beta = core::f64::consts::PI / (2.0 * TEETH as f64) + (libm::tan(alpha) - alpha);
let step = core::f64::consts::TAU / TEETH as f64;
let ts: Vec<f64> = (0..=2 * FLANK_ARCS)
.map(|i| t_max * i as f64 / (2 * FLANK_ARCS) as f64)
.collect();
let start = polar(rf, -beta);
let mut segments = Vec::with_capacity(TEETH * 12);
for k in 0..TEETH {
let centre = k as f64 * step;
let (right, left) = (centre - beta, centre + beta);
if k > 0 {
let from = (centre - step) + beta;
segments.push(ProfileSegment::ArcTo {
to: polar(rf, right),
via: polar(rf, (from + right) / 2.0),
});
}
segments.push(ProfileSegment::LineTo(involute(rb, 0.0, right, false)));
for i in 0..FLANK_ARCS {
segments.push(ProfileSegment::ArcTo {
to: involute(rb, ts[2 * i + 2], right, false),
via: involute(rb, ts[2 * i + 1], right, false),
});
}
segments.push(ProfileSegment::ArcTo {
to: involute(rb, t_max, left, true),
via: polar(ra, centre),
});
for i in (0..FLANK_ARCS).rev() {
segments.push(ProfileSegment::ArcTo {
to: involute(rb, ts[2 * i], left, true),
via: involute(rb, ts[2 * i + 1], left, true),
});
}
segments.push(ProfileSegment::LineTo(polar(rf, left)));
}
let from = (TEETH - 1) as f64 * step + beta;
segments.push(ProfileSegment::ArcTo {
to: start,
via: polar(rf, (from + core::f64::consts::TAU - beta) / 2.0),
});
Profile {
plane: Frame::world(),
outer: ProfileLoop::Path { start, segments },
holes: Vec::new(),
}
}
fn circle(radius: f64) -> Profile {
Profile {
plane: Frame::world(),
outer: ProfileLoop::Circle {
center: Point2::new(0.0, 0.0),
radius,
},
holes: Vec::new(),
}
}
fn revolve_recipe(m: &mut Model) -> (arris::topo::Body, arris::topo::Provenance) {
let p = |x, y| Point2::new(x, y);
let profile = Profile {
plane: Frame::world(),
outer: ProfileLoop::Path {
start: p(2.0, 0.0),
segments: vec![
ProfileSegment::LineTo(p(4.0, 0.0)),
ProfileSegment::LineTo(p(4.0, 3.0)),
ProfileSegment::ArcTo {
to: p(2.0, 3.0),
via: p(3.0, 3.5),
},
ProfileSegment::LineTo(p(2.0, 0.0)),
],
},
holes: Vec::new(),
};
let axis = Axis::new(Point3::origin(), Vec3::y()).expect("the y axis is an axis");
revolve(m, &profile, axis, 5.0).expect("the profile revolves")
}
fn boolean_recipe(m: &mut Model) -> (arris::topo::Body, arris::topo::Provenance) {
let (a, _) =
primitive_cylinder(m, Axis::z_at(Point3::origin()), 2.0, 6.0).expect("the cylinder builds");
let along_x = Axis::new(Point3::new(-4.0, 0.3, 3.0), Vec3::x()).expect("an axis");
let (b, _) = primitive_cylinder(m, along_x, 0.7, 8.0).expect("the cylinder builds");
cut(m, a, b).expect("the cylinders cut")
}
fn fillet_recipe(m: &mut Model) -> (arris::topo::Body, arris::topo::Provenance) {
let (b, _) =
primitive_box(m, Point3::origin(), Point3::new(4.0, 3.0, 2.0)).expect("the box builds");
let edges = m.edges(b).expect("the box has edges");
fillet(m, b, &edges, 0.4).expect("the box fillets")
}
fn sweep_recipe(m: &mut Model) -> (arris::topo::Body, arris::topo::Provenance) {
let (r, a) = (8.0, core::f64::consts::FRAC_PI_3);
let at = |t: f64| Point3::new(r * (1.0 - libm::cos(t)), 0.0, 10.0 + r * libm::sin(t));
let path = Path {
start: Point3::origin(),
segments: vec![
PathSegment::LineTo(Point3::new(0.0, 0.0, 10.0)),
PathSegment::ArcTo {
via: at(a / 2.0),
to: at(a),
},
],
};
arris::ops::sweep(
m,
&circle(1.5),
&path,
FrameLaw::RotationMinimizing,
&Control::NONE,
)
.expect("the pipe sweeps")
}
fn nurbs_sweep_recipe(m: &mut Model) -> (arris::topo::Body, arris::topo::Provenance) {
let curve = NurbsCurve::new(
3,
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 10.0),
Point3::new(10.0, 8.0, 18.0),
Point3::new(20.0, 12.0, 20.0),
],
vec![1.0; 4],
)
.expect("the curve is valid");
let path = Path {
start: Point3::origin(),
segments: vec![PathSegment::Nurbs(curve)],
};
arris::ops::sweep(
m,
&circle(1.5),
&path,
FrameLaw::RotationMinimizing,
&Control::NONE,
)
.expect("the wire sweeps")
}
fn step_recipe(m: &mut Model) -> (arris::topo::Body, arris::topo::Provenance) {
const HALF_BALL: &str =
include_str!("../../../tests/fixtures/real/edge-through-sphere-pole/half-ball.stp");
let read = step_read(m, HALF_BALL, &ReadOptions::default()).expect("the half ball reads");
let solid = read
.solids
.into_iter()
.next()
.expect("the half ball has a solid");
let body = solid.result.expect("the half ball builds");
(body.body, body.provenance)
}
fn tessellation_record(m: &mut Model) -> serde_json::Value {
let (body, _) = boolean_recipe(m);
let mesh = tessellate(m, body, 0.01).expect("the body tessellates");
let mut bytes = Vec::new();
for p in mesh.positions().iter().flatten() {
bytes.extend_from_slice(&p.to_bits().to_le_bytes());
}
for i in mesh.triangles().iter().flatten() {
bytes.extend_from_slice(&i.to_le_bytes());
}
serde_json::json!({
"mesh_sha256": sha256(&bytes),
"triangles": mesh.triangles().len(),
})
}
type Recipe = fn(&mut Model) -> (arris::topo::Body, arris::topo::Provenance);
fn recipes() -> BTreeMap<&'static str, serde_json::Value> {
let mut out = BTreeMap::new();
let mut m = Model::default();
let (body, prov) =
extrude(&mut m, &gear_profile(), Vec3::new(0.0, 0.0, 1.0), 8.0).expect("the gear extrudes");
out.insert("gear-extrude", record(&m, body, &prov));
let builds: [(&'static str, Recipe); 6] = [
("revolve", revolve_recipe),
("boolean-cut", boolean_recipe),
("fillet", fillet_recipe),
("sweep-analytic", sweep_recipe),
("sweep-nurbs", nurbs_sweep_recipe),
("step-read", step_recipe),
];
for (name, build) in builds {
let mut m = Model::default();
let (body, prov) = build(&mut m);
out.insert(name, record(&m, body, &prov));
}
out.insert("tessellation", tessellation_record(&mut Model::default()));
out
}
#[test]
fn the_gear_has_40_lines_and_200_arcs() {
let ProfileLoop::Path { segments, .. } = gear_profile().outer else {
panic!("the gear's outline is a path");
};
let arcs = segments
.iter()
.filter(|s| matches!(s, ProfileSegment::ArcTo { .. }))
.count();
assert_eq!((segments.len() - arcs, arcs), (40, 200));
}
#[test]
fn every_recipe_matches_its_digest() {
let got = serde_json::to_value(recipes()).expect("records serialise");
#[cfg(not(target_arch = "wasm32"))]
if std::env::var_os("ARRIS_BLESS").is_some() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/determinism/digests.json"
);
let text = serde_json::to_string_pretty(&got).expect("records print") + "\n";
std::fs::write(path, text).expect("the digests file writes");
return;
}
let want: serde_json::Value = serde_json::from_str(DIGESTS).expect("digests.json parses");
assert_eq!(
got,
want,
"a body differs from its committed digest on {}",
std::env::consts::ARCH
);
}