@mailwoman/map-tui 9.4.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.
- package/README.md +5 -6
- package/{lib/cli-args.ts → cli/args.ts} +49 -28
- package/{lib/cli.ts → cli/main.ts} +13 -24
- package/lib/browser.ts +33 -76
- package/lib/frame.ts +50 -28
- package/lib/input.ts +46 -69
- package/lib/mercator.ts +13 -12
- package/lib/mvt.ts +2 -2
- package/lib/raster.ts +53 -27
- package/lib/renderer.ts +19 -38
- package/lib/style.ts +21 -15
- package/lib/tile-source.ts +30 -19
- package/out/browser.d.ts +24 -33
- package/out/browser.d.ts.map +1 -1
- package/out/browser.js +27 -72
- package/out/browser.js.map +1 -1
- package/out/{cli-args.d.ts → cli/args.d.ts} +19 -9
- package/out/cli/args.d.ts.map +1 -0
- package/out/{cli-args.js → cli/args.js} +42 -26
- package/out/cli/args.js.map +1 -0
- package/out/{cli.d.ts → cli/main.d.ts} +1 -1
- package/out/cli/main.d.ts.map +1 -0
- package/out/{cli.js → cli/main.js} +14 -25
- package/out/cli/main.js.map +1 -0
- package/out/cli/tsconfig.tsbuildinfo +1 -0
- package/out/frame.d.ts +40 -23
- package/out/frame.d.ts.map +1 -1
- package/out/frame.js +48 -26
- package/out/frame.js.map +1 -1
- package/out/input.d.ts +18 -36
- package/out/input.d.ts.map +1 -1
- package/out/input.js +37 -63
- package/out/input.js.map +1 -1
- package/out/mercator.d.ts +12 -11
- package/out/mercator.d.ts.map +1 -1
- package/out/mercator.js +4 -10
- package/out/mercator.js.map +1 -1
- package/out/mvt.js +2 -2
- package/out/raster.d.ts +45 -23
- package/out/raster.d.ts.map +1 -1
- package/out/raster.js +44 -22
- package/out/raster.js.map +1 -1
- package/out/renderer.d.ts +6 -7
- package/out/renderer.d.ts.map +1 -1
- package/out/renderer.js +13 -22
- package/out/renderer.js.map +1 -1
- package/out/style.d.ts +17 -11
- package/out/style.d.ts.map +1 -1
- package/out/style.js +11 -9
- package/out/style.js.map +1 -1
- package/out/tile-source.d.ts +12 -8
- package/out/tile-source.d.ts.map +1 -1
- package/out/tile-source.js +21 -14
- package/out/tile-source.js.map +1 -1
- package/out/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +35 -83
- package/out/cli-args.d.ts.map +0 -1
- package/out/cli-args.js.map +0 -1
- package/out/cli.d.ts.map +0 -1
- package/out/cli.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`:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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..
|
|
33
|
+
* Codepoint per cell, row-major (braille U+2800.. Or overlay text).
|
|
30
34
|
*/
|
|
31
35
|
chars: Uint32Array
|
|
32
36
|
/**
|
|
33
|
-
* 0xRRGGBB per cell
|
|
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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
|
65
|
+
* Converts a 2×4-subpixel rgba grid into braille cells.
|
|
59
66
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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.
|
|
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
|
|
80
|
-
* inked to inkless shouldn't touch color state.
|
|
81
|
-
*
|
|
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
|
|
115
|
-
* written — a single colliding cell rejects the whole label
|
|
116
|
-
*
|
|
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.
|
|
164
|
-
*
|
|
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.
|
|
172
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
180
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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),
|
|
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
|
|
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.
|
|
48
|
-
*
|
|
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
|
|
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
|
|
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>`)
|
|
87
|
-
*
|
|
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
|
|
93
|
-
*
|
|
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.
|
|
100
|
-
*
|
|
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
|
|
106
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
128
|
-
* sequence
|
|
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
|
|
141
|
-
*
|
|
142
|
-
*
|
|
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
|
|
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
|
|
167
|
-
*
|
|
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
|
|
216
|
-
*
|
|
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
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
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 (
|
|
11
|
-
* `@mailwoman/cartographer` or `@mailwoman/spatial`.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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):
|
|
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.
|
|
68
|
-
*
|
|
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)
|
|
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
|
-
*
|
|
8
|
+
* Rgba pixel rasterizer for map-tui's debug view.
|
|
9
9
|
*
|
|
10
|
-
* Rasterizes vector geometry (polylines, filled polygons, circles) onto a plain
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
20
|
-
*
|
|
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
|
-
*
|
|
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).
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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`.
|
|
58
|
-
*
|
|
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.
|
|
72
|
-
*
|
|
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.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
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
|
|
178
|
-
* octant points per step.
|
|
179
|
-
*
|
|
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)
|