@mailwoman/map-tui 10.0.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +5 -6
  2. package/{lib/cli → cli}/args.ts +49 -28
  3. package/{lib/cli/index.ts → cli/main.ts} +12 -23
  4. package/lib/browser.ts +33 -76
  5. package/lib/frame.ts +50 -28
  6. package/lib/input.ts +46 -69
  7. package/lib/mercator.ts +13 -12
  8. package/lib/mvt.ts +2 -2
  9. package/lib/raster.ts +53 -27
  10. package/lib/renderer.ts +19 -38
  11. package/lib/style.ts +21 -15
  12. package/lib/tile-source.ts +30 -19
  13. package/out/browser.d.ts +24 -33
  14. package/out/browser.d.ts.map +1 -1
  15. package/out/browser.js +27 -72
  16. package/out/browser.js.map +1 -1
  17. package/out/cli/args.d.ts +18 -8
  18. package/out/cli/args.d.ts.map +1 -1
  19. package/out/cli/args.js +41 -25
  20. package/out/cli/args.js.map +1 -1
  21. package/out/cli/{index.d.ts → main.d.ts} +1 -1
  22. package/out/cli/main.d.ts.map +1 -0
  23. package/out/cli/{index.js → main.js} +13 -24
  24. package/out/cli/main.js.map +1 -0
  25. package/out/cli/tsconfig.tsbuildinfo +1 -0
  26. package/out/frame.d.ts +40 -23
  27. package/out/frame.d.ts.map +1 -1
  28. package/out/frame.js +48 -26
  29. package/out/frame.js.map +1 -1
  30. package/out/input.d.ts +18 -36
  31. package/out/input.d.ts.map +1 -1
  32. package/out/input.js +37 -63
  33. package/out/input.js.map +1 -1
  34. package/out/mercator.d.ts +12 -11
  35. package/out/mercator.d.ts.map +1 -1
  36. package/out/mercator.js +4 -10
  37. package/out/mercator.js.map +1 -1
  38. package/out/mvt.js +2 -2
  39. package/out/raster.d.ts +45 -23
  40. package/out/raster.d.ts.map +1 -1
  41. package/out/raster.js +44 -22
  42. package/out/raster.js.map +1 -1
  43. package/out/renderer.d.ts +6 -7
  44. package/out/renderer.d.ts.map +1 -1
  45. package/out/renderer.js +13 -22
  46. package/out/renderer.js.map +1 -1
  47. package/out/style.d.ts +17 -11
  48. package/out/style.d.ts.map +1 -1
  49. package/out/style.js +11 -9
  50. package/out/style.js.map +1 -1
  51. package/out/tile-source.d.ts +12 -8
  52. package/out/tile-source.d.ts.map +1 -1
  53. package/out/tile-source.js +21 -14
  54. package/out/tile-source.js.map +1 -1
  55. package/out/tsconfig.test.tsbuildinfo +1 -1
  56. package/package.json +35 -83
  57. package/out/cli/index.d.ts.map +0 -1
  58. package/out/cli/index.js.map +0 -1
package/lib/frame.ts CHANGED
@@ -7,11 +7,15 @@
7
7
  /**
8
8
  * Braille frame value + conversion for map-tui's debug view.
9
9
  *
10
- * `rasterizeToFrame` turns an `RGBAGrid` (drawn by ./raster.ts) into a `MapFrame`: one codepoint and one packed color
11
- * per cell. The braille dither/luminance work is asciify's — `FrameRasterizer` subclasses `AsciifyTerminal` with a
12
- * no-op sink purely to reach its protected `_computeBrailleCells`, `_cellChars`, `_cellColors`, so this module never
13
- * re-implements the dot math. `frameToANSILines` and `overlayText` then work on the plain `MapFrame` value, with no
14
- * further asciify dependency; `blitFrame` is the path back the other way, for callers driving a live terminal.
10
+ * `rasterizeToFrame` turns an `RGBAGrid` (drawn by ./raster.ts) into a `MapFrame`:
11
+ * one codepoint and one packed color per cell.
12
+ * The braille dither/luminance work is asciify's.
13
+ *
14
+ * `FrameRasterizer` subclasses `AsciifyTerminal` with a no-op sink purely to reach its guarded
15
+ * `_computeBrailleCells`, `_cellChars`, `_cellColors`, so this module never re-implements the dot math.
16
+ *
17
+ * `frameToANSILines` and `overlayText` then work on the plain `MapFrame` value, with no further
18
+ * asciify dependency; `blitFrame` is the path back the other way, for callers driving a live terminal.
15
19
  */
16
20
 
17
21
  import { AsciifyTerminal, SGR_RESET } from "@sister.software/asciify/tui"
@@ -26,20 +30,23 @@ export interface MapFrame {
26
30
  columns: number
27
31
  rows: number
28
32
  /**
29
- * Codepoint per cell, row-major (braille U+2800.. or overlay text).
33
+ * Codepoint per cell, row-major (braille U+2800.. Or overlay text).
30
34
  */
31
35
  chars: Uint32Array
32
36
  /**
33
- * 0xRRGGBB per cell; 0 = inkless.
37
+ * 0xRRGGBB per cell. 0 = inkless.
34
38
  */
35
39
  colors: Uint32Array
36
40
  attribution: string
37
41
  }
38
42
 
39
43
  /**
40
- * Reaches asciify's protected braille conversion from the outside. Constructed fresh per {@link rasterizeToFrame} call
41
- * — cheap, since the state is just two typed arrays sized to the cell grid. The `write` sink is never invoked: this
42
- * class only ever calls `_computeBrailleCells` directly, never `rasterize`/`flush`.
44
+ * Reaches asciify's guarded braille conversion from the outside.
45
+ *
46
+ * Constructed fresh per {@link rasterizeToFrame} call — cheap, since the state
47
+ * is just two typed arrays sized to the cell grid.
48
+ * The `write` sink is never invoked: this class only ever calls `_computeBrailleCells`
49
+ * directly, never `rasterize`/`flush`.
43
50
  */
44
51
  class FrameRasterizer extends AsciifyTerminal {
45
52
  constructor(columns: number, rows: number) {
@@ -55,10 +62,12 @@ class FrameRasterizer extends AsciifyTerminal {
55
62
  }
56
63
 
57
64
  /**
58
- * Converts a 2×4-subpixel RGBA grid into braille cells. Grid must be `columns * 2` x `rows * 4`.
65
+ * Converts a 2×4-subpixel rgba grid into braille cells.
59
66
  *
60
- * @throws If the grid's dimensions don't match `columns * 2` x `rows * 4` — a caller sizing bug, not something to
61
- * silently clip.
67
+ * Grid must be `columns * 2` x `rows * 4`.
68
+ *
69
+ * @throws If the grid's dimensions don't match `columns * 2` x `rows * 4` —
70
+ * a caller sizing bug rather than something to silently clip.
62
71
  */
63
72
  export function rasterizeToFrame(grid: RGBAGrid, columns: number, rows: number, attribution: string): MapFrame {
64
73
  if (grid.width !== columns * 2 || grid.height !== rows * 4) {
@@ -74,11 +83,14 @@ export function rasterizeToFrame(grid: RGBAGrid, columns: number, rows: number,
74
83
  }
75
84
 
76
85
  /**
77
- * One SGR-styled string per row. `color: false` strips styling (NO_COLOR consumers).
86
+ * One SGR-styled string per row.
87
+ *
88
+ * `color: false` strips styling (NO_COLOR consumers).
78
89
  *
79
- * Inkless cells (packed color 0) never emit a color escape, matching asciify's own damage emitter — a cell going from
80
- * inked to inkless shouldn't touch color state. Each styled line ends with the SGR reset so a truncated terminal write
81
- * never bleeds color into whatever follows.
90
+ * Inkless cells (packed color 0) never emit a color escape, matching asciify's own damage
91
+ * emitter — a cell going from inked to inkless shouldn't touch color state.
92
+ * Each styled line ends with the SGR reset so a truncated terminal write never
93
+ * bleeds color into whatever follows.
82
94
  */
83
95
  export function frameToANSILines(frame: MapFrame, options?: { color?: boolean }): string[] {
84
96
  const colorEnabled = options?.color ?? true
@@ -111,9 +123,11 @@ export function frameToANSILines(frame: MapFrame, options?: { color?: boolean })
111
123
  /**
112
124
  * Writes text into cells (clipped); occupied is the label-collision bitmap, updated in place.
113
125
  *
114
- * When `occupied` is given, every target cell plus one cell of padding on each side is checked before anything is
115
- * written — a single colliding cell rejects the whole label rather than partially overlaying it. Without `occupied`,
116
- * writes proceed unconditionally (still clipped to the frame's bounds) and always report success.
126
+ * When `occupied` is given, every target cell plus one cell of padding on each side is
127
+ * checked before anything is written — a single colliding cell rejects the whole label
128
+ * rather than partially overlaying it.
129
+ * Without `occupied`, writes proceed unconditionally (still clipped to the frame's bounds)
130
+ * and always report success.
117
131
  */
118
132
  export function overlayText(
119
133
  frame: MapFrame,
@@ -160,25 +174,33 @@ export function overlayText(
160
174
  }
161
175
 
162
176
  /**
163
- * Packs RGB channels into the frame's 0xRRGGBB color representation. Shared with Task 8's marker colors so both sides
164
- * of the frame boundary agree on the packing.
177
+ * Packs RGB channels into the frame's 0xRRGGBB color representation.
178
+ *
179
+ * Shared with Task 8's marker colors so both sides of the frame boundary agree on the packing.
165
180
  */
166
181
  export function rgbToPacked(color: RGB): number {
167
182
  return (color[0] << 16) | (color[1] << 8) | color[2]
168
183
  }
169
184
 
170
185
  /**
171
- * Codepoint written for a cell the frame left empty. `MapFrame` stores 0 there; `AsciifyTerminal` expects a real
172
- * character, and normalizes a space to the inkless color itself.
186
+ * Codepoint written for a cell the frame left empty.
187
+ *
188
+ * `MapFrame` stores 0 there.
189
+ * `AsciifyTerminal` expects a real character. and normalizes a space to the inkless color itself.
173
190
  */
174
191
  const SPACE_CODEPOINT = 0x20
175
192
 
176
193
  /**
177
- * Writes a frame's cells into an `AsciifyTerminal`'s current frame. Call `flush()` afterwards to emit the damage.
194
+ * Writes a frame's cells into an `AsciifyTerminal`'s current frame.
195
+ *
196
+ * Call `flush()` afterwards to emit the damage.
197
+ *
198
+ * The frame's packed color is the exact representation asciify canonicalizes truecolor to,
199
+ * so this is a copy and not a conversion.
200
+ * The channels are unpacked here only because {@linkcode AsciifyTerminal.setCell} takes them apart.
178
201
  *
179
- * The frame's packed color is the exact representation asciify canonicalizes truecolor to, so this is a copy and not a
180
- * conversion — the channels are unpacked here only because {@linkcode AsciifyTerminal.setCell} takes them apart. Cells
181
- * beyond the terminal's own grid are dropped by `setCell`, so a frame larger than the pane clips rather than throws.
202
+ * Cells beyond the terminal's own grid are dropped by `setCell`, so a frame
203
+ * larger than the pane clips rather than throws.
182
204
  */
183
205
  export function blitFrame(terminal: AsciifyTerminal, frame: MapFrame): void {
184
206
  for (let row = 0; row < frame.rows; row++) {
package/lib/input.ts CHANGED
@@ -5,52 +5,34 @@
5
5
  */
6
6
 
7
7
  /**
8
- * Terminal input decoding for the interactive map browser.
9
- *
10
- * `decodeInputChunk` turns a raw-mode stdin chunk into zero or more {@link MapTUIInput} events. It stays pure — the
11
- * caller owns the only state there is, the unresolved trailing fragment the function hands back — so one chunk in gives
12
- * the same events out every time.
13
- *
14
- * THE FALLBACK IS THE DANGEROUS PART. An ESC this decoder had no rule for used to mean "the Esc key", i.e. QUIT, which
15
- * made every unrecognized escape sequence a quit: F1 (`ESC O P`), an OSC reply the terminal sends unasked (`ESC ] 11 ;
16
- * rgb:… BEL`), and — worst, because it needs no exotic key at all — a mouse report split across two stdin reads, whose
17
- * first half ends inside the sequence. So the fallback now separates three cases:
18
- *
19
- * - A sequence this decoder recognizes is consumed and acted on (arrows, SGR mouse).
20
- * - A sequence it does NOT recognize is consumed WHOLE and ignored: CSI (`ESC [ … final`), SS3 (`ESC O final`), and the
21
- * string family (OSC/DCS/SOS/PM/APC, terminated by BEL or ST). Re-scanning their bodies as characters is how a `q`
22
- * inside a cursor-position report quit the app.
23
- * - A chunk that ENDS mid-sequence — including a lone trailing ESC, which is byte-for-byte the start of one — is not
24
- * decoded at all: it comes back as {@link DecodedInput.pending} for the caller to prepend to the next chunk. Quit is
25
- * emitted only for an ESC that is neither, i.e. one whose following byte cannot continue a sequence.
26
- *
27
- * Holding costs a lone Esc keypress its effect until the next byte arrives. That is the right side of the trade for a
28
- * browser whose advertised quit keys are `q` and Ctrl+C: the alternative is a drag ending the session because the
29
- * kernel split a read.
30
- *
31
- * Mouse reports are SGR-encoded (DEC private mode 1006), which is what {@link MOUSE_ENABLE} asks for. Their coordinates
32
- * are 1-based on the wire and 0-based in every event here — the off-by-one lives at this boundary and nowhere else.
8
+ * The fallback separates a sequence this decoder recognizes (consumed and acted on), a complete
9
+ * but unrecognized sequence (consumed whole so a `q` inside it cannot quit the app), and a
10
+ * chunk that ends mid-sequence (returned as {@link DecodedInput.pending} rather than decoded),
11
+ * emitting quit only for an ESC whose following byte cannot continue a sequence.
33
12
  */
34
13
 
35
14
  /**
36
- * Enables mouse reporting: button events (1000), drag/button-motion tracking (1002), and SGR extended coordinates
37
- * (1006) so columns past 223 survive.
15
+ * Enables mouse reporting: button events (1000), drag/button-motion tracking (1002),
16
+ * and SGR extended coordinates (1006) so columns past 223 survive.
38
17
  */
39
18
  export const MOUSE_ENABLE = "\u001B[?1000h\u001B[?1002h\u001B[?1006h"
40
19
 
41
20
  /**
42
- * Disables mouse reporting, in the reverse order it was enabled.
21
+ * Disables the three mouse-reporting modes {@link MOUSE_ENABLE} turns on.
43
22
  */
44
23
  export const MOUSE_DISABLE = "\u001B[?1006l\u001B[?1002l\u001B[?1000l"
45
24
 
46
25
  /**
47
- * A decoded input event. Pan and zoom carry direction and magnitude only — how far a step moves the map is the
48
- * browser's decision, not the decoder's.
26
+ * A decoded input event.
27
+ *
28
+ * Pan and zoom encode direction and magnitude only, since how far a step moves the
29
+ * map is the browser's decision rather than the decoder's.
49
30
  */
50
31
  export type MapTUIInput =
51
32
  | { kind: "quit" }
52
33
  /**
53
- * Ctrl+C. Distinct from `quit` because the process must exit 130, and because raw mode means no SIGINT is raised.
34
+ * Ctrl+C sends an interrupt. The process exits with status 130.
35
+ * Raw mode prevents the terminal from raising SIGINT.
54
36
  */
55
37
  | { kind: "interrupt" }
56
38
  | { kind: "pan"; dx: number; dy: number }
@@ -77,46 +59,48 @@ const MOUSE_SGR_PATTERN = /\u001B\[<(\d+);(\d+);(\d+)([Mm])/y
77
59
  const ARROW_PATTERN = /\u001B(?:\[|O)([ABCD])/y
78
60
 
79
61
  /**
80
- * Any other CSI sequence, consumed whole and ignored. Without this, an unhandled sequence's body would be re-scanned as
81
- * individual key presses, and a stray `q` inside one would quit the app.
62
+ * Any other CSI sequence, consumed whole and ignored so an unhandled body is not re-scanned as key presses.
82
63
  */
83
64
  const UNKNOWN_CSI_PATTERN = /\u001B\[[\d;<>?]*[\u0020-\u002F]*[\u0040-\u007E]/y
84
65
 
85
66
  /**
86
- * Any other SS3 sequence (`ESC O <final>`) — F1–F4 on xterm, and the numeric keypad in application mode. Two bytes
87
- * shorter than a CSI, and until this pattern existed the likeliest key on a keyboard to quit the browser by accident.
67
+ * Any other SS3 sequence (`ESC O <final>`).
68
+ *
69
+ * On xterm this includes F1–F4 and the application-mode numeric keypad.
88
70
  */
89
71
  const UNKNOWN_SS3_PATTERN = /\u001BO[\u0040-\u007E]/y
90
72
 
91
73
  /**
92
- * The string-sequence family: OSC (`ESC ]`), DCS (`ESC P`), SOS (`ESC X`), PM (`ESC ^`), APC (`ESC _`), each running to
93
- * a BEL or an ST (`ESC \`). A terminal sends these UNASKED — an OSC colour or clipboard reply lands on stdin with no
94
- * key pressed — so consuming them is not a nicety.
74
+ * The string-sequence family (OSC, DCS, SOS, PM, APC), each running to a BEL
75
+ * or an ST, sent unasked by a terminal with no key pressed.
95
76
  */
96
77
  const STRING_SEQUENCE_PATTERN = /\u001B[P\]X^_][\s\S]*?(?:\u0007|\u001B\\)/y
97
78
 
98
79
  /**
99
- * Every "unrecognized but complete" sequence, in the order they are tried. Sharing one list is what keeps a new
100
- * sequence family from being added to the consumer and forgotten in the incomplete test below.
80
+ * Every "unrecognized but complete" sequence, in the order they are tried.
81
+ *
82
+ * One shared list keeps a new family from being added here and forgotten in the incomplete test.
101
83
  */
102
84
  const UNRECOGNIZED_PATTERNS = [UNKNOWN_CSI_PATTERN, UNKNOWN_SS3_PATTERN, STRING_SEQUENCE_PATTERN] as const
103
85
 
104
86
  /**
105
- * A chunk that STOPS inside a sequence. The end-anchors are what make these "incomplete" rather than "unrecognized":
106
- * each requires the WHOLE remainder of the chunk to be a legal prefix and nothing more. The first covers both a lone
107
- * trailing ESC and an `ESC O` still waiting for its final byte.
87
+ * A chunk that stops inside a sequence, each end-anchored pattern requiring the whole
88
+ * remainder of the chunk to be a legal prefix and no more.
108
89
  */
109
90
  const PARTIAL_PATTERNS = [/\u001BO?$/y, /\u001B\[[\d;<>?]*[\u0020-\u002F]*$/y, /\u001B[P\]X^_][^\u0007]*$/y] as const
110
91
 
111
92
  /* oxlint-enable no-control-regex */
112
93
 
113
94
  /**
114
- * Wheel reports set bit 6 of the button field; the low bit then separates up (0) from down (1).
95
+ * Wheel reports set bit 6 of the button field.
96
+ * The low bit separates up (0) from down (1).
115
97
  */
116
98
  const WHEEL_FLAG = 64
117
99
 
118
100
  /**
119
- * Motion reports set bit 5. With mode 1002 that means "moved with a button held" — a drag.
101
+ * Motion reports set bit 5.
102
+ *
103
+ * In mode 1002, that bit means "moved with a button held" — a drag.
120
104
  */
121
105
  const MOTION_FLAG = 32
122
106
 
@@ -124,8 +108,8 @@ const BUTTON_MASK = 3
124
108
  const LEFT_BUTTON = 0
125
109
 
126
110
  /**
127
- * The longest fragment worth holding for the next chunk (64 KB). See the drop site: this bounds an unterminated string
128
- * sequence, not a real key.
111
+ * The longest fragment held for the next chunk (64 KB), bounding an unterminated
112
+ * string sequence rather than a real key.
129
113
  */
130
114
  const MAX_PENDING_LENGTH = 65_536
131
115
 
@@ -137,9 +121,13 @@ const ARROW_INPUTS: Record<string, MapTUIInput> = {
137
121
  }
138
122
 
139
123
  /**
140
- * Single-character bindings. `a`/`z` are mapscii's zoom keys, `+`/`-` the ones every other map uses, and `hjkl` the vim
141
- * pan set mapscii also accepts. `y` joins `z` for zoom-out because on a QWERTZ keyboard it sits where `z` does on
142
- * QWERTY — mapscii binds both for the same reason.
124
+ * Single-character bindings taken from mapscii.
125
+ *
126
+ * `a` and `z` zoom.
127
+ * `+` and `-` form the common pair.
128
+ *
129
+ * `hjkl` pans like vim.
130
+ * `y` zooms out beside `z` on QWERTZ keyboards, where QWERTY places `z`.
143
131
  */
144
132
  const CHARACTER_INPUTS: Record<string, MapTUIInput> = {
145
133
  q: { kind: "quit" },
@@ -158,20 +146,17 @@ const CHARACTER_INPUTS: Record<string, MapTUIInput> = {
158
146
  }
159
147
 
160
148
  /**
161
- * One decoded chunk: its events, plus whatever trailing bytes could not be decoded YET.
149
+ * One decoded chunk: its events, plus whatever trailing bytes could not be decoded yet.
162
150
  */
163
151
  export interface DecodedInput {
164
152
  events: MapTUIInput[]
165
153
  /**
166
- * An unresolved escape fragment from the end of the chunk — prepend it to the next one. Empty when the chunk ended
167
- * cleanly, which is the overwhelmingly common case.
154
+ * An unresolved escape fragment from the end of the chunk to prepend to the next,
155
+ * empty when the chunk ended cleanly.
168
156
  */
169
157
  pending: string
170
158
  }
171
159
 
172
- /**
173
- * The length of the unrecognized-but-complete sequence at `index`, or null when there isn't one.
174
- */
175
160
  function consumeUnrecognized(buffer: string, index: number): number | null {
176
161
  for (const pattern of UNRECOGNIZED_PATTERNS) {
177
162
  pattern.lastIndex = index
@@ -182,10 +167,6 @@ function consumeUnrecognized(buffer: string, index: number): number | null {
182
167
  return null
183
168
  }
184
169
 
185
- /**
186
- * True when everything from `index` to the end of the chunk is a legal PREFIX of a sequence — i.e. the terminal is
187
- * mid-sequence and the rest is in the next read.
188
- */
189
170
  function isIncompleteSequence(buffer: string, index: number): boolean {
190
171
  for (const pattern of PARTIAL_PATTERNS) {
191
172
  pattern.lastIndex = index
@@ -196,9 +177,6 @@ function isIncompleteSequence(buffer: string, index: number): boolean {
196
177
  return false
197
178
  }
198
179
 
199
- /**
200
- * Builds the event for one SGR mouse report.
201
- */
202
180
  function mouseInput(button: number, column: number, row: number, final: string): MapTUIInput | null {
203
181
  if (button & WHEEL_FLAG) {
204
182
  return { kind: "wheel", delta: button & 1 ? -1 : 1, column, row }
@@ -212,8 +190,8 @@ function mouseInput(button: number, column: number, row: number, final: string):
212
190
  }
213
191
 
214
192
  /**
215
- * Decodes one raw-mode stdin chunk into input events. Unrecognized bytes are dropped; an unresolved trailing escape
216
- * fragment is returned rather than decoded, and the caller passes it back as `pending` with the next chunk.
193
+ * Decodes one raw-mode stdin chunk into input events, dropping unrecognized bytes
194
+ * and returning an unresolved trailing escape fragment as `pending`.
217
195
  */
218
196
  export function decodeInputChunk(chunk: string, pending = ""): DecodedInput {
219
197
  const events: MapTUIInput[] = []
@@ -272,10 +250,9 @@ export function decodeInputChunk(chunk: string, pending = ""): DecodedInput {
272
250
  if (isIncompleteSequence(buffer, index)) {
273
251
  const fragment = buffer.slice(index)
274
252
 
275
- // …unless it has stopped being plausible. An unterminated string sequence would otherwise grow the held
276
- // fragment for the life of the process. Dropping is the safe failure: it emits nothing, where flushing
277
- // the fragment back through the decoder would read its body as keys, which is the bug this all exists
278
- // for. The cap is generous because an OSC 52 clipboard reply is legitimately large.
253
+ // An unterminated string sequence could grow the held fragment for the life of the process.
254
+ // Drop an overlong fragment.
255
+ // A body flush would make terminal content read as key presses.
279
256
  return { events, pending: fragment.length > MAX_PENDING_LENGTH ? "" : fragment }
280
257
  }
281
258
 
package/lib/mercator.ts CHANGED
@@ -7,22 +7,21 @@
7
7
  /**
8
8
  * Web-Mercator projection math for map-tui.
9
9
  *
10
- * This module reimplements the standard Web-Mercator projection (EPSG:3857) math locally rather than importing from
11
- * `@mailwoman/cartographer` or `@mailwoman/spatial`. The cartographer dependency drags maplibre-gl +
12
- * `@mailwoman/tiger`; spatial drags `@mailwoman/core`'s shipped data. map-tui maintains a dependency-lean surface for
13
- * the standalone `npx` story (the nuts-lookup precedent).
10
+ * This module reimplements the standard Web-Mercator projection (epsg:3857) math locally
11
+ * rather than importing from `@mailwoman/cartographer` or `@mailwoman/spatial`.
12
+ * The cartographer dependency drags maplibre-gl + `@mailwoman/tiger`;
13
+ * spatial drags `@mailwoman/core`'s shipped data.
14
+ *
15
+ * Map-tui maintains a dependency-lean surface for the standalone `npx` story (the nuts-lookup precedent).
14
16
  */
15
17
 
18
+ import type { LatLon } from "@mailwoman/spatial"
19
+
16
20
  /**
17
21
  * Number of pixels per tile in the Web-Mercator projection (standard: 256).
18
22
  */
19
23
  export const TILE_SIZE = 256
20
24
 
21
- export interface LonLat {
22
- lon: number
23
- lat: number
24
- }
25
-
26
25
  export interface WorldPx {
27
26
  x: number
28
27
  y: number
@@ -38,7 +37,7 @@ export function lonLatToWorldPx(lon: number, lat: number, zoom: number): WorldPx
38
37
  }
39
38
  }
40
39
 
41
- export function worldPxToLonLat(x: number, y: number, zoom: number): LonLat {
40
+ export function worldPxToLonLat(x: number, y: number, zoom: number): LatLon {
42
41
  const scale = TILE_SIZE * 2 ** zoom
43
42
  const n = Math.PI * (1 - (2 * y) / scale)
44
43
 
@@ -64,8 +63,10 @@ export function wrapLongitude(lon: number): number {
64
63
  }
65
64
 
66
65
  /**
67
- * Subpixel dimensions of one braille cell — 2 columns wide, 4 rows tall. The unit every projection-to-cell conversion
68
- * works in, shared by the renderer and the browser so both sides of the frame boundary agree on the grid.
66
+ * Subpixel dimensions of one braille cell — 2 columns wide, 4 rows tall.
67
+ *
68
+ * The unit every projection-to-cell conversion works in, shared by the renderer
69
+ * and the browser so both sides of the frame boundary agree on the grid.
69
70
  */
70
71
  export const SUBPIXEL_COLUMNS_PER_CELL = 2
71
72
 
package/lib/mvt.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  /**
8
8
  * Mapbox Vector Tile (MVT) decoding for map-tui.
9
9
  *
10
- * Wraps @mapbox/vector-tile + pbf behind a plain-data shape (DecodedLayer / DecodedFeature) so the rest of map-tui
11
- * never touches the upstream library's lazy-geometry classes.
10
+ * Wraps @mapbox/vector-tile + pbf behind a plain-data shape (DecodedLayer / DecodedFeature)
11
+ * so the rest of map-tui never touches the upstream library's lazy-geometry classes.
12
12
  */
13
13
 
14
14
  import { VectorTile } from "@mapbox/vector-tile"
package/lib/raster.ts CHANGED
@@ -5,25 +5,33 @@
5
5
  */
6
6
 
7
7
  /**
8
- * RGBA pixel rasterizer for map-tui's debug view.
8
+ * Rgba pixel rasterizer for map-tui's debug view.
9
9
  *
10
- * Rasterizes vector geometry (polylines, filled polygons, circles) onto a plain RGBA byte grid — the shape asciify's
11
- * rasterize step consumes. Every draw call clips through {@link RGBAGrid.setPixel}, so callers never need to
12
- * bounds-check geometry themselves. The polyline and circle primitives floor their coordinates to integers on entry;
13
- * {@link fillPolygon} does not — its scanline edge math keeps ring vertices as given, see its own docstring.
10
+ * Rasterizes vector geometry (polylines, filled polygons, circles) onto a plain rgba
11
+ * byte grid — the shape asciify's rasterize step consumes.
12
+ * Every draw call clips through {@link RGBAGrid.setPixel}, so callers never
13
+ * need to bounds-check geometry themselves.
14
+ *
15
+ * The polyline and circle primitives floor their coordinates to integers on entry;
16
+ * {@link fillPolygon} does not.
17
+ * Its scanline edge math keeps ring vertices as given, see its own docstring.
14
18
  */
15
19
 
16
20
  import type { RGB } from "#style"
17
21
 
18
22
  /**
19
- * A row-major RGBA pixel buffer. Alpha starts at 0 (unlit/transparent) everywhere; drawing a pixel sets it to 255,
20
- * which is how callers (including this module's own tests) distinguish "lit" from "background".
23
+ * A row-major rgba pixel buffer.
24
+ *
25
+ * Alpha starts at 0 (unlit/transparent) everywhere.
26
+ * A drawn pixel has value 255.
27
+ *
28
+ * Callers, including this module's own tests, distinguish "lit" from "background".
21
29
  */
22
30
  export class RGBAGrid {
23
31
  readonly width: number
24
32
  readonly height: number
25
33
  /**
26
- * RGBA, row-major, width * height * 4 bytes — the shape asciify's rasterize consumes.
34
+ * Rgba, row-major, width * height * 4 bytes — the shape asciify's rasterize consumes.
27
35
  */
28
36
  readonly data: Uint8ClampedArray
29
37
 
@@ -34,9 +42,12 @@ export class RGBAGrid {
34
42
  }
35
43
 
36
44
  /**
37
- * Writes a pixel's RGB channels and sets alpha to fully opaque (255). Coordinates outside the grid are silently
38
- * ignored — this is the one clipping boundary every drawing primitive in this module routes through, so geometry that
39
- * runs off the grid (or arrives with negative/oversized coordinates) never needs special-casing upstream.
45
+ * Writes a pixel's RGB channels and sets alpha to fully opaque (255).
46
+ *
47
+ * Coordinates outside the grid are silently ignored.
48
+ * This is the one clipping boundary every drawing primitive in this module routes through,
49
+ * so geometry that runs off the grid (or arrives with negative/oversized coordinates)
50
+ * never needs special-casing upstream.
40
51
  */
41
52
  setPixel(x: number, y: number, color: RGB): void {
42
53
  const px = Math.floor(x)
@@ -54,8 +65,9 @@ export class RGBAGrid {
54
65
  }
55
66
 
56
67
  /**
57
- * Stamps a `width`-sized square centered on (x, y) — used by {@link drawPolyline} for `width > 1`. `width === 1` callers
58
- * should use `grid.setPixel` directly rather than pay the square-stamp loop.
68
+ * Stamps a `width`-sized square centered on (x, y) — used by {@link drawPolyline} for `width > 1`.
69
+ *
70
+ * `width === 1` callers should use `grid.setPixel` directly rather than pay the square-stamp loop.
59
71
  */
60
72
  function stampSquare(grid: RGBAGrid, x: number, y: number, color: RGB, width: number): void {
61
73
  const half = Math.floor(width / 2)
@@ -68,8 +80,11 @@ function stampSquare(grid: RGBAGrid, x: number, y: number, color: RGB, width: nu
68
80
  }
69
81
 
70
82
  /**
71
- * Draws a single line segment with the integer Bresenham algorithm. Coordinates are floored on entry; every plotted
72
- * point routes through `grid.setPixel`, so segments that run partly or fully off-grid clip for free.
83
+ * Draws a single line segment with the integer Bresenham algorithm.
84
+ *
85
+ * Coordinates are floored on entry.
86
+ * Every plotted point routes through `grid.setPixel`, so segments that run partly
87
+ * or fully off-grid clip for free.
73
88
  */
74
89
  function drawSegment(grid: RGBAGrid, x0: number, y0: number, x1: number, y1: number, color: RGB, width: number): void {
75
90
  let x = Math.floor(x0)
@@ -107,9 +122,11 @@ function drawSegment(grid: RGBAGrid, x0: number, y0: number, x1: number, y1: num
107
122
  }
108
123
 
109
124
  /**
110
- * Draws a connected polyline through `points`, one Bresenham segment per consecutive pair. `width > 1` stamps a
111
- * `width`-sized square at every plotted step instead of a single pixel, thickening the line. Segments that run outside
112
- * the grid clip through `setPixel` rather than throwing.
125
+ * Draws a connected polyline through `points`, one Bresenham segment per consecutive pair.
126
+ *
127
+ * `width > 1` stamps a `width`-sized square at every plotted step instead of
128
+ * a single pixel, thickening the line.
129
+ * Segments that run outside the grid clip through `setPixel` rather than throwing.
113
130
  */
114
131
  export function drawPolyline(
115
132
  grid: RGBAGrid,
@@ -126,12 +143,18 @@ export function drawPolyline(
126
143
  }
127
144
 
128
145
  /**
129
- * Fills one or more polygon rings with the even-odd rule: a pixel is interior when a ray from it crosses an odd number
130
- * of ring edges. Rings after the first behave as holes wherever they overlap the first ring, and holes nested inside
131
- * holes fill again, purely as a consequence of the parity count — no explicit hole/outer distinction is tracked.
132
- * Scanlines sample row centers (`y + 0.5`) rather than integer row coordinates, which is what keeps horizontal edges
133
- * and grid-aligned polygon boundaries from producing degenerate (zero-width or doubled) intersections. Coordinates are
134
- * floored to the grid via `setPixel`; edge math itself uses the ring vertices as given.
146
+ * Fills one or more polygon rings with the even-odd rule: a pixel is interior
147
+ * when a ray from it crosses an odd number of ring edges.
148
+ *
149
+ * Rings after the first behave as holes wherever they overlap the first ring.
150
+ * Holes nested inside holes fill again as a consequence of the parity count.
151
+ * No explicit hole/outer distinction is tracked.
152
+ *
153
+ * Scanlines sample row centers (`y + 0.5`) rather than integer row coordinates.
154
+ * This keeps horizontal edges and grid-aligned polygon boundaries from producing
155
+ * degenerate (zero-width or doubled) intersections.
156
+ *
157
+ * Coordinates are floored to the grid via `setPixel`; edge math itself uses the ring vertices as given.
135
158
  */
136
159
  export function fillPolygon(
137
160
  grid: RGBAGrid,
@@ -174,9 +197,12 @@ export function fillPolygon(
174
197
  }
175
198
 
176
199
  /**
177
- * Draws a circle's outline (ring, not a filled disc) with the midpoint circle algorithm, plotting all eight symmetric
178
- * octant points per step. `centerX`/`centerY`/`radius` are floored on entry; every plotted point routes through
179
- * `setPixel`, so a circle that runs off the grid clips rather than throwing.
200
+ * Draws a circle's outline (ring rather than a filled disc) with the midpoint circle
201
+ * algorithm, plotting all eight symmetric octant points per step.
202
+ *
203
+ * `centerX`/`centerY`/`radius` are floored on entry.
204
+ * Every plotted point routes through `setPixel`, so a circle that runs off
205
+ * the grid clips rather than throwing.
180
206
  */
181
207
  export function drawCircle(grid: RGBAGrid, centerX: number, centerY: number, radius: number, color: RGB): void {
182
208
  const cx = Math.floor(centerX)