Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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");
}