feat(ui): scale-relative fuzzy default, measured (M2-4 B5)
`FUZZY_FRACTION = 1e-7`: every production boolean runs at `1e-7 × the target's bounding-box diagonal` — 5.2e-6 mm on the 52 mm block. Both bounds measured before the number was written. LOWER BOUND — the noise the default must cover. `measure_flush_wall_noise` (ignored harness, compile/tests/booleans.rs) fuses a 40x30x15 box with a 10x30x15 box authored `gap` past its x = 40 wall, over a (gap x fuzzy) grid. At fuzzy 0 OCCT's own confusion closes gaps up to 2e-7 mm and leaves 5e-7 open: twelve faces, two solids. So delta* = 5e-7. A fuzzy value closes a gap iff it is >= the gap (5e-7 closes 5e-7, 1e-6 closes 1e-6, ...). The spec's rule, fraction * diag >= 2 * delta*, gives fraction >= 1.9e-8 on diag = sqrt(40^2+30^2+15^2) = 52.2. UPPER BOUND — the largest fraction keeping every gate green. Swept 1e-4, 1e-5, 1e-6, 1e-7, 1e-8 over vernier-ui, vernier-app and the selftest. Every analytic 1e-9 gate held at every value — mass properties carry no tolerance — but one thing does move: OCCT's fuzzy inflates the result's tolerances and the tolerance-inclusive Bnd_Box grows by ~1.6x the fuzzy value, and that box IS `bbox_diagonal`, NAMING.md section 5's scale. Lane A's split-rebind fixture pins D at 1e-6 relative: 1e-6: 52.20161787 vs 52.20153254 analytic, 1.6e-6 -> red 1e-7: green (1.6e-7) So 1e-7 sits five times over the floor and five times under the ceiling. Not a STOP: the window [1.9e-8, ~6e-7] is open. WHAT THE FIXTURE HAD TO LEARN. The fuzzy fuse FILLS the gap rather than snapping the neighbour across it: the volume is 22500.000224999996 against 22500 + 5e-7 * 30 * 15 = 22500.000225 (1.8e-16 relative), and the bare sum 22500 is off by exactly 1e-8, ten times the gate. The fixtures assert the filled union. `a_flush_wall_fuses_to_one_face_under_the_default` (document level, production policy: 1 solid, 6 faces, exact volume) and `the_same_gap_stays_a_sliver_at_zero_fuzzy` (the SAME two boxes at fuzzy 0: 12 faces, 2 solids; at the default: 6, 1; and a 1e-7 gap closes either way, so the default only works where zero fuzzy fails). Selftest `m2-flush-wall` (29th, appended) records both outcomes: (94, f9fb78428f4def90). Also fixed in the bridging fixture from B3: its underside filter caught the slot's ceiling (normal -Z at z = 5) once fuzzy nudged OCCT's face order — it now demands z = 0 as well. Selftest digests, B4 -> B5: l1-bike-light-mount (195, 0e0084d3d6e499af) -> (195, 4aff517a7a816113) probed: same normal, same volume, same 12 angled faces; the centroid's last digits moved at 1.2e-14 relative (10.016404753393848 vs ...973), the per-face integration order again. m3-mirror unchanged at 12: the bracket's ~1e-9 solved-plane offset is under the confusion and its walls still do not unify. MISTAKES.md's zero-volume entry records the resolution; README's count 27 -> 28 -> 29 across B4/B5 (set to what this branch holds). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+13
@@ -41,6 +41,19 @@ These are documented in advance because they are near-certain and expensive.
|
||||
A second confirmed-clean data point, and the reason the scale-relative-fuzzy calibration
|
||||
is now more urgent than it was: the geometry that would expose a wrong default is no
|
||||
longer something a user has to go out of their way to draw.
|
||||
- **Resolved 2026-09-06 with `FUZZY_FRACTION = 1e-7`** (`vernier-ui/src/compile/merge.rs`,
|
||||
M2-4): every production boolean now runs at `1e-7 × the target's bounding-box diagonal`.
|
||||
Both bounds were measured, not chosen: at zero fuzzy OCCT's own confusion closes a flush
|
||||
gap of 2e-7 mm and leaves 5e-7 open (two solids, twelve faces), and a fuzzy value closes a
|
||||
gap iff it is at least the gap — so the floor is 2·5e-7 / 52.2 mm ≈ 1.9e-8; the ceiling
|
||||
is that OCCT's fuzzy inflates the result's tolerances and the tolerance-inclusive
|
||||
`Bnd_Box` (`bbox_diagonal`, §5's scale) grows by ~1.6× the fuzzy value, which the
|
||||
split-rebind fixture's 1e-6 pin on D refuses at 1e-6 and accepts at 1e-7. Fixture:
|
||||
`a_flush_wall_fuses_to_one_face_under_the_default` with its zero-fuzzy sibling
|
||||
`the_same_gap_stays_a_sliver_at_zero_fuzzy` (`compile/tests/booleans.rs`), selftest
|
||||
`m2-flush-wall`. One thing the fixture had to learn: the fuzzy fuse FILLS the gap rather
|
||||
than snapping the neighbour across it, so the exact volume is `V + δ·A_contact`, and
|
||||
asserting the bare sum at 1e-9 fails by exactly 1e-8.
|
||||
|
||||
### Fillet silently moves to a different edge after a sketch edit
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Headless checks need no display at all:
|
||||
|
||||
```sh
|
||||
cargo test --workspace # every crate
|
||||
cargo run --release -p vernier-cli -- --selftest # 28 end-to-end checks, JSON
|
||||
cargo run --release -p vernier-cli -- --selftest # 29 end-to-end checks, JSON
|
||||
cargo fmt --all --check
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
```
|
||||
@@ -159,7 +159,7 @@ the viewport.
|
||||
|
||||
## Tests and gates
|
||||
|
||||
`cargo test --workspace` runs everything; `--selftest` runs 28 end-to-end
|
||||
`cargo test --workspace` runs everything; `--selftest` runs 29 end-to-end
|
||||
checks with a JSON report, including golden geometry and a recompute-cost
|
||||
gauge. Screenshot and picking tests render through the production pipeline and
|
||||
read the framebuffer back.
|
||||
|
||||
@@ -135,3 +135,116 @@ pub(crate) fn check_m2_unify_push_pull() -> Result<Vec<u8>, String> {
|
||||
)
|
||||
.into_bytes())
|
||||
}
|
||||
|
||||
/// A standalone id source for the kernel-level control below.
|
||||
struct Counter(u64);
|
||||
impl vernier_kernel::naming::EntityIdSource for Counter {
|
||||
fn allocate(&mut self) -> vernier_kernel::EntityId {
|
||||
self.0 += 1;
|
||||
vernier_kernel::EntityId::from_raw(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// `m2-flush-wall`: the scale-relative fuzzy default closes the gap a
|
||||
/// zero-fuzzy fuse leaves open (M2-4).
|
||||
///
|
||||
/// A 40 × 30 × 15 block and a 10 × 30 × 15 neighbour authored δ* = 5e-7 mm
|
||||
/// past its `x = 40` wall — the smallest gap OCCT's own confusion does not
|
||||
/// close (measured 2026-09-06: 2e-7 closes, 5e-7 does not). Compiled
|
||||
/// through the production policy the part is ONE solid of 6 faces and,
|
||||
/// the gap FILLED, exactly `22500 + δ*·30·15 = 22500.000225 mm³` at 1e-9
|
||||
/// relative (measured 1.8e-16 from that; the bare 22500 would be 1e-8 off,
|
||||
/// ten times the gate — the fuzzy fuse fills a gap, it does not snap the
|
||||
/// neighbour across it). The control runs the same two
|
||||
/// boxes through the kernel at `fuzzy_mm = 0.0`: two solids, twelve
|
||||
/// faces — so the check cannot pass for the wrong reason (a δ* the
|
||||
/// confusion already covers would make the default a no-op that looks like
|
||||
/// a success).
|
||||
///
|
||||
/// Artifact: both outcomes and the numbers.
|
||||
pub(crate) fn check_m2_flush_wall() -> Result<Vec<u8>, String> {
|
||||
use vernier_kernel::evaluate::{
|
||||
BooleanKind, BooleanOptions, Transform, evaluate_boolean, evaluate_box, evaluate_transform,
|
||||
};
|
||||
use vernier_kernel::naming::NameTable;
|
||||
const GAP: f64 = 5e-7;
|
||||
const TOL: f64 = 1e-9;
|
||||
let close = |what: &str, got: f64, want: f64| -> Result<(), String> {
|
||||
if ((got - want) / want).abs() > TOL {
|
||||
return Err(format!("{what} is {got}, expected {want}"));
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut doc = Document::new();
|
||||
block(&mut doc, "block", [0.0, 0.0], [40.0, 30.0], 15.0)?;
|
||||
block(
|
||||
&mut doc,
|
||||
"neighbour",
|
||||
[40.0 + GAP, 0.0],
|
||||
[50.0 + GAP, 30.0],
|
||||
15.0,
|
||||
)?;
|
||||
let mut kernel = vernier_kernel::OcctKernel::new();
|
||||
let mut store = NamingStore::new();
|
||||
let compiled =
|
||||
compile_document(&mut doc, &mut kernel, &mut store).map_err(|e| e.to_string())?;
|
||||
if !compiled.warnings.is_empty() {
|
||||
return Err(format!(
|
||||
"the flush fuse must warn of nothing: {:?}",
|
||||
compiled.warnings
|
||||
));
|
||||
}
|
||||
let body = compiled.body.ok_or("no fused body")?;
|
||||
let (faces, solids, volume) = (
|
||||
body.faces.len(),
|
||||
body.summary.topology.solids,
|
||||
body.summary.geometry.volume,
|
||||
);
|
||||
if (faces, solids) != (6, 1) {
|
||||
return Err(format!(
|
||||
"under the default the flush walls are {faces} faces in {solids} solids, not 6 in 1"
|
||||
));
|
||||
}
|
||||
close("the fused volume", volume, 22500.0 + GAP * 30.0 * 15.0)?;
|
||||
|
||||
// The control: the same two boxes at zero fuzzy stay two solids.
|
||||
let mut ids = Counter(0);
|
||||
let mut tables = (NameTable::new(), NameTable::new(), NameTable::new());
|
||||
let a = evaluate_box(&mut kernel, &mut tables.0, &mut ids, 40.0, 30.0, 15.0)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let b_home = evaluate_box(&mut kernel, &mut tables.1, &mut ids, 10.0, 30.0, 15.0)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let b = evaluate_transform(&kernel, &b_home, Transform::Translate(40.0 + GAP, 0.0, 0.0))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let open = evaluate_boolean(
|
||||
&kernel,
|
||||
&mut tables.2,
|
||||
&mut ids,
|
||||
BooleanKind::Fuse,
|
||||
&a,
|
||||
&b,
|
||||
BooleanOptions {
|
||||
fuzzy_mm: 0.0,
|
||||
simplify: true,
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let (open_faces, open_solids) = (open.faces.len(), open.summary.topology.solids);
|
||||
if (open_faces, open_solids) != (12, 2) {
|
||||
return Err(format!(
|
||||
"at zero fuzzy the gap must stay open — {open_faces} faces in {open_solids} solids \
|
||||
says δ* = {GAP} is not the noise, and the fixture is the bug"
|
||||
));
|
||||
}
|
||||
close(
|
||||
"the control's volume",
|
||||
open.summary.geometry.volume,
|
||||
22500.0,
|
||||
)?;
|
||||
Ok(format!(
|
||||
"gap {GAP}\ndefault faces {faces} solids {solids} volume {volume}\n\
|
||||
zero-fuzzy faces {open_faces} solids {open_solids}\n"
|
||||
)
|
||||
.into_bytes())
|
||||
}
|
||||
|
||||
@@ -190,6 +190,10 @@ pub(crate) fn registry() -> Vec<Check> {
|
||||
name: "m2-unify-push-pull",
|
||||
run: m2_booleans::check_m2_unify_push_pull,
|
||||
},
|
||||
Check {
|
||||
name: "m2-flush-wall",
|
||||
run: m2_booleans::check_m2_flush_wall,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,25 @@ use super::{CompileError, CompileWarning, evaluate};
|
||||
const CUT_MIN_REMOVAL: f64 = 1e-9;
|
||||
|
||||
/// Fuzzy boolean tolerance as a fraction of the TARGET body's bounding-box
|
||||
/// diagonal (façade-computed). Calibrated by
|
||||
/// `a_flush_wall_fuses_to_one_face_under_the_default` and bounded above by
|
||||
/// every analytic gate at 1e-9; measured value and both bounds in the
|
||||
/// commit that set it (M2-4). `0.0` until then: the flip of `simplify`
|
||||
/// (M2-3) and the fuzzy default are two decisions with two measurements.
|
||||
pub(super) const FUZZY_FRACTION: f64 = 0.0;
|
||||
/// diagonal (façade-computed). **Both bounds measured 2026-09-06 (M2-4):**
|
||||
///
|
||||
/// * Lower: two flush walls with a gap of δ* = 5e-7 mm are the smallest
|
||||
/// gap OCCT's own confusion leaves open (2e-7 closes, 5e-7 is two solids
|
||||
/// and twelve faces — `measure_flush_wall_noise`), and a fuzzy value
|
||||
/// closes a gap iff it is ≥ the gap. The default must cover 2δ* on the
|
||||
/// 52.2 mm block: fraction ≥ 1.9e-8. Calibrated by
|
||||
/// `a_flush_wall_fuses_to_one_face_under_the_default`, proven by its
|
||||
/// zero-fuzzy sibling.
|
||||
/// * Upper: OCCT's fuzzy inflates the result's tolerances, and the
|
||||
/// tolerance-inclusive `Bnd_Box` grows by ~1.6× the fuzzy value — which
|
||||
/// is `bbox_diagonal`, NAMING.md §5's scale. Every analytic gate at 1e-9
|
||||
/// holds at every fraction tried (mass properties carry no tolerance),
|
||||
/// but the split-rebind fixture pins D at 1e-6 relative: 1e-6 fails
|
||||
/// (1.6e-6), 1e-7 passes.
|
||||
///
|
||||
/// 1e-7 sits five times over the floor and five times under the ceiling:
|
||||
/// 5.2e-6 mm on the 52 mm block, ten times δ*.
|
||||
pub(super) const FUZZY_FRACTION: f64 = 1e-7;
|
||||
|
||||
/// The one production boolean policy: unify same-domain faces (M2-3,
|
||||
/// Option C — a target face that absorbs tool faces of the same operation
|
||||
|
||||
@@ -409,10 +409,14 @@ fn a_boss_bridging_two_strips_of_one_face_is_merged_by_name() {
|
||||
.body
|
||||
.unwrap();
|
||||
near(slotted.summary.geometry.volume, 18000.0 - 10.0 * 30.0 * 5.0);
|
||||
// The underside's two strips: normal −Z AND at z = 0 — the slot's
|
||||
// ceiling at z = 5 faces down too.
|
||||
let strips: Vec<_> = slotted
|
||||
.faces
|
||||
.iter()
|
||||
.filter(|(_, r)| r.normal.is_some_and(|n| (n[2] + 1.0).abs() < 1e-9))
|
||||
.filter(|(_, r)| {
|
||||
r.normal.is_some_and(|n| (n[2] + 1.0).abs() < 1e-9) && r.centroid[2].abs() < 1e-9
|
||||
})
|
||||
.map(|(&id, _)| id)
|
||||
.collect();
|
||||
assert_eq!(strips.len(), 2, "the slot splits the underside in two");
|
||||
@@ -465,3 +469,144 @@ fn a_boss_bridging_two_strips_of_one_face_is_merged_by_name() {
|
||||
let wall = generated(&store, block, block_curves[0]).unwrap();
|
||||
assert_eq!(filled.verdicts.get(wall), Some(&Verdict::Continued(wall)));
|
||||
}
|
||||
|
||||
/// A standalone id source for kernel-level probes.
|
||||
struct Counter(u64);
|
||||
impl vernier_kernel::naming::EntityIdSource for Counter {
|
||||
fn allocate(&mut self) -> vernier_kernel::EntityId {
|
||||
self.0 += 1;
|
||||
vernier_kernel::EntityId::from_raw(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// **δ\***: the smallest gap between two flush walls that a zero-fuzzy
|
||||
/// fuse leaves open, mm. Measured by `measure_flush_wall_noise`
|
||||
/// (2026-09-06): OCCT's own confusion closes 2e-7 and stops at 5e-7 —
|
||||
/// twelve faces, two solids. The default must cover twice this.
|
||||
const FLUSH_GAP: f64 = 5e-7;
|
||||
|
||||
/// Fuses a 40 × 30 × 15 box with a 10 × 30 × 15 box whose left face sits
|
||||
/// at `x = 40 + gap`, under `fuzzy_mm` (`None` = the production policy for
|
||||
/// that target) and `simplify`. Returns (faces, solids, volume).
|
||||
fn flush_fuse(gap: f64, fuzzy_mm: Option<f64>) -> (usize, u32, f64) {
|
||||
use vernier_kernel::evaluate::{Transform, evaluate_box, evaluate_transform};
|
||||
let mut kernel = OcctKernel::new();
|
||||
let mut ids = Counter(0);
|
||||
let mut table_a = NameTable::new();
|
||||
let mut table_b = NameTable::new();
|
||||
let mut fuse_table = NameTable::new();
|
||||
let a = evaluate_box(&mut kernel, &mut table_a, &mut ids, 40.0, 30.0, 15.0).unwrap();
|
||||
let b_home = evaluate_box(&mut kernel, &mut table_b, &mut ids, 10.0, 30.0, 15.0).unwrap();
|
||||
let b =
|
||||
evaluate_transform(&kernel, &b_home, Transform::Translate(40.0 + gap, 0.0, 0.0)).unwrap();
|
||||
let fused = evaluate_boolean(
|
||||
&kernel,
|
||||
&mut fuse_table,
|
||||
&mut ids,
|
||||
BooleanKind::Fuse,
|
||||
&a,
|
||||
&b,
|
||||
BooleanOptions {
|
||||
fuzzy_mm: fuzzy_mm
|
||||
.unwrap_or_else(|| crate::compile::merge::production_options(&a).fuzzy_mm),
|
||||
simplify: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
(
|
||||
fused.faces.len(),
|
||||
fused.summary.topology.solids,
|
||||
fused.summary.geometry.volume,
|
||||
)
|
||||
}
|
||||
|
||||
/// **The M2-4 noise probe** (spec B(5)): what gap between two flush walls
|
||||
/// the fuse still closes at `fuzzy_mm = 0`, and what fuzzy value closes a
|
||||
/// given gap. Print-only and ignored; run as
|
||||
/// `cargo test -p vernier-ui measure_flush_wall_noise -- --ignored --nocapture`.
|
||||
/// The numbers it printed are in the commit that set `FUZZY_FRACTION`.
|
||||
#[test]
|
||||
#[ignore = "measurement harness: prints, never asserts"]
|
||||
fn measure_flush_wall_noise() {
|
||||
let gaps = [
|
||||
0.0, 1e-12, 1e-10, 1e-9, 1e-8, 5e-8, 1e-7, 1.5e-7, 2e-7, 5e-7, 1e-6, 1e-5, 1e-4,
|
||||
];
|
||||
let fuzzies = [0.0, 1e-8, 1e-7, 2e-7, 5e-7, 1e-6, 1e-5, 1e-4, 1e-3];
|
||||
println!("gap \\ fuzzy: faces/solids/volume-22500");
|
||||
for gap in gaps {
|
||||
let mut row = format!("{gap:>8.1e}:");
|
||||
for fuzzy in fuzzies {
|
||||
let (faces, solids, volume) = flush_fuse(gap, Some(fuzzy));
|
||||
row.push_str(&format!(
|
||||
" {fuzzy:.0e}->{faces}/{solids}/{:+.2e}",
|
||||
volume - 22500.0
|
||||
));
|
||||
}
|
||||
println!("{row}");
|
||||
}
|
||||
}
|
||||
|
||||
/// **The M2-4 calibration fixture.** A 40 × 30 × 15 block and a 10 × 30 × 15
|
||||
/// neighbour authored `FLUSH_GAP` = δ* past its `x = 40` wall — the gap a
|
||||
/// zero-fuzzy fuse leaves open (`the_same_gap_stays_a_sliver_at_zero_fuzzy`
|
||||
/// is the proof) — compiled through the production policy: ONE solid, 6
|
||||
/// faces, and the volume of the union WITH THE GAP FILLED,
|
||||
/// `40·30·15 + 10·30·15 + δ*·30·15 = 22500.000225`, at 1e-9 relative.
|
||||
/// Measured: the fuzzy fuse fills the gap rather than snapping the
|
||||
/// neighbour across it — 22500.000224999996 against 22500.000225, 1.8e-16
|
||||
/// relative — so that is the exact number, and 22500 would be off by
|
||||
/// 1e-8, ten times the gate. This is the zero-volume trap of MISTAKES.md
|
||||
/// (2026-08-10) closed with a number rather than a warning.
|
||||
#[test]
|
||||
fn a_flush_wall_fuses_to_one_face_under_the_default() {
|
||||
let mut doc = Document::new();
|
||||
let (_, _) = boss(&mut doc, "block", [0.0, 0.0], [40.0, 30.0], 15.0, false);
|
||||
let (_, _) = boss(
|
||||
&mut doc,
|
||||
"neighbour",
|
||||
[40.0 + FLUSH_GAP, 0.0],
|
||||
[50.0 + FLUSH_GAP, 30.0],
|
||||
15.0,
|
||||
false,
|
||||
);
|
||||
let mut kernel = OcctKernel::new();
|
||||
let mut store = NamingStore::new();
|
||||
let compiled = compile_document(&mut doc, &mut kernel, &mut store).unwrap();
|
||||
assert!(compiled.warnings.is_empty(), "{:?}", compiled.warnings);
|
||||
let body = compiled.body.unwrap();
|
||||
assert_eq!(
|
||||
body.summary.topology.solids, 1,
|
||||
"the default closes the gap"
|
||||
);
|
||||
assert_eq!(body.faces.len(), 6, "one block, its flush walls unified");
|
||||
near(
|
||||
body.summary.geometry.volume,
|
||||
22500.0 + FLUSH_GAP * 30.0 * 15.0,
|
||||
);
|
||||
}
|
||||
|
||||
/// The mutation that proves the default works: the SAME gap under
|
||||
/// `fuzzy_mm = 0.0` is two solids and twelve faces. If this ever passes at
|
||||
/// zero fuzzy, δ* was not the noise and the fixture above is the bug
|
||||
/// (MISTAKES.md 2026-08-10) — so both halves are asserted here, against
|
||||
/// the same two boxes.
|
||||
#[test]
|
||||
fn the_same_gap_stays_a_sliver_at_zero_fuzzy() {
|
||||
let (faces, solids, volume) = flush_fuse(FLUSH_GAP, Some(0.0));
|
||||
assert_eq!(
|
||||
(faces, solids),
|
||||
(12, 2),
|
||||
"zero fuzzy must leave the gap open"
|
||||
);
|
||||
near(volume, 22500.0);
|
||||
let (faces, solids, volume) = flush_fuse(FLUSH_GAP, None);
|
||||
assert_eq!(
|
||||
(faces, solids),
|
||||
(6, 1),
|
||||
"the production default must close it"
|
||||
);
|
||||
near(volume, 22500.0 + FLUSH_GAP * 30.0 * 15.0);
|
||||
// …and a gap the confusion alone closes is closed either way, so the
|
||||
// default is doing work only where zero fuzzy fails.
|
||||
assert_eq!(flush_fuse(1e-7, Some(0.0)).0, 6);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user