forked from 1b-agent/fieldcraft-upstream
mesh: platform-independent maths (libm) so meshes are bit-identical on macOS and Linux
The system maths library's sin, cos, atan2, log … are not correctly rounded and differ between platforms: mesh.hex.generate gave the plate of the strong-relax test a different mesh on Linux than on macOS (nodes from 2809 on), so the test passed on a Mac and failed in CI. Every transcendental call in fieldcraft-mesh now goes through the libm crate (a pure-Rust port of musl's), which gives the same bits everywhere. Part of #438. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
f297ffca3a
commit
700a7e3d10
15 files changed
+41
-30
No files matched your search
@@ -372,7 +372,7 @@ fn composite_tags(surface: &BoundarySurface, surfaces: &[Option<Surface>]) -> Ve
|
||||
unit(cross(sub(b, a), sub(c, a)))
|
||||
})
|
||||
};
|
||||
let cos_min = SMOOTH_ANGLE.cos();
|
||||
let cos_min = libm::cos(SMOOTH_ANGLE);
|
||||
// Per pair of faces (low tag, high tag): whether every segment between them is smooth.
|
||||
let mut pairs: BTreeMap<(u32, u32), bool> = BTreeMap::new();
|
||||
for fe in surface.feature_edges().iter().filter(|e| e.kind == FeatureKind::FaceBoundary) {
|
||||
|
||||
@@ -166,7 +166,7 @@ fn sub(a: Q, b: Q) -> Q {
|
||||
}
|
||||
|
||||
fn len(a: Q) -> f64 {
|
||||
a[0].hypot(a[1])
|
||||
libm::hypot(a[0], a[1])
|
||||
}
|
||||
|
||||
fn cross(a: Q, b: Q) -> f64 {
|
||||
@@ -186,14 +186,17 @@ fn unit(a: Q) -> Option<Q> {
|
||||
/// region on the left: π on a straight line, less at a convex corner, more at a concave one.
|
||||
fn interior(a: Q, o: Q, b: Q) -> f64 {
|
||||
let (u, v) = (sub(o, a), sub(b, o));
|
||||
std::f64::consts::PI - cross(u, v).atan2(dot(u, v))
|
||||
std::f64::consts::PI - crate::vec3::atan2(cross(u, v), dot(u, v))
|
||||
}
|
||||
|
||||
/// The inward bisector at a corner of interior angle `theta` whose outgoing edge runs along unit
|
||||
/// `w`: `w` turned left by half the angle.
|
||||
fn bisector(w: Q, theta: f64) -> Q {
|
||||
let half = 0.5 * theta;
|
||||
[w[0] * half.cos() - w[1] * half.sin(), w[0] * half.sin() + w[1] * half.cos()]
|
||||
{
|
||||
let (s, c) = (libm::sin(half), libm::cos(half));
|
||||
[w[0] * c - w[1] * s, w[0] * s + w[1] * c]
|
||||
}
|
||||
}
|
||||
|
||||
fn centroid(pts: &[Q]) -> Q {
|
||||
@@ -1320,7 +1323,7 @@ mod tests {
|
||||
/// The half crank's cross-section (y ≤ 0) as the extrusion projects it: the outline, the
|
||||
/// bosses' circles where they rise above the arm, and the pocket's outline.
|
||||
fn crank_half() -> (Profile, Vec<Vec<Q>>) {
|
||||
let deg = |p: Q, c: Q| (p[1] - c[1]).atan2(p[0] - c[0]).to_degrees();
|
||||
let deg = |p: Q, c: Q| crate::vec3::atan2(p[1] - c[1], p[0] - c[0]).to_degrees();
|
||||
let spindle = [0.0, 0.0];
|
||||
let pedal = [170.0, 0.0];
|
||||
let j1 = [13.455063072993756, -18.65372021077189];
|
||||
@@ -1344,7 +1347,7 @@ mod tests {
|
||||
};
|
||||
let outer = vec![
|
||||
a(spindle, 23.0, [-23.0, 0.0], j1, true),
|
||||
a(c1, (j1[0] - c1[0]).hypot(j1[1] - c1[1]), j1, r1, false),
|
||||
a(c1, libm::hypot(j1[0] - c1[0], j1[1] - c1[1]), j1, r1, false),
|
||||
line(r1, r2),
|
||||
a(c2, 6.0, r2, j2, false),
|
||||
a(pedal, 15.0, j2, [185.0, 0.0], true),
|
||||
|
||||
@@ -410,7 +410,7 @@ fn section_boundary(quads: &[[u32; 4]]) -> Result<SectionBoundary> {
|
||||
/// The layer parameters `0 = t_0 < … < t_n = 1`, thicknesses in geometric progression with the
|
||||
/// last over the first equal to `bias`.
|
||||
pub(crate) fn parameters(n: usize, bias: f64) -> Vec<f64> {
|
||||
let r = if n > 1 { bias.powf(1.0 / (n - 1) as f64) } else { 1.0 };
|
||||
let r = if n > 1 { libm::pow(bias, 1.0 / (n - 1) as f64) } else { 1.0 };
|
||||
let mut out = Vec::with_capacity(n + 1);
|
||||
let (mut sum, mut h) = (0.0, 1.0);
|
||||
out.push(0.0);
|
||||
|
||||
@@ -101,7 +101,7 @@ impl Surface {
|
||||
let h = dot(w, axis);
|
||||
let radial = unit(sub(w, scale(axis, h)))?;
|
||||
let rho = norm(sub(w, scale(axis, h)));
|
||||
let (c, s) = (half_angle.cos(), half_angle.sin());
|
||||
let (c, s) = (libm::cos(half_angle), libm::sin(half_angle));
|
||||
let t = (h * c + rho * s).max(0.0);
|
||||
add(apex, add(scale(axis, t * c), scale(radial, t * s)))
|
||||
}
|
||||
@@ -128,7 +128,7 @@ impl Surface {
|
||||
Surface::Cone { apex, axis, half_angle } => {
|
||||
let w = sub(p, apex);
|
||||
let radial = unit(sub(w, scale(axis, dot(w, axis))))?;
|
||||
unit(sub(scale(radial, half_angle.cos()), scale(axis, half_angle.sin())))
|
||||
unit(sub(scale(radial, libm::cos(half_angle)), scale(axis, libm::sin(half_angle))))
|
||||
}
|
||||
Surface::Sphere { center, .. } => unit(sub(p, center)),
|
||||
Surface::Torus { center, axis, major, .. } => {
|
||||
@@ -383,7 +383,7 @@ impl<'a> Inside<'a> {
|
||||
let (a, b, c) = (sub(*a, x), sub(*b, x), sub(*c, x));
|
||||
let (la, lb, lc) = (norm(a), norm(b), norm(c));
|
||||
let den = la * lb * lc + dot(a, b) * lc + dot(a, c) * lb + dot(b, c) * la;
|
||||
sum += 2.0 * dot(a, cross(b, c)).atan2(den);
|
||||
sum += 2.0 * crate::vec3::atan2(dot(a, cross(b, c)), den);
|
||||
}
|
||||
Ok(sum / (4.0 * std::f64::consts::PI))
|
||||
}
|
||||
|
||||
@@ -98,8 +98,8 @@ impl Frame {
|
||||
let w = sub(p, o);
|
||||
let z = dot(w, a);
|
||||
let (x, y) = (dot(w, e1), dot(w, e2));
|
||||
let t = y.atan2(x);
|
||||
([z, x.hypot(y)], if t < 0.0 { t + TAU } else { t })
|
||||
let t = crate::vec3::atan2(y, x);
|
||||
([z, libm::hypot(x, y)], if t < 0.0 { t + TAU } else { t })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ impl Frame {
|
||||
pub(crate) fn point(&self, q: Q, h: f64) -> P {
|
||||
match *self {
|
||||
Frame::Translate { o, u, v, d } => add(o, add(add(scale(u, q[0]), scale(v, q[1])), scale(d, h))),
|
||||
Frame::Rotate { o, a, e1, e2 } => add(o, add(scale(a, q[0]), scale(add(scale(e1, h.cos()), scale(e2, h.sin())), q[1]))),
|
||||
Frame::Rotate { o, a, e1, e2 } => add(o, add(scale(a, q[0]), scale(add(scale(e1, libm::cos(h)), scale(e2, libm::sin(h))), q[1]))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -479,7 +479,7 @@ impl CellGrid {
|
||||
}
|
||||
(lo, hi)
|
||||
});
|
||||
let per_axis = ((ctxs.len() as f64).cbrt().ceil() as usize * 2).clamp(1, MAX_GRID_DIM);
|
||||
let per_axis = (crate::vec3::cbrt(ctxs.len() as f64).ceil() as usize * 2).clamp(1, MAX_GRID_DIM);
|
||||
let extent = (0..3).map(|k| hi[k] - lo[k]).fold(0.0, f64::max);
|
||||
let step = if extent > 0.0 && extent.is_finite() { extent / per_axis as f64 } else { 1.0 };
|
||||
let dim = |k: usize| (((hi[k] - lo[k]) / step).ceil() as usize).clamp(1, MAX_GRID_DIM);
|
||||
|
||||
@@ -68,7 +68,7 @@ fn angles(lp: &[Vec<Q>], ccw: bool) -> Option<Vec<f64>> {
|
||||
let next = lp.get(k)?;
|
||||
let (a, o, b) = (*prev.get(prev.len().checked_sub(2)?)?, *next.first()?, *next.get(1)?);
|
||||
let (u, v) = ([o[0] - a[0], o[1] - a[1]], [b[0] - o[0], b[1] - o[1]]);
|
||||
let turn = (u[0] * v[1] - u[1] * v[0]).atan2(u[0] * v[0] + u[1] * v[1]);
|
||||
let turn = crate::vec3::atan2(u[0] * v[1] - u[1] * v[0], u[0] * v[0] + u[1] * v[1]);
|
||||
out.push(PI - if ccw { turn } else { -turn });
|
||||
}
|
||||
Some(out)
|
||||
@@ -443,8 +443,11 @@ fn disk(loops: &[Vec<Q>], size: &dyn SizeField, opts: &QuadOptions) -> Result<Op
|
||||
// ∫y², ∫x² (both × 12) and ∫xy (× 24), signed as the loop: the region's long direction is
|
||||
// the principal axis of the largest ∫ along², the angle of 0.5 · atan2(2 ∫xy, ∫x² − ∫y²).
|
||||
let sign = a2.signum();
|
||||
let theta = 0.5 * (sign * ixy).atan2(sign * (iyy - ixx));
|
||||
let (u, v) = ([theta.cos(), theta.sin()], [-theta.sin(), theta.cos()]);
|
||||
let theta = 0.5 * crate::vec3::atan2(sign * ixy, sign * (iyy - ixx));
|
||||
let (u, v) = {
|
||||
let (s, c) = (libm::sin(theta), libm::cos(theta));
|
||||
([c, s], [-s, c])
|
||||
};
|
||||
let local = |p: Q| {
|
||||
let d = [p[0] - centre[0], p[1] - centre[1]];
|
||||
[d[0] * u[0] + d[1] * u[1], d[0] * v[0] + d[1] * v[1]]
|
||||
|
||||
Reference in new issue
Block a user