facett-map/tests/inner_miter_clamp.rs
//! **The inner-miter clamp, proven on the geometry the GPU rasterises** (GFX_V2 §3.A).
//!
//! A miter join's concave corner retreats `hw · miter · sin(τ/2)` backwards along its
//! segment. Unbounded, a segment shorter than that retreat at both ends has its two
//! concave corners cross: the quad winds backwards and paints a bowtie. This sweeps the
//! join angle, the stroke width and the zoom over a family of polylines built to
//! bowtie, and asserts on the **applied screen positions**
//! ([`facett_map::gpu::line::applied_screen_pos`] — the vertex shader's own arithmetic,
//! proven against real device pixels by
//! `line_gpu.rs::gpu_draws_exactly_the_clamped_join_geometry_the_cpu_model_predicts`).
//!
//! Every invariant ships with the mutation that must break it in the SAME run:
//!
//! | invariant | mutation run here | measured |
//! |---|---|---|
//! | each quad is a simple polygon | `inner_lim = NO_INNER_LIMIT` | 0 of 5040 faulted clamped, 1974 unclamped |
//! | no concave edge runs backwards | `inner_lim = NO_INNER_LIMIT` | worst unclamped span −89 px on a 15 px segment |
//! | the ceiling is frame-independent | six zooms, 0.25 → 100 000 | worst vertex drift 0.0001 px |
//! | width is exact away from a join | the `min(L_A, L_B)` ceiling | 6.000 px held vs 4.325 px tapered |
//!
//! The mutations run as assertions, not as a manual experiment: if the clamp were a
//! no-op, or the sweep never reached a pathological case, the "must fail" arms would
//! find nothing to fail and this file goes red. That is what stops it being a hollow
//! guard — the failure mode the four hollow-guard-hunting agents of 2026-07-31 each
//! shipped.
#![cfg(feature = "wgpu")]
mod miter_geom;
use facett_map::gpu::line::{LinePolyline, LineVertex, NO_INNER_LIMIT, applied_screen_pos, build_line_geometry};
use miter_geom::{across, along, faults, staircase, unclamped};
const W: f32 = 1024.0;
const H: f32 = 1024.0;
/// This suite's frame: the shared builder at the fixed 1024² viewport.
fn frame(zoom_x: f32, zoom_y: f32, width_scale: f32, feather_px: f32) -> (facett_map::gpu::MapTransform, facett_map::gpu::line::MapLineUniforms) {
miter_geom::frame(W, H, zoom_x, zoom_y, width_scale, feather_px)
}
// ── The sweep ─────────────────────────────────────────────────────────────────
/// Join angles, spanning the whole usable range: a near-straight join, the comfortable
/// ones a test could sit on and learn nothing from, and the pathological reflex end
/// where the retreat diverges. 179.9° exercises the 180°-reversal branch, where the
/// bisector vanishes and there is no meaningful miter at all.
const ANGLES: [f32; 12] = [1.0, 5.0, 15.0, 30.0, 45.0, 60.0, 90.0, 120.0, 150.0, 170.0, 178.0, 179.9];
/// Themed stroke widths × the deck.gl `widthScale`. Chosen so the pair resolves BOTH
/// pixel clamps and several widths in between: GFX_V2 warns a glyph is four vertices at
/// 6 pt and at 60 pt, and a clamp checked at one width is the same trap.
const WIDTHS: [(f32, f32); 7] =
[(1.0, 1.0), (3.0, 1.0), (3.0, 2.0), (6.0, 1.0), (9.0, 2.0), (12.0, 3.0), (0.6, 0.5)];
/// Zooms, deliberately OFF the identity — at `zoom = 1.0` a Mercator unit IS a pixel and
/// the ceiling's Mercator→px conversion cannot be told from a dead one. Spans six orders
/// of magnitude, which is what the origin-local rebase exists to make survivable.
const ZOOMS: [f32; 6] = [0.25, 1.0, 4.0, 137.0, 5000.0, 100_000.0];
/// Middle-segment lengths **in screen pixels**. The short ones are far below the stroke
/// width, which is the regime the retreat overruns.
const MIDS_PX: [f32; 5] = [0.5, 2.0, 8.0, 40.0, 200.0];
/// The outer arms' length in screen pixels — long enough that their own ceilings never
/// bind, so any fault the sweep finds is the middle segment's.
const ARM_PX: f32 = 300.0;
/// The geometry is specified in **pixels** and divided by the zoom to get Mercator, so
/// the shape that lands on screen is IDENTICAL at every zoom. Two things follow: the
/// clamp's unit conversion is under test (a wrong `merc_to_px_len` changes where the
/// ceiling binds, at every zoom but one), and coordinate magnitudes stay in the range f32
/// can actually resolve — which is exactly what the live lane's origin-local rebase
/// achieves, and without it a 300 px arm at zoom 1e5 is 3·10⁷ px, where f32 spacing is
/// ~4 px and every predicate is measuring rounding noise rather than geometry.
fn staircase_px(turn_deg: f32, mid_px: f32, zoom: f32) -> Vec<[f32; 2]> {
staircase(turn_deg, mid_px / zoom, ARM_PX / zoom)
}
#[test]
fn the_clamp_holds_across_the_angle_width_and_zoom_sweep_and_nothing_holds_without_it() {
let mut cases = 0usize;
let mut clamped_faults: Vec<String> = Vec::new();
let mut cases_broken_without_clamp = 0usize;
let mut faults_without_clamp = 0usize;
// How often the ceiling actually BOUND. If this stayed 0 the sweep would be green and
// vacuous — it would only have proven that joins needing no clamp are fine.
let mut cases_where_the_ceiling_bound = 0usize;
for &turn in &ANGLES {
for &mid_px in &MIDS_PX {
for &(style_w, scale) in &WIDTHS {
for &zoom in &ZOOMS {
let pts = staircase_px(turn, mid_px, zoom);
let verts =
build_line_geometry(&[LinePolyline { pts: pts.clone(), width_px: style_w, col: [1.0; 4], depth: 0.0, dash: false }]);
let bare = unclamped(&verts);
for &feather in &[0.0f32, 1.0] {
cases += 1;
let (t, u) = frame(zoom, zoom, scale, feather);
let moved = verts.iter().zip(&bare).any(|(a, b)| {
let (pa, pb) = (applied_screen_pos(a, &t, &u), applied_screen_pos(b, &t, &u));
(pa[0] - pb[0]).abs() > 0.01 || (pa[1] - pb[1]).abs() > 0.01
});
if moved {
cases_where_the_ceiling_bound += 1;
}
let f = faults(&verts, &pts, &t, &u);
if !f.is_empty() && clamped_faults.len() < 8 {
clamped_faults.push(format!(
"turn {turn}° mid {mid_px}px width {style_w}×{scale} zoom {zoom} feather {feather}: {}",
f[0].what
));
}
let bad = faults(&bare, &pts, &t, &u);
if !bad.is_empty() {
cases_broken_without_clamp += 1;
faults_without_clamp += bad.len();
}
}
}
}
}
}
eprintln!(
"[miter] {cases} cases ({} angles × {} mid-lengths × {} widths × {} zooms × 2 feathers); \
ceiling bound in {cases_where_the_ceiling_bound}; \
WITHOUT the clamp {cases_broken_without_clamp} cases self-intersect ({faults_without_clamp} faults)",
ANGLES.len(),
MIDS_PX.len(),
WIDTHS.len(),
ZOOMS.len()
);
// ── GREEN: with the clamp, nothing self-intersects anywhere in the sweep.
assert!(
clamped_faults.is_empty(),
"the inner-miter clamp did not hold in {} of {cases} cases:\n {}",
clamped_faults.len(),
clamped_faults.join("\n ")
);
// ── RED, in the SAME run: strip the ceilings and the same assertions must break, or
// the sweep never reached a pathological case and the green above proves nothing.
assert!(
cases_broken_without_clamp > 200,
"MUTATION FAILED: with every ceiling stripped only {cases_broken_without_clamp} of {cases} cases \
self-intersect. Either the clamp is not what prevents it, or this sweep never reaches a \
pathological join — either way the green above is hollow"
);
assert!(
cases_where_the_ceiling_bound > 200,
"MUTATION FAILED: the ceiling changed a vertex position in only {cases_where_the_ceiling_bound} \
of {cases} cases — the sweep is sitting on joins that never needed clamping"
);
facett_core::testmatrix::emit(
"facett-map/gpu-line",
"inner_miter_clamp_holds_across_the_sweep",
true,
&format!(
"GFX_V2 item 5: {cases} angle×width×zoom cases self-intersection-free; ceiling bound in \
{cases_where_the_ceiling_bound}; {cases_broken_without_clamp} break with the clamp stripped"
),
);
}
/// **The ceiling is a property of the polyline, not of the frame.** The same on-screen
/// shape at six zooms three orders of magnitude apart must produce the same applied
/// pixels — that is what makes `inner_lim` correct to bake in Mercator units and convert
/// per frame, and a wrong `merc_to_px_len` fails it at every zoom but the identity.
#[test]
fn the_ceiling_converts_to_the_same_pixels_at_every_zoom() {
let mut worst = 0.0f32;
for &turn in &[45.0f32, 120.0, 170.0] {
for &mid_px in &[2.0f32, 8.0, 40.0] {
let reference: Vec<[f32; 2]> = {
let pts = staircase_px(turn, mid_px, 1.0);
let v = build_line_geometry(&[LinePolyline { pts, width_px: 9.0, col: [1.0; 4], depth: 0.0, dash: false }]);
let (t, u) = frame(1.0, 1.0, 2.0, 1.0);
v.iter().map(|x| applied_screen_pos(x, &t, &u)).collect()
};
for &zoom in &ZOOMS {
let pts = staircase_px(turn, mid_px, zoom);
let v = build_line_geometry(&[LinePolyline { pts, width_px: 9.0, col: [1.0; 4], depth: 0.0, dash: false }]);
let (t, u) = frame(zoom, zoom, 2.0, 1.0);
for (i, x) in v.iter().enumerate() {
let p = applied_screen_pos(x, &t, &u);
let e = (p[0] - reference[i][0]).abs().max((p[1] - reference[i][1]).abs());
assert!(
e < 0.05,
"turn {turn}° mid {mid_px}px: vertex {i} lands at {p:?} at zoom {zoom} but \
{:?} at zoom 1 — the ceiling's Mercator→px conversion is zoom-dependent",
reference[i]
);
worst = worst.max(e);
}
}
}
}
eprintln!("[miter] zoom invariance over {} zooms: worst vertex drift {worst:.4} px", ZOOMS.len());
assert!(worst > 0.0, "identity-value guard: every case landed on EXACTLY the reference — suspect a dead sweep");
}
/// **The reflex end, on its own**, because it is where the retreat diverges and where a
/// test that stopped at 90° would learn nothing. At 178° the raw miter is pinned to
/// `MITER_LIMIT`, so the retreat is `4·hw·sin(89°) ≈ 4·hw` — four times the stroke
/// half-width, back along a segment that is a fraction of the stroke's own width.
#[test]
fn a_near_reflex_join_on_a_segment_shorter_than_the_stroke_still_winds_forwards() {
let (t, u) = frame(60.0, 60.0, 2.0, 1.0);
// 15 px of middle segment against a 24 px stroke: the segment is narrower than the
// road is wide, the worst case there is.
let pts = staircase_px(178.0, 15.0, 60.0);
let verts = build_line_geometry(&[LinePolyline { pts: pts.clone(), width_px: 12.0, col: [1.0; 4], depth: 0.0, dash: false }]);
let clean = faults(&verts, &pts, &t, &u);
assert!(clean.is_empty(), "a 178° join on a sub-stroke segment must still be sound: {clean:#?}");
let bad = faults(&unclamped(&verts), &pts, &t, &u);
assert!(
!bad.is_empty(),
"MUTATION FAILED: the 178° / 15 px-segment case is supposed to be THE pathological one, \
yet it survives with every ceiling stripped — the case is not pathological and proves nothing"
);
eprintln!("[miter] 178° on a 15 px segment under a 24 px stroke: sound clamped, {} faults unclamped", bad.len());
eprintln!("[miter] first unclamped fault: {}", bad[0].what);
}
/// **Width invariance** — the clamp may not change the stroke width away from a join, and
/// this is where taking the ceiling from `min(L_A, L_B)` (the GFX_V2 §3.A sketch's
/// spelling) fails while the per-segment ceiling holds.
///
/// A long arm meets a short stub at a sharp corner. The arm is far longer than any
/// retreat, so its own ceiling never binds and its width must be EXACT along its whole
/// length. Under `min(L_A, L_B)` the arm inherits the stub's ceiling, its far corner is
/// dragged off the offset line, and the taper runs the entire arm.
///
/// Width is measured as the perpendicular gap between the two side EDGES at a given
/// distance along the centreline — not as the distance between interpolated corner pairs,
/// which conflates the convex corner's legitimate miter extension with a width change.
#[test]
fn the_clamp_does_not_narrow_a_long_arm_and_the_min_of_both_segments_would() {
let (t, u) = frame(4.0, 4.0, 2.0, 0.0);
// Long arm 0→(200,0), then a 2-unit stub on to (198.4,1.2): a ~143° turn.
let pts = vec![[0.0f32, 0.0], [200.0, 0.0], [198.4, 1.2]];
let style_w = 3.0f32;
let expect_w = facett_map::style::line_width_px_scaled(style_w, 2.0); // 6.0 px
let verts = build_line_geometry(&[LinePolyline { pts: pts.clone(), width_px: style_w, col: [1.0; 4], depth: 0.0, dash: false }]);
let arm_lim = verts[..6].iter().map(|v| v.inner_lim).fold(f32::MAX, f32::min);
let stub_lim = verts[6..].iter().map(|v| v.inner_lim).fold(f32::MAX, f32::min);
assert!(stub_lim < arm_lim, "the stub's ceiling is the tighter one ({stub_lim} vs {arm_lim})");
// The mutation: give the ARM the stub's ceiling — literally `min(L_A, L_B)`.
let min_of_both: Vec<LineVertex> = verts
.iter()
.map(|v| LineVertex { inner_lim: if v.inner_lim < NO_INNER_LIMIT { stub_lim } else { v.inner_lim }, ..*v })
.collect();
let s0 = t.project(pts[0]);
let s1 = t.project(pts[1]);
let seg_px = along(s1, s0, s1);
// The perpendicular offset of one side's EDGE at `s` px along the centreline.
let edge_at = |c0: [f32; 2], c1: [f32; 2], s: f32| -> f32 {
let (a0, x0) = (along(c0, s0, s1), across(c0, s0, s1));
let (a1, x1) = (along(c1, s0, s1), across(c1, s0, s1));
x0 + (x1 - x0) * (s - a0) / (a1 - a0)
};
let width_at = |vs: &[LineVertex], f: f32| -> f32 {
let p = |i: usize| applied_screen_pos(&vs[i], &t, &u);
let s = f * seg_px;
edge_at(p(0), p(2), s) - edge_at(p(1), p(4), s)
};
let mut worst_ok = 0.0f32;
let mut worst_mut = 0.0f32;
for f in [0.0f32, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
let ok = width_at(&verts, f);
let mutated = width_at(&min_of_both, f);
eprintln!(
"[miter] width at {:>3.0}% of the {seg_px:.0} px arm: per-segment {ok:.3} px, \
min(L_A,L_B) {mutated:.3} px (want {expect_w:.3})",
f * 100.0
);
worst_ok = worst_ok.max((ok - expect_w).abs());
worst_mut = worst_mut.max((mutated - expect_w).abs());
}
assert!(
worst_ok < 1.0e-3,
"the per-segment ceiling must leave the long arm at exactly {expect_w:.3} px — worst error {worst_ok:.4} px"
);
// RED: the sketch's spelling visibly narrows the same arm.
assert!(
worst_mut > 1.0,
"MUTATION FAILED: giving the long arm the stub's ceiling — the `min(L_A,L_B)` the GFX_V2 §3.A \
sketch specifies — was supposed to taper it, but the worst error is only {worst_mut:.4} px. \
Then the per-segment choice is not what preserves the width and this test proves nothing"
);
eprintln!("[miter] per-segment worst width error {worst_ok:.5} px; min(L_A,L_B) worst {worst_mut:.3} px");
}
/// A **straight** polyline, **gentle bends** and a **zigzag** must come out bit-identical
/// to what they were before the clamp existed: no ceiling may bind where there is no
/// pathology. The straight case is what the live map is almost entirely made of.
#[test]
fn the_clamp_is_inert_where_there_is_nothing_to_clamp() {
let (t, u) = frame(137.0, 137.0, 2.0, 1.0);
let cases: [(&str, Vec<[f32; 2]>); 3] = [
("straight", vec![[0.0, 0.0], [50.0, 0.0], [100.0, 0.0], [150.0, 0.0]]),
("gentle bends", vec![[0.0, 0.0], [80.0, 4.0], [160.0, -4.0], [240.0, 2.0]]),
// Alternating handedness: the concave corners land on OPPOSITE sides of the
// middle segment, so they cannot cross however sharp the turns get.
("zigzag", vec![[0.0, 0.0], [40.0, 30.0], [80.0, 0.0], [120.0, 30.0]]),
];
for (name, pts) in cases {
let verts = build_line_geometry(&[LinePolyline { pts, width_px: 6.0, col: [1.0; 4], depth: 0.0, dash: false }]);
for (i, (a, b)) in verts.iter().zip(unclamped(&verts)).enumerate() {
let (pa, pb) = (applied_screen_pos(a, &t, &u), applied_screen_pos(&b, &t, &u));
assert_eq!(
pa, pb,
"{name}: vertex {i} moved when the clamp was applied ({pa:?} vs unclamped {pb:?}) — \
the clamp is biting where nothing needed bounding"
);
}
eprintln!("[miter] {name}: all {} vertices bit-identical clamped and unclamped", verts.len());
}
}
/// A **non-uniform fit** (`MapView`'s anisotropic zoom) still may not self-intersect. The
/// ceiling is baked from Mercator-space lengths and angles, which a stretched view is not
/// a similarity transform of, so `merc_to_px_len` divides the allowance by the anisotropy
/// ratio to stay CONSERVATIVE — it may clamp harder than needed, never looser. This is
/// the assertion that the compensation is enough.
#[test]
fn a_non_uniform_fit_does_not_self_intersect_either() {
let mut bound = 0usize;
for &(zx, zy) in &[(3.0f32, 1.0f32), (1.0, 3.0), (40.0, 5.0), (5.0, 40.0)] {
for &turn in &ANGLES {
for &mid in &[1.0f32, 5.0, 30.0] {
let pts = staircase(turn, mid, 300.0);
let verts =
build_line_geometry(&[LinePolyline { pts: pts.clone(), width_px: 9.0, col: [1.0; 4], depth: 0.0, dash: false }]);
let (t, u) = frame(zx, zy, 2.0, 1.0);
let f = faults(&verts, &pts, &t, &u);
assert!(f.is_empty(), "anisotropic zoom {zx}×{zy}, turn {turn}°, mid {mid}: {:#?}", f);
if faults(&unclamped(&verts), &pts, &t, &u).is_empty() {
continue;
}
bound += 1;
}
}
}
eprintln!("[miter] anisotropic: {bound} cases that self-intersect unclamped are sound clamped");
assert!(bound > 20, "MUTATION FAILED: the anisotropic sweep found only {bound} pathological cases");
}