Files
zeekayandHanzo Dev 78062c4a99 cut the prose
Globe.tsx was 23% comment. The code says what it does; the comments now say only
what it cannot — why sidereal and not a phase, why one seam carries no cross-links,
why a terminal tracks one satellite. Same for Nav, the tests, the workflow and
LLM.md.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-10 17:18:35 -07:00

92 lines
3.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
Rasterises coastlines into `src/content/land.ts`.
Run: node scripts/land.mjs path/to/countries.geojson
Scanline by latitude row, even-odd fill — so lakes fall out without ring winding,
and it is ~200x less work than point-in-polygon per cell.
*/
import { readFileSync, writeFileSync } from 'node:fs'
const STEP = 1.5
const COLS = Math.round(360 / STEP)
const ROWS = Math.round(180 / STEP)
const src = process.argv[2]
if (!src) {
console.error('usage: node scripts/land.mjs <countries.geojson>')
process.exit(1)
}
/** Every ring of every feature, flattened — the fill rule does not care which is which. */
const rings = []
for (const f of JSON.parse(readFileSync(src, 'utf8')).features) {
const g = f.geometry
if (!g) continue
if (g.type === 'Polygon') rings.push(...g.coordinates)
else if (g.type === 'MultiPolygon') for (const poly of g.coordinates) rings.push(...poly)
}
const bits = new Uint8Array(Math.ceil((COLS * ROWS) / 8))
const set = (col, row) => {
const i = row * COLS + col
bits[i >> 3] |= 1 << (i & 7)
}
for (let row = 0; row < ROWS; row++) {
// Cell CENTRE, not its edge: a row sampled at its boundary sits exactly on the
// coastline for any coast that runs east-west, and picks up a rounding coin flip.
const lat = -90 + (row + 0.5) * STEP
const xs = []
for (const ring of rings) {
for (let i = 0; i < ring.length - 1; i++) {
const [lon1, lat1] = ring[i]
const [lon2, lat2] = ring[i + 1]
// Half-open in latitude so a vertex exactly on the row counts once, not twice.
if (lat1 <= lat === lat2 <= lat) continue
xs.push(lon1 + ((lat - lat1) / (lat2 - lat1)) * (lon2 - lon1))
}
}
if (xs.length < 2) continue
xs.sort((a, b) => a - b)
for (let i = 0; i + 1 < xs.length; i += 2) {
const from = Math.ceil((xs[i] + 180) / STEP - 0.5)
const to = Math.floor((xs[i + 1] + 180) / STEP - 0.5)
for (let col = Math.max(0, from); col <= Math.min(COLS - 1, to); col++) set(col, row)
}
}
let land = 0
for (const b of bits) land += b.toString(2).replace(/0/g, '').length
writeFileSync(
new URL('../src/content/land.ts', import.meta.url),
`/*
Where the land is — ${COLS}×${ROWS} cells of ${STEP}°, one bit each, row 0 at the
south pole, column 0 at 180°W. ${land} land cells. Generated by scripts/land.mjs.
A mask, not a list of dots: the renderer picks its own lattice and asks this.
*/
export const LAND_STEP = ${STEP}
export const LAND_COLS = ${COLS}
export const LAND_ROWS = ${ROWS}
const PACKED =
'${Buffer.from(bits).toString('base64')}'
const bits_ = typeof atob === 'function'
? Uint8Array.from(atob(PACKED), (c) => c.charCodeAt(0))
: Uint8Array.from(Buffer.from(PACKED, 'base64'))
/** True where (lat, lon) in degrees is over land. */
export function isLand(lat: number, lon: number): boolean {
const row = Math.floor((lat + 90) / LAND_STEP)
const col = Math.floor(((((lon + 180) % 360) + 360) % 360) / LAND_STEP)
if (row < 0 || row >= LAND_ROWS) return false
const i = row * LAND_COLS + col
return (bits_[i >> 3] & (1 << (i & 7))) !== 0
}
`,
)
console.log(`land.ts: ${COLS}x${ROWS} @ ${STEP}deg, ${land} land cells (${((land / (COLS * ROWS)) * 100).toFixed(1)}%), ${bits.length} bytes`)