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/miter_geom/mod.rs

//! **Shared geometry support for the inner-miter clamp proofs** (GFX_V2 §3.A).
//!
//! Used by BOTH `inner_miter_clamp.rs` (the pure-CPU angle × width × zoom sweep) and
//! `line_gpu.rs` (the real-device pixel proof), so the pathological shape family and the
//! self-intersection predicates cannot drift between them (LAW #5). The GPU suite asserts
//! the device's pixels against the same [`applied_screen_pos`] model this file judges, so
//! a predicate written twice would be two chances to be wrong.
#![allow(dead_code, reason = "each consumer uses a subset; the module is shared support")]

use facett_map::gpu::line::{LineVertex, MapLineUniforms, NO_INNER_LIMIT, applied_screen_pos, decode_round_end};
use facett_map::gpu::MapTransform;

// ── The pathological family ───────────────────────────────────────────────────

/// A **staircase**: two turns of the same angle and the same handedness with a short
/// segment between them — the shape that bowties. `mid` is the middle segment's length
/// and `arm` the two outer arms', all in origin-local Mercator units.
///
/// Both turns are LEFT turns, so both concave corners land on `side = +1` of the middle
/// segment and their retreats eat it from both ends. A zigzag (alternating handedness)
/// puts them on opposite sides and cannot bowtie, which is why the sweep uses this one.
pub fn staircase(turn_deg: f32, mid: f32, arm: f32) -> Vec<[f32; 2]> {
    let t = turn_deg.to_radians();
    let (c, s) = (t.cos(), t.sin());
    let p1 = [0.0f32, 0.0];
    let p2 = [mid, 0.0];
    // Incoming direction is (1,0) rotated by −τ, so the turn onto (1,0) is +τ (left).
    let p0 = [p1[0] - arm * c, p1[1] + arm * s];
    // Outgoing is (1,0) rotated by +τ.
    let p3 = [p2[0] + arm * c, p2[1] + arm * s];
    vec![p0, p1, p2, p3]
}

/// The transform + uniforms a frame would carry, for a given viewport, zoom and width
/// scale. Origin, centre and `ref` are all zero, so an origin-local Mercator coordinate
/// at `zoom = 1` is a pixel offset from the pane centre.
pub fn frame(
    w: f32,
    h: f32,
    zoom_x: f32,
    zoom_y: f32,
    width_scale: f32,
    feather_px: f32,
) -> (MapTransform, MapLineUniforms) {
    let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(w, h));
    let t = MapTransform::new([0.0, 0.0], [0.0, 0.0], zoom_x, zoom_y, rect);
    let u = MapLineUniforms::new(&t, w, h, feather_px, 0.0, None).with_width_scale(width_scale);
    (t, u)
}

// ── Geometric predicates on the APPLIED positions ─────────────────────────────

/// Do two screen-space segments properly cross? Used to decide whether an emitted quad
/// is a **simple polygon**, which is the exact statement of "does not self-intersect".
///
/// Proper crossing only: touching endpoints and collinear overlap do not count, because
/// maximal clamping is DESIGNED to collapse a concave edge to zero length (the ceiling
/// gives `a_start + a_end ≤ L`, with equality allowed) and that must read as sound.
pub fn crosses(p1: [f32; 2], p2: [f32; 2], p3: [f32; 2], p4: [f32; 2]) -> bool {
    let d = |a: [f32; 2], b: [f32; 2], c: [f32; 2]| {
        (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
    };
    let (d1, d2, d3, d4) = (d(p3, p4, p1), d(p3, p4, p2), d(p1, p2, p3), d(p1, p2, p4));
    ((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0)) && d1 != 0.0 && d2 != 0.0 && d3 != 0.0 && d4 != 0.0
}

/// How far a point sits **along** a segment, in px, measured from its start.
pub fn along(p: [f32; 2], s0: [f32; 2], s1: [f32; 2]) -> f32 {
    let d = [s1[0] - s0[0], s1[1] - s0[1]];
    let l = (d[0] * d[0] + d[1] * d[1]).sqrt().max(1e-20);
    ((p[0] - s0[0]) * d[0] + (p[1] - s0[1]) * d[1]) / l
}

/// Perpendicular (signed) distance from the segment's line, in px.
pub fn across(p: [f32; 2], s0: [f32; 2], s1: [f32; 2]) -> f32 {
    let d = [s1[0] - s0[0], s1[1] - s0[1]];
    let l = (d[0] * d[0] + d[1] * d[1]).sqrt().max(1e-20);
    ((p[0] - s0[0]) * -d[1] + (p[1] - s0[1]) * d[0]) / l
}

/// What went wrong in one segment of one case.
#[derive(Debug, Clone)]
pub struct Fault {
    pub what: String,
}

/// Check every quad of an expanded polyline. Returns the faults found.
///
/// Two predicates, and the choice between them is load-bearing:
///
/// * **the quad is a simple polygon** — neither pair of non-adjacent edges crosses. This
///   is self-intersection stated exactly. A signed-area/winding test was tried first and
///   REJECTED: it also fires on a legitimately non-convex "dart" quad, which is what a
///   near-reflex join under [`MITER_LIMIT`] produces whether or not the inner clamp is
///   there — it flagged 8 pre-existing cases that are equally flagged with the clamp
///   stripped, i.e. it was not measuring this change. Note the bowtie is caught by the
///   **end-cap** pair, not the side pair: when a concave edge reverses, the two long
///   edges stay on their own sides of the centreline and never meet, while the start and
///   end caps overlap in `along` and cross. A crossing test on the side edges alone
///   would be a hollow guard.
/// * **no concave edge runs backwards** — the `along` span of each side's edge must be
///   non-negative. This is "the inner vertex does not cross the segment" in the one
///   quantity that can express it, and it localises a bowtie to a side and a segment.
pub fn faults(verts: &[LineVertex], pts: &[[f32; 2]], t: &MapTransform, u: &MapLineUniforms) -> Vec<Fault> {
    let mut out = Vec::new();
    // Sub-thousandth of a pixel. Maximal clamping collapses a concave edge to ZERO
    // length by design, so zero is sound and only a genuine reversal is a fault.
    const EPS_PX: f32 = 1.0e-3;
    for (seg, chunk) in verts.as_chunks::<6>().0.iter().enumerate() {
        // Lane GG #25 appends the ROUND ENDS (cap / joint half-discs) after the quads.
        // They belong to no segment — there is no `pts[seg]` for them and no concave
        // corner to bound — so the clamp predicates stop here. Their own proof is
        // `line_gpu::a_dead_end_road_ends_in_a_semicircle_not_a_bar`.
        if decode_round_end(chunk[0].miter).0 {
            break;
        }
        let p = |v: &LineVertex| applied_screen_pos(v, t, u);
        // Emission order per segment: [a_pos, a_neg, b_pos, a_neg, b_neg, b_pos].
        let (a_pos, a_neg, b_pos, b_neg) = (p(&chunk[0]), p(&chunk[1]), p(&chunk[2]), p(&chunk[4]));
        let s0 = t.project(pts[seg]);
        let s1 = t.project(pts[seg + 1]);

        if crosses(a_pos, b_pos, a_neg, b_neg) {
            out.push(Fault { what: format!("segment {seg}: the two SIDE edges cross — the quad is a bowtie") });
        }
        if crosses(a_pos, a_neg, b_pos, b_neg) {
            out.push(Fault {
                what: format!(
                    "segment {seg}: the START and END caps cross — the ribbon doubles back on itself \
                     (concave spans: +1 {:.3}px, −1 {:.3}px of a {:.3}px segment)",
                    along(b_pos, s0, s1) - along(a_pos, s0, s1),
                    along(b_neg, s0, s1) - along(a_neg, s0, s1),
                    along(s1, s0, s1)
                ),
            });
        }
        for (name, start, end) in [("+1", a_pos, b_pos), ("−1", a_neg, b_neg)] {
            let span = along(end, s0, s1) - along(start, s0, s1);
            if span < -EPS_PX {
                out.push(Fault {
                    what: format!(
                        "segment {seg} side {name} edge runs BACKWARDS by {:.4} px — its two corners crossed",
                        -span
                    ),
                });
            }
        }
    }
    out
}


/// Strip every ceiling — the clamp, disabled by DATA on the exact same code path. This
/// is the mutation, and it runs inside the test rather than being something a reader has
/// to take on trust.
pub fn unclamped(verts: &[LineVertex]) -> Vec<LineVertex> {
    verts.iter().map(|v| LineVertex { inner_lim: NO_INNER_LIMIT, ..*v }).collect()
}

/// **The QUAD half of an expansion** — the round ends (lane GG #25) dropped.
///
/// The inner-miter proofs model coverage as a set of TRIANGLES, and a round end is
/// deliberately not one: its two triangles bound a half-disc that the fragment stage
/// carves a circle out of, so a triangle model would claim the four corners the shader
/// discards. Rather than teach the clamp's model a second shape, the clamp is proven on
/// the geometry it is about. The discs have their own oracle on the same device
/// (`line_gpu::a_dead_end_road_ends_in_a_semicircle_not_a_bar`).
pub fn quads_only(verts: &[LineVertex]) -> Vec<LineVertex> {
    verts.iter().filter(|v| !decode_round_end(v.miter).0).copied().collect()
}