facett-core 0.1.18

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
// ─────────────────────────────────────────────────────────────────────────────
// THE shared WGSL prelude — the ONE writer of the geometry maths every facett
// line/SDF shader repeats.
//
// WGSL has no `#include`, so this file is a text PRELUDE: `render::gpu::wgsl()`
// concatenates it in front of a shader body before `create_shader_module`. Every
// consumer (facett-core's `sdf.wgsl` / `line.wgsl` / `draw.wgsl`, facett-map's
// `line.wgsl`) goes through that one composer, so a change to the maths below lands
// in exactly one place instead of the five copies this replaced.
//
// LAW #5: reuse, do not twin — fixed BY CONSTRUCTION. The byte-identity guard that
// used to watch two copies of `cull.wgsl` / `draw.wgsl` agree is gone, because there
// is no longer a second copy to disagree.
//
// NOTHING here may change a rendered pixel. Each function below is the EXACT
// expression, in the exact association order, that the shader it was lifted out of
// evaluated — see the per-function notes.
// ─────────────────────────────────────────────────────────────────────────────

/// Screen pixel → clip space. Screen y runs DOWN from the top-left; clip y runs UP,
/// hence the flip.
///
/// The two spellings this replaces — `px.x / vp.x * 2.0 - 1.0` (the SDF lanes) and
/// `px.x / (vp.x * 0.5) - 1.0` (the map lanes) — are BIT-identical, not merely equal:
/// `vp * 0.5` is exact (a power-of-two scale) and rounding commutes with scaling by
/// two, so `2·fl(x/vp) == fl(x/(vp/2))`. Unifying them therefore cannot move a pixel.
fn px_to_ndc(px: vec2<f32>, viewport: vec2<f32>) -> vec2<f32> {
    return vec2<f32>(px.x / (viewport.x * 0.5) - 1.0, 1.0 - px.y / (viewport.y * 0.5));
}

/// Anti-aliased coverage from an OUTWARD signed distance: `d < 0` inside the shape,
/// `d > 0` outside, feathered over ±`aa`. `aa <= 0` gives a hard (aliased) edge.
///
/// Byte-for-byte the CPU `render::cpu::sdf::coverage_from_sd`, which is what makes a
/// GPU frame match a CPU frame in `tests/gpu_sdf_parity.rs`. Change one, change both.
fn coverage_from_sd(d: f32, aa: f32) -> f32 {
    if (aa <= 0.0) {
        return select(0.0, 1.0, d <= 0.0);
    }
    return 1.0 - smoothstep(-aa, aa, d);
}

/// Anti-aliased coverage from an INWARD distance — deck.gl's convention, where
/// `inside_px` is how far the fragment sits *inside* the drawn edge (0 at the
/// boundary, growing toward the centreline).
///
/// This is deck.gl's `smoothedge`: a HALF-pixel smoothstep rather than a linear ramp
/// over a whole pixel, which is why their roads read crisper than a naive feather.
///
/// The ramp is **centred on the boundary**: 50% coverage at `inside_px == 0`, 0 at
/// `-feather_px` (outside), 1 at `+feather_px` (inside). That is the definition of
/// unbiased antialiasing — coverage is the fraction of the pixel the shape covers —
/// and it is what makes the drawn width integrate to `w_px` EXACTLY, so the GPU line
/// lane and `facett_map::style::line_width_px_scaled` resolve one width, not two.
///
/// **A caller MUST expand its rasterised geometry by `feather_px`** so the outer half
/// of the ramp has fragments to land on; otherwise the quad edge truncates the ramp at
/// 50% and you get a hard step (see `facett-map/src/gpu/line.wgsl`'s `hw_ext`). This
/// bias used to be baked in here instead — `smoothstep(-r, r, inside_px - r)`, whose
/// 50% crossing sat one feather INSIDE the boundary — which silently drew every road
/// `2·feather_px` narrower than nominal. Measured before the fix: a nominal 3.000 px
/// road drew 2.002 px.
///
/// It is exactly `coverage_from_sd` with the sign flipped (inward → outward distance),
/// so the two conventions share ONE ramp and cannot drift apart (LAW #5).
fn coverage_inside(inside_px: f32, feather_px: f32) -> f32 {
    return coverage_from_sd(-inside_px, max(feather_px, 0.0001));
}

/// deck.gl `PathLayer`'s width resolution: scale a themed stroke width for THIS
/// frame's view, then clamp it into the pixel band.
///
/// **This is the GPU twin of `facett_map::style::line_width_px_scaled`, and the two
/// MUST agree.** They once did not: the CPU painter scaled by `clamp(zoom/50_000,
/// 0.5, 3.0)` while the GPU baked `width × 1.0` once into the vertex buffer, and the
/// map drew its land and its roads at two different scales. A positional parity test
/// cannot see that — only WIDTH diverges — so the net that guards it measures drawn
/// pixels (see `coverage_inside` above).
fn resolve_line_width_px(style_width: f32, width_scale: f32, min_px: f32, max_px: f32) -> f32 {
    return clamp(style_width * width_scale, min_px, max_px);
}

// ── The 2D ground frame — the ONE writer of the map's rotate + foreshorten ────
//
// `facett_core::render::GroundFrame2d::apply`, transcribed. The CPU painter
// (`MapView::project`) has always applied it; the GPU lanes did not, and that is
// why a rotated camera had to fall back to the CPU painter entirely — measured
// 2026-08-03 on a real 1600x1000 window / RTX 4090: the line lane's `lane_frames`
// went 249 -> 0 and the frame went 0.857 ms -> 12.28 ms the instant the bearing
// left zero, because `MapTransform` carried zoom/ref/screen_center and no rotation
// term at all.
//
// The operation ORDER is load-bearing and is not an accident: rotate, THEN scale
// the rotated y by `cos(tilt)`. Folding the foreshorten into the rotation rows
// distributes a multiply over an addition, which is not exact in floating point —
// measured over 400k draws it differs in 52.5 % of cases, always by one ULP. See
// `GroundFrame2d`'s own note; this is its GPU twin and must match it operation for
// operation.
fn ground_apply(d: vec2<f32>, rot: vec4<f32>, foreshorten: f32) -> vec2<f32> {
    let rx = rot.x * d.x + rot.y * d.y;
    let ry = rot.z * d.x + rot.w * d.y;
    return vec2<f32>(rx, ry * foreshorten);
}

/// **Origin-local Mercator vertex -> logical screen pixel**, through the full 2D
/// camera: subtract-before-scale (the high-zoom f32 cancellation fix), then the
/// ground frame, then the pane centre.
///
/// Mirrors the CPU `MapView::project` exactly:
///     dx = (merc - centre) * world_px  ;  (rx, ry) = frame.apply(dx, dy)
///     screen = pane_centre + (rx, ry)
/// with `pos - ref` standing in for `merc - centre` in origin-local space.
///
/// At bearing 0 / tilt 0 the frame is the identity (`rot = (1,0,0,1)`,
/// `foreshorten = 1`) and this reduces to `(pos - ref) * zoom + screen_center` —
/// the expression it replaced, term for term, so the north-up frame cannot move.
fn project_ground_px(
    pos:           vec2<f32>,
    ref_xy:        vec2<f32>,
    zoom:          vec2<f32>,
    rot:           vec4<f32>,
    foreshorten:   f32,
    screen_center: vec2<f32>,
) -> vec2<f32> {
    let d = vec2<f32>((pos.x - ref_xy.x) * zoom.x, (pos.y - ref_xy.y) * zoom.y);
    return ground_apply(d, rot, foreshorten) + screen_center;
}