Files
solidcraft-upstream/crates/kernel/src/heal.rs
T
Iraklis KourtisandClaude Opus 5.5 43ddde4a5d feat(kernel): an edge's meeting sides are sets of origins (merged faces)
EdgeOrigin::Meet now names each side as a Side: the sorted, de-duplicated
origins of the face there. substitute maps a side to the union of the inner
faces' origins, so a meeting with a face an inner operation merged keeps its
lineage; meeting() derives sides from whole faces. Fillets after box cuts
that leave coplanar pieces now report exact history. Prism lineage meets a
cap of several origins and resolves wall junctions away from shared
vertices by the operand's one edge between the walls. bore_union's recut
reports history only for a one-face bore (pieces of a two-arc bore's recut
wall were given both arcs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00

595 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Clean-up after operations: coplanar neighbouring faces become one face, and two straight
//! edges meeting in line at a vertex nothing else uses become one edge (as Fusion keeps them).
use std::collections::{BTreeSet, HashMap};
use solvecraft_geom::Vec3;
use truck_modeling::{self as mt, builder};
use crate::body::{Solid, from_p3};
use crate::history::{Change, EdgeOrigin, FaceProv};
fn plane_of(f: &mt::Face) -> Option<(Vec3, f64)> {
match f.oriented_surface() {
mt::Surface::Plane(p) => {
let n = p.normal();
let n = Vec3::new(n.x, n.y, n.z).normalized()?;
let o = p.origin();
Some((n, n.dot(Vec3::new(o.x, o.y, o.z))))
}
_ => None,
}
}
/// A face like `f` with new boundary wires given in `f`'s absolute (stored) orientation:
/// built on the stored surface and turned like `f` (rebuilding on the turned surface can make
/// the mesher pick the wrong side of a trim on periodic surfaces).
pub(crate) fn absolute_face(f: &mt::Face, wires: Vec<mt::Wire>) -> Option<mt::Face> {
let mut nf = mt::Face::try_new(wires, f.surface()).ok()?;
if !f.orientation() {
nf.invert();
}
Some(nf)
}
pub(crate) fn find(p: &mut [usize], mut i: usize) -> usize {
while let Some(&q) = p.get(i) {
if q == i {
break;
}
i = q;
}
i
}
/// Points along an oriented edge (for loop areas and containment).
fn samples(e: &mt::Edge) -> Vec<Vec3> {
use mt::{BoundedCurve, ParametricCurve};
let c = e.oriented_curve();
let (t0, t1) = c.range_tuple();
(0..8).map(|k| t0 + (t1 - t0) * k as f64 / 8.0).map(|t| from_p3(c.subs(t))).collect()
}
fn loop_area(edges: &[mt::Edge], n: Vec3) -> f64 {
let pts: Vec<Vec3> = edges.iter().flat_map(samples).collect();
let k = pts.len();
let mut s = Vec3::ZERO;
for i in 0..k {
if let (Some(a), Some(b)) = (pts.get(i), pts.get((i + 1) % k)) {
s += a.cross(*b);
}
}
s.dot(n) * 0.5
}
fn inside(p: Vec3, edges: &[mt::Edge], n: Vec3) -> bool {
let u = n.any_perp();
let w = n.cross(u);
let pts: Vec<(f64, f64)> = edges.iter().flat_map(samples).map(|q| (q.dot(u), q.dot(w))).collect();
let (px, py) = (p.dot(u), p.dot(w));
let k = pts.len();
let mut c = false;
for i in 0..k {
let (Some(&(ax, ay)), Some(&(bx, by))) = (pts.get(i), pts.get((i + 1) % k)) else { continue };
if (ay > py) != (by > py) && px < ax + (py - ay) / (by - ay) * (bx - ax) {
c = !c;
}
}
c
}
/// Do the points lie along one of the lines faces were split along on purpose (polylines;
/// chords of a curved line sag by under a tenth of their length)?
pub(crate) fn on_split_line(pts: &[Vec3], keep: &[Vec<Vec3>]) -> bool {
!pts.is_empty()
&& keep.iter().any(|line| {
let seg = line.windows(2).filter_map(|w| Some(w.first()?.dist(*w.get(1)?))).fold(0.0, f64::max);
let tol = 1e-6 * (1.0 + seg) + if line.len() > 2 { seg * 0.1 } else { 0.0 };
pts.iter().all(|q| line.windows(2).any(|w| w.first().zip(w.get(1)).is_some_and(|(a, b)| q.dist_to_segment(*a, *b) <= tol)))
})
}
/// Points along an edge (ends and three inside).
pub(crate) fn edge_points(e: &mt::Edge) -> Vec<Vec3> {
use mt::{BoundedCurve, ParametricCurve};
let c = e.curve();
let (t0, t1) = c.range_tuple();
(0..=4).map(|k| from_p3(c.subs(t0 + (t1 - t0) * k as f64 / 4.0))).collect()
}
/// Merge each group of edge-connected coplanar faces into one face (keeping faces apart along
/// the lines in `keep`). Returns the new shell and, for each of its faces, the faces of `shell`
/// it was made from (by position); None when nothing changed or a merged face can't be built.
fn merge_coplanar(shell: &mt::Shell, tol: f64, keep: &[Vec<Vec3>]) -> Option<(mt::Shell, Vec<Vec<usize>>)> {
let faces: Vec<mt::Face> = shell.face_iter().cloned().collect();
let planes: Vec<Option<(Vec3, f64)>> = faces.iter().map(plane_of).collect();
let mut parent: Vec<usize> = (0..faces.len()).collect();
let mut by_edge: HashMap<mt::EdgeID, Vec<usize>> = HashMap::new();
for (i, f) in faces.iter().enumerate() {
for e in f.edge_iter() {
by_edge.entry(e.id()).or_default().push(i);
}
}
let mut merged_any = false;
let edges: HashMap<mt::EdgeID, mt::Edge> = shell.edge_iter().map(|e| (e.id(), e)).collect();
for (id, fs) in &by_edge {
if let [a, b] = fs[..]
&& a != b
&& (keep.is_empty() || !edges.get(id).is_some_and(|e| on_split_line(&edge_points(e), keep)))
&& let (Some(Some((na, da))), Some(Some((nb, db)))) = (planes.get(a), planes.get(b))
&& na.dot(*nb) > 1.0 - 1e-9
&& (da - db).abs() < tol
{
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
if ra != rb {
if let Some(x) = parent.get_mut(ra) {
*x = rb;
}
merged_any = true;
}
}
}
if !merged_any {
return None;
}
let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
for i in 0..faces.len() {
let r = find(&mut parent, i);
groups.entry(r).or_default().push(i);
}
let mut out: Vec<mt::Face> = Vec::new();
let mut from: Vec<Vec<usize>> = Vec::new();
let mut order: Vec<usize> = groups.keys().copied().collect();
order.sort_unstable();
for g in order {
let members = groups.get(&g)?;
if members.len() == 1 {
out.push(faces.get(*members.first()?)?.clone());
from.push(members.clone());
continue;
}
let (n, _) = (*planes.get(*members.first()?)?)?;
// Oriented boundary edges of the group; edges used twice inside it are dropped.
let mut count: HashMap<mt::EdgeID, usize> = HashMap::new();
for m in members {
for e in faces.get(*m)?.boundary_iters().into_iter().flatten() {
*count.entry(e.id()).or_insert(0) += 1;
}
}
let mut rest: Vec<mt::Edge> = Vec::new();
for m in members {
for e in faces.get(*m)?.boundary_iters().into_iter().flatten() {
if count.get(&e.id()).copied().unwrap_or(0) == 1 {
rest.push(e);
}
}
}
// Chain into loops.
let mut loops: Vec<Vec<mt::Edge>> = Vec::new();
while let Some(first) = rest.pop() {
let start = first.front().clone();
let mut cur = first.back().clone();
let mut lp = vec![first];
let mut guard = 0;
while cur != start && guard < 100_000 {
guard += 1;
let k = rest.iter().position(|e| e.front() == &cur)?;
let e = rest.remove(k);
cur = e.back().clone();
lp.push(e);
}
loops.push(lp);
}
let (outer, holes): (Vec<Vec<mt::Edge>>, Vec<Vec<mt::Edge>>) = loops.into_iter().partition(|l| loop_area(l, n) > 0.0);
let surface = faces.get(*members.first()?)?.oriented_surface();
for o in &outer {
let mut wires: Vec<mt::Wire> = vec![o.clone().into()];
for h in &holes {
let probe = h.first().map(|e| from_p3(e.front().point()))?;
// The innermost outer loop around the hole owns it.
let owner = outer.iter().filter(|x| inside(probe, x, n)).min_by(|a, b| loop_area(a, n).total_cmp(&loop_area(b, n)));
if owner.is_some_and(|x| std::ptr::eq(x, o)) {
wires.push(h.clone().into());
}
}
out.push(mt::Face::try_new(wires, surface.clone()).ok()?);
from.push(members.clone());
}
}
Some((out.into(), from))
}
/// Join pairs of collinear straight edges at vertices only they use.
fn merge_collinear(shell: &mt::Shell, trace: &mut Trace) -> Option<mt::Shell> {
let mut faces: Vec<mt::Face> = shell.face_iter().cloned().collect();
let mut changed = false;
for _round in 0..10_000 {
let mut at: HashMap<mt::VertexID, Vec<mt::Edge>> = HashMap::new();
let mut seen = std::collections::HashSet::new();
for f in &faces {
for e in f.edge_iter() {
if seen.insert(e.id()) {
at.entry(e.front().id()).or_default().push(e.absolute_clone());
at.entry(e.back().id()).or_default().push(e.absolute_clone());
}
}
}
let pair = at.iter().find_map(|(v, es)| {
let [a, b] = &es[..] else { return None };
if !matches!(a.curve(), mt::Curve::Line(_)) || !matches!(b.curve(), mt::Curve::Line(_)) {
return None;
}
let other = |e: &mt::Edge| if e.front().id() == *v { e.back().clone() } else { e.front().clone() };
let mid = if a.front().id() == *v { a.front().clone() } else { a.back().clone() };
let (pa, pm, pb) = (from_p3(other(a).point()), from_p3(mid.point()), from_p3(other(b).point()));
let (u, w) = ((pm - pa).normalized()?, (pb - pm).normalized()?);
(u.dot(w) > 1.0 - 1e-10).then(|| (a.clone(), b.clone(), other(a), other(b)))
});
let Some((a, b, va, vb)) = pair else { break };
let joined = builder::line(&va, &vb);
trace.made(&joined, [a.id(), b.id()]);
for f in &mut faces {
if !f.edge_iter().any(|e| e.id() == a.id()) {
continue;
}
let mut wires = Vec::new();
for w in f.absolute_boundaries() {
let mut es: Vec<mt::Edge> = Vec::new();
for e in w.edge_iter() {
if e.id() == b.id() {
continue;
}
if e.id() == a.id() {
// a+b (in either order) become the joined edge, in this wire's direction.
es.push(if e.front() == &va { joined.clone() } else { joined.inverse() });
continue;
}
es.push(e.clone());
}
wires.push(mt::Wire::from(es));
}
*f = absolute_face(f, wires)?;
}
changed = true;
}
changed.then(|| faces.into())
}
/// The exact curve an approximate edge (a boolean's intersection curve) really is: a line or a
/// circular arc, when every sample lies on it.
fn exact_curve(e: &mt::Edge, tol: f64) -> Option<mt::Edge> {
// Only the approximate curves booleans make; exact ones stay as they are.
if !matches!(e.curve(), mt::Curve::IntersectionCurve(_)) {
return None;
}
fit_exact(e, tol)
}
/// A line or circular arc through the edge's samples, if they all lie on one.
fn fit_exact(e: &mt::Edge, tol: f64) -> Option<mt::Edge> {
use mt::{BoundedCurve, ParametricCurve};
let c = e.curve();
let (t0, t1) = c.range_tuple();
let pts: Vec<Vec3> = (0..=16).map(|k| from_p3(c.subs(t0 + (t1 - t0) * k as f64 / 16.0))).collect();
let (a, b) = (from_p3(e.absolute_front().point()), from_p3(e.absolute_back().point()));
if e.absolute_front() == e.absolute_back() || a.dist(b) < tol {
return None;
}
if pts.iter().all(|p| p.dist_to_segment(a, b) < tol) {
return Some(builder::line(e.absolute_front(), e.absolute_back()));
}
// A circle through the ends and the middle sample.
let m = *pts.get(8)?;
let n = (m - a).cross(b - a).normalized()?;
let (ab, am) = (b - a, m - a);
// Centre in the plane: solve |c-a| = |c-b| = |c-m|.
let d = 2.0 * ab.cross(am).len2();
if d < 1e-300 {
return None;
}
// Circumcentre: a + (|u|² v×w + |v|² w×u) / (2|w|²), u = b−a, v = m−a, w = u×v.
let w = ab.cross(am);
let centre = a + (am.cross(w) * ab.len2() + w.cross(ab) * am.len2()) * (1.0 / d);
let r = centre.dist(a);
if pts.iter().all(|p| (p.dist(centre) - r).abs() < tol && (*p - a).dot(n).abs() < tol) {
return Some(builder::circle_arc(e.absolute_front(), e.absolute_back(), mt::Point3::new(m.x, m.y, m.z)));
}
None
}
/// Replace approximate edges that are really lines or arcs with exact ones.
fn exact_edges(shell: &mt::Shell, tol: f64, trace: &mut Trace) -> Option<mt::Shell> {
let mut subst: HashMap<mt::EdgeID, mt::Edge> = HashMap::new();
for e in shell.edge_iter() {
if !subst.contains_key(&e.id())
&& let Some(x) = exact_curve(&e, tol)
{
trace.made(&x, [e.id()]);
subst.insert(e.id(), x);
}
}
if subst.is_empty() {
return None;
}
let mut faces = Vec::new();
for f in shell.face_iter() {
let mut wires = Vec::new();
for w in f.absolute_boundaries() {
let es: Vec<mt::Edge> = w
.edge_iter()
.map(|e| match subst.get(&e.id()) {
// The new edge runs absolute front → back; match this use's direction.
Some(n) => {
if e.front() == e.absolute_front() {
n.clone()
} else {
n.inverse()
}
}
None => e.clone(),
})
.collect();
wires.push(mt::Wire::from(es));
}
faces.push(absolute_face(f, wires)?);
}
Some(faces.into())
}
/// Faces whose surface is flat but not a plane (a revolved line square to the axis, a swept
/// line) get a real plane, so they merge with their coplanar neighbours and blend like planes.
fn planar_surfaces(shell: &mt::Shell, tol: f64) -> Option<mt::Shell> {
use mt::{BoundedCurve, ParametricCurve, ParametricSurface, ParametricSurface3D, SearchNearestParameter};
let mut changed = false;
let mut faces = Vec::new();
for f in shell.face_iter() {
let surf = f.surface();
if matches!(surf, mt::Surface::Plane(_)) {
faces.push(f.clone());
continue;
}
// Parameter box of the boundary, then a grid of surface points in it.
let mut uv: Vec<(f64, f64)> = Vec::new();
let mut pts: Vec<Vec3> = Vec::new();
for w in f.absolute_boundaries() {
for e in w.edge_iter() {
let c = e.oriented_curve();
let (t0, t1) = c.range_tuple();
for k in 0..6 {
let p = c.subs(t0 + (t1 - t0) * k as f64 / 6.0);
pts.push(from_p3(p));
if let Some(x) = surf.search_nearest_parameter(p, uv.last().copied(), 50) {
uv.push(x);
}
}
}
}
if uv.len() < 3 || pts.len() < 3 {
faces.push(f.clone());
continue;
}
let (u0, u1) = uv.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |a, x| (a.0.min(x.0), a.1.max(x.0)));
let (v0, v1) = uv.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |a, x| (a.0.min(x.1), a.1.max(x.1)));
for i in 0..=4 {
for j in 0..=4 {
pts.push(from_p3(surf.subs(u0 + (u1 - u0) * i as f64 / 4.0, v0 + (v1 - v0) * j as f64 / 4.0)));
}
}
// The plane through the points (normal from the surface).
let (um, vm) = ((u0 + u1) / 2.0, (v0 + v1) / 2.0);
let nn = surf.normal(um, vm);
let Some(n) = Vec3::new(nn.x, nn.y, nn.z).normalized() else {
faces.push(f.clone());
continue;
};
let o = from_p3(surf.subs(um, vm));
if pts.iter().any(|p| (*p - o).dot(n).abs() > tol) {
faces.push(f.clone());
continue;
}
let u = n.any_perp();
let w = n.cross(u);
let plane = mt::Plane::new(crate::body::p3(o), crate::body::p3(o + u), crate::body::p3(o + w));
match mt::Face::try_new(f.absolute_boundaries().clone(), mt::Surface::Plane(plane)) {
Ok(mut nf) => {
if !f.orientation() {
nf.invert();
}
faces.push(nf);
changed = true;
}
Err(_) => faces.push(f.clone()),
}
}
changed.then(|| faces.into())
}
/// Heal a solid; returns the input unchanged when nothing applies or healing fails.
#[cfg(test)]
pub fn heal(solid: Solid, size: f64) -> Solid {
healed(solid, size, &[], false).0
}
/// What healing made each face and edge of its result from.
#[derive(Debug)]
pub(crate) struct HealMap {
/// For each face of the result (in its face order), the input faces it was made from (by
/// position in the input's face order): one, or several merged into it.
pub faces: Vec<Vec<usize>>,
/// For each edge of the result (by identity), the input edges it was made from (by
/// position in the input's edge order): one, or several joined into it.
pub edges: HashMap<mt::EdgeID, Vec<usize>>,
}
impl HealMap {
/// The history of the healed solid, from that of its input (`faces` in the input's face
/// order, each a region of operand faces; `edges` the origins of each edge, in its edge
/// order): a face healing merged from several is Merged when they came from several, the
/// rest keep their origins (see [`crate::record::regions`]). Faces in the healed solid's
/// face order, edges by identity.
pub(crate) fn carry(&self, faces: &[FaceProv], edges: &[Vec<EdgeOrigin>]) -> Option<(Vec<FaceProv>, HashMap<mt::EdgeID, Vec<EdgeOrigin>>)> {
let regions = self
.faces
.iter()
.map(|under| {
let whole = matches!(under.as_slice(), [i] if faces.get(*i).is_some_and(|f| f.change == Change::Kept));
let mut origins = BTreeSet::new();
for i in under {
origins.extend(faces.get(*i)?.origins.iter().copied());
}
Some((origins, whole))
})
.collect::<Option<Vec<_>>>()?;
let edges = self
.edges
.iter()
.map(|(id, under)| {
let mut origins = BTreeSet::new();
for i in under {
origins.extend(edges.get(*i)?.iter().cloned());
}
Some((*id, origins.into_iter().collect()))
})
.collect::<Option<HashMap<_, _>>>()?;
Some((crate::record::regions(regions), edges))
}
/// Every face and edge of `solid` as itself (nothing healed).
fn identity(solid: &Solid) -> HealMap {
let faces = (0..solid.face_iter().count()).map(|i| vec![i]).collect();
let edges = crate::record::edge_index(solid).into_iter().map(|(id, i)| (id, vec![i])).collect();
HealMap { faces, edges }
}
}
/// Heal a solid (the input unchanged when nothing applies or healing fails), keeping faces apart
/// along lines they were split along on purpose (`keep`), with what each face and edge of the
/// result was made from (`None` when that could not be followed).
pub(crate) fn heal_traced(solid: Solid, size: f64, keep: &[Vec<Vec3>]) -> (Solid, Option<HealMap>) {
healed(solid, size, keep, true)
}
/// The healed solid, and what it was made from when `traced`.
fn healed(solid: Solid, size: f64, keep: &[Vec<Vec3>], traced: bool) -> (Solid, Option<HealMap>) {
// Edges a boolean made are intersection curves, which panic where they no longer
// evaluate; the solid then stays as it is.
match crate::guard("heal", || Ok(heal_shells(&solid, size, keep, traced))) {
Ok(Some((healed, map))) => (healed, map),
_ => {
let map = if traced { crate::guard("heal map", || Ok(HealMap::identity(&solid))).ok() } else { None };
(solid, map)
}
}
}
/// The input edges each edge made while healing comes from. Input edges live as long as the
/// healing (the input solid is borrowed throughout), so their identities are never reused; an
/// edge healing makes is recorded when it is made, from edges then alive, which overwrites any
/// entry left under its identity by an edge made earlier and dropped since. Records nothing
/// when not `on`.
struct Trace {
on: bool,
input: HashMap<mt::EdgeID, usize>,
made: HashMap<mt::EdgeID, Vec<usize>>,
}
impl Trace {
/// The input edges under the live edge `id`.
fn of(&self, id: &mt::EdgeID) -> Vec<usize> {
match self.made.get(id) {
Some(from) => from.clone(),
None => self.input.get(id).map(|i| vec![*i]).unwrap_or_default(),
}
}
/// `edge` was made from the live edges `from`.
fn made<const N: usize>(&mut self, edge: &mt::Edge, from: [mt::EdgeID; N]) {
if !self.on {
return;
}
let mut under: Vec<usize> = from.iter().flat_map(|id| self.of(id)).collect();
under.sort_unstable();
under.dedup();
self.made.insert(edge.id(), under);
}
}
fn heal_shells(solid: &Solid, size: f64, keep: &[Vec<Vec3>], traced: bool) -> Option<(Solid, Option<HealMap>)> {
let tol = (size * 1e-7).max(1e-9);
let mut shells = Vec::new();
let mut changed = false;
let input = if traced { crate::record::edge_index(solid) } else { HashMap::new() };
let mut trace = Trace { on: traced, input, made: HashMap::new() };
// Each face of the shells so far, as the input faces it was made from.
let mut faces: Vec<Vec<usize>> = Vec::new();
let mut first = 0;
for sh in solid.boundaries() {
let mut cur = sh.clone();
let count = cur.face_iter().count();
let mut from: Vec<Vec<usize>> = (first..first + count).map(|i| vec![i]).collect();
first += count;
// Faces come out in place, one for one, except where coplanar ones merge.
if let Some(m) = planar_surfaces(&cur, (size * 1e-7).max(1e-9)) {
cur = m;
changed = true;
}
if let Some(m) = exact_edges(&cur, (size * 1e-5).max(1e-7), &mut trace) {
cur = m;
changed = true;
}
if let Some((m, groups)) = merge_coplanar(&cur, tol, keep) {
cur = m;
from = groups.iter().map(|g| g.iter().filter_map(|i| from.get(*i)).flatten().copied().collect()).collect();
changed = true;
}
if let Some(m) = merge_collinear(&cur, &mut trace) {
cur = m;
changed = true;
}
faces.extend(from);
shells.push(cur);
}
if !changed {
return None;
}
let healed = Solid::try_new(shells).ok()?;
if !traced {
return Some((healed, None));
}
let edges: Option<HashMap<mt::EdgeID, Vec<usize>>> = crate::body::edge_order(healed.face_iter())
.0
.iter()
.map(|e| {
let under = trace.of(&e.id());
(!under.is_empty()).then(|| (e.id(), under))
})
.collect();
let map = edges.map(|edges| HealMap { faces, edges });
Some((healed, map))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn approximate_arcs_become_exact() {
// A cubic (non-rational) spline through a quarter circle.
let (a, b) = (builder::vertex(mt::Point3::new(10.0, 0.0, 0.0)), builder::vertex(mt::Point3::new(0.0, 10.0, 0.0)));
let pts: Vec<Vec3> = (0..=40)
.map(|k| {
let t = std::f64::consts::FRAC_PI_2 * k as f64 / 40.0;
Vec3::new(10.0 * t.cos(), 10.0 * t.sin(), 0.0)
})
.collect();
let curve = mt::Curve::BSplineCurve(crate::build::interpolate_cubic(&pts).expect("spline"));
let e = mt::Edge::new(&a, &b, curve);
let x = fit_exact(&e, 1e-2).expect("an arc");
use mt::{BoundedCurve, ParametricCurve};
let c = x.curve();
let (t0, t1) = c.range_tuple();
let mid = c.subs((t0 + t1) / 2.0);
assert!(((mid.x * mid.x + mid.y * mid.y).sqrt() - 10.0).abs() < 1e-9, "{mid:?}");
}
}