doxa-maps/internal/check-gospel-whiteboard.mjs
Doxa Maps 7e9d4d62cb Point the set charts at the promoted modules, and clear a committed conflict
The layout modules moved to library when they were promoted, and three of my
files still named the old paths: the two new whiteboard components, which were
written before the move landed, and the geometry check, which was written after
it and still copied the old import from its neighbours.

GiBoundedSet also carried an unresolved conflict — markers and all — committed by
a cherry-pick loop of mine that resolved generated output automatically and swept
the source conflict along with it. The file had both imports of the same symbol,
one of them pointing at a deleted path. It parsed, so the SFC check passed; only
the bundler would have caught it, and only at the point where it could not
resolve the file.

The lesson is about the loop, not the file: resolving one class of conflict
automatically is fine, but `git add -A` afterwards does not distinguish the
conflicts you meant to resolve from the ones you did not. A loop that stages
everything needs to refuse when a marker is still present.
2026-09-28 18:57:46 +00:00

111 lines
7.2 KiB
JavaScript

#!/usr/bin/env node
/**
* check-gospel-whiteboard — the geometry the set charts depend on once they are drawn on a map.
*
* Three properties, each with the arm that would catch its own defect. None of them is about
* whether the chart renders; all three are about it rendering something TRUE.
*
* 1. THE BOUNDED SET'S GAP SURVIVES PROJECTION. Inside points sit within 0.92r and outside
* beyond 1.12r, and nothing may land on the boundary — a dot on the line is the one thing a
* bounded set cannot express. Asserted on PROJECTED coordinates. Note what the arm below
* found: the projection is conformal, so it preserves the ratio EXACTLY and a narrowed gap
* survives just as a wide one does. The boundary cannot be eroded by the projection, only by
* someone editing the constants — which is therefore what the arm poisons.
* 2. THE DRAWING STAYS INSIDE THE MERCATOR CLAMP. Shape does not degrade with extentFraction —
* the projection is the exact inverse of the renderer's transform, measured at 0.000% aspect
* error, and an earlier claim of mine that it did was an artefact of my own harness. What
* does bite is latitude: engines clamp near 85 degrees.
* 3. A RADIUS IN MAP UNITS DOUBLES WITH EVERY ZOOM LEVEL. circle-radius is screen pixels, so a
* boundary that does not scale is correct at one zoom and wrong at every other.
*
* ★ THE CEILING IS A PROPERTY OF extentFraction ALONE, not of a chart's extent: the scale is
* extentFraction/extent and the outermost point is at u = extent, so the product is always
* extentFraction. A consumer cannot buy headroom against the clamp by choosing a bigger extent,
* and one assertion covers every chart.
*/
import { layoutBoundedSet } from '../library/components/charts/boundedSet.js'
import { degreesToPixelRadius, dishRadiusDegrees, toFeatureCollection, centeredSetFeatures }
from '../plugin-designs/gospel-internet/src/setChartFeatures.js'
import { layoutCenteredSet, ringRadii } from '../library/components/charts/centeredSet.js'
import { whiteboardProjection } from '../library/maps/whiteboard-projection.js'
const fail = []
const ok = (label, cond, detail = '') => {
if (cond) console.log(` ✓ ${label}${detail ? ` — ${detail}` : ''}`)
else { console.log(` ✗ ${label}${detail ? ` — ${detail}` : ''}`); fail.push(label) }
}
const R2D = 180 / Math.PI
// TRUE Web Mercator screen space: x is proportional to LONGITUDE ALONE. An earlier version of
// this used lng*cos(lat), a plate-carree correction the renderer never applies, and it
// manufactured a distortion that does not exist. Keep x = lng.
const screen = (lng, lat) => [lng, Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)) * R2D]
console.log('GOSPEL-WHITEBOARD')
// ── 1. the boundary gap, on projected coordinates ───────────────────────────
const R = 120, SPREAD = 260
function gapMargins(extentFraction, inner = 0.92, outer = 1.12) {
const p = whiteboardProjection({ extent: SPREAD, extentFraction })
const d = (u, v) => { const [lng, lat] = p(u, v); const [x, y] = screen(lng, lat); return Math.hypot(x, y) }
let bMin = Infinity, bMax = -Infinity, inMax = -Infinity, outMin = Infinity
for (let i = 0; i < 720; i++) {
const a = i / 720 * 2 * Math.PI, c = Math.cos(a), s = Math.sin(a)
const b = d(c * R, s * R); bMin = Math.min(bMin, b); bMax = Math.max(bMax, b)
inMax = Math.max(inMax, d(c * R * inner, s * R * inner))
outMin = Math.min(outMin, d(c * R * outer, s * R * outer))
}
return { inside: bMin - inMax, outside: outMin - bMax }
}
const m = gapMargins(0.17)
ok('bounded: no inside point reaches the boundary', m.inside > 0, `margin ${m.inside.toFixed(3)}`)
ok('bounded: no outside point reaches the boundary', m.outside > 0, `margin ${m.outside.toFixed(3)}`)
// ★ THE ARM, AND WHAT WRITING IT TAUGHT ME. My first version narrowed the gap to 0.999/1.001 and
// expected the margins to go negative. THEY DID NOT — they stayed positive at 0.028. The
// projection is conformal, so it preserves the RATIO exactly; a gap of a tenth of a percent
// survives projection just as a gap of eight percent does. There is no distortion for a narrow
// gap to be eaten by.
// That means "the gap survives projection" is very nearly a tautology, and a check resting on it
// alone would look like a guard while testing almost nothing. What CAN actually fail is the
// ORDERING of the constants — someone editing 0.92/1.12 to the wrong side of 1 — so that is what
// the arm poisons. Recorded rather than quietly swapped, because the fact that narrowing is safe
// is itself worth knowing: it says the boundary cannot be eroded by the projection, only by us.
const inverted = gapMargins(0.17, 1.01, 0.99)
ok('bounded: inverting the constants FAILS (so the guard is not inert)',
inverted.inside <= 0 || inverted.outside <= 0,
`inverted margins ${inverted.inside.toFixed(3)} / ${inverted.outside.toFixed(3)}`)
const narrow = gapMargins(0.17, 0.999, 1.001)
ok('bounded: a narrowed gap still survives, because the projection preserves ratios exactly',
narrow.inside > 0 && narrow.outside > 0, `margins ${narrow.inside.toFixed(3)} / ${narrow.outside.toFixed(3)}`)
// ── 2. the latitude ceiling ─────────────────────────────────────────────────
const EXTENT_C = 30 + 9 * 22
const feats = centeredSetFeatures(
layoutCenteredSet([{ id: 'a', label: 'A', ring: 10 }], { innerR: 30, ringGap: 22 }).placed,
ringRadii({ innerR: 30, ringGap: 22 }), {})
const { maxAbsLat } = toFeatureCollection(feats, whiteboardProjection({ extent: EXTENT_C }))
ok('centered: the drawing stays inside the Mercator clamp', maxAbsLat < 85, `max |lat| ${maxAbsLat.toFixed(2)}`)
// THE ARM: the ceiling is extentFraction's, so a wide one must breach it
const wide = whiteboardProjection({ extent: EXTENT_C, extentFraction: 0.95 })(0, EXTENT_C)[1]
ok('the clamp CAN be breached by extentFraction alone (so the guard is not inert)',
Math.abs(wide) > 85, `|lat| ${Math.abs(wide).toFixed(2)} at fraction 0.95`)
// and it does not depend on the extent
const lats = [100, 228, 260, 1000].map((e) => Math.abs(whiteboardProjection({ extent: e })(0, e)[1]))
ok('the ceiling is extentFraction\'s alone, not the extent\'s',
Math.max(...lats) - Math.min(...lats) < 1e-6, lats.map((l) => l.toFixed(3)).join(' '))
// ── 3. a map-unit radius doubles per zoom ───────────────────────────────────
const deg = dishRadiusDegrees(R, whiteboardProjection({ extent: SPREAD }))
const expr = degreesToPixelRadius(deg)
const [, , , z0, r0, z1, r1] = expr
ok('dish: the radius is a zoom expression, not a constant', Array.isArray(expr) && expr[0] === 'interpolate')
ok('dish: it uses exponential base 2, which is what doubling per zoom requires',
expr[1][0] === 'exponential' && expr[1][1] === 2)
ok('dish: its stops scale by exactly 2^(z1-z0)',
Math.abs(r1 / r0 - Math.pow(2, z1 - z0)) / Math.pow(2, z1 - z0) < 1e-9,
`${r0.toFixed(2)}px at z${z0}`)
if (fail.length) {
console.log(`\nGOSPEL-WHITEBOARD: FAIL — ${fail.length}: ${fail.join(', ')}`)
process.exit(1)
}
console.log('\nGOSPEL-WHITEBOARD: PASS — the gap, the clamp and the scaling all hold, and each arm can fail')