@mailwoman/map-tui 9.1.1 → 9.3.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 +59 -0
- package/lib/browser.ts +525 -0
- package/lib/cli-args.ts +233 -0
- package/lib/cli.ts +141 -0
- package/{frame.ts → lib/frame.ts} +33 -6
- package/lib/index.ts +13 -0
- package/lib/input.ts +288 -0
- package/{mercator.ts → lib/mercator.ts} +20 -0
- package/{mvt.ts → lib/mvt.ts} +2 -2
- package/{raster.ts → lib/raster.ts} +1 -1
- package/{renderer.ts → lib/renderer.ts} +86 -36
- package/lib/style.ts +115 -0
- package/{tile-source.ts → lib/tile-source.ts} +76 -20
- package/out/browser.d.ts +126 -0
- package/out/browser.d.ts.map +1 -0
- package/out/browser.js +373 -0
- package/out/browser.js.map +1 -0
- package/out/cli-args.d.ts +47 -0
- package/out/cli-args.d.ts.map +1 -0
- package/out/cli-args.js +171 -0
- package/out/cli-args.js.map +1 -0
- package/out/cli.d.ts +8 -0
- package/out/cli.d.ts.map +1 -0
- package/out/cli.js +119 -0
- package/out/cli.js.map +1 -0
- package/out/frame.d.ts +20 -2
- package/out/frame.d.ts.map +1 -1
- package/out/frame.js +28 -3
- package/out/frame.js.map +1 -1
- package/out/index.d.ts +7 -7
- package/out/index.d.ts.map +1 -1
- package/out/index.js +7 -7
- package/out/index.js.map +1 -1
- package/out/input.d.ts +93 -0
- package/out/input.d.ts.map +1 -0
- package/out/input.js +214 -0
- package/out/input.js.map +1 -0
- package/out/mercator.d.ts +13 -0
- package/out/mercator.d.ts.map +1 -1
- package/out/mercator.js +16 -0
- package/out/mercator.js.map +1 -1
- package/out/mvt.d.ts.map +1 -1
- package/out/mvt.js +2 -2
- package/out/mvt.js.map +1 -1
- package/out/raster.d.ts +1 -1
- package/out/raster.d.ts.map +1 -1
- package/out/raster.js.map +1 -1
- package/out/renderer.d.ts +11 -14
- package/out/renderer.d.ts.map +1 -1
- package/out/renderer.js +46 -26
- package/out/renderer.js.map +1 -1
- package/out/style.d.ts +22 -11
- package/out/style.d.ts.map +1 -1
- package/out/style.js +49 -4
- package/out/style.js.map +1 -1
- package/out/tile-source.d.ts +25 -4
- package/out/tile-source.d.ts.map +1 -1
- package/out/tile-source.js +46 -14
- package/out/tile-source.js.map +1 -1
- package/out/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +105 -6
- package/index.ts +0 -13
- package/out/tsconfig.tsbuildinfo +0 -1
- package/style.ts +0 -62
package/lib/cli-args.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { errorMessage } from "@mailwoman/core/errors/schema"
|
|
2
|
+
import { parseArguments } from "@mailwoman/core/scripting/arguments"
|
|
3
|
+
/**
|
|
4
|
+
* @copyright Sister Software.
|
|
5
|
+
* @license AGPL-3.0
|
|
6
|
+
* @author Teffen Ellis, et al.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Argument parsing for the `map-tui` bin.
|
|
11
|
+
*
|
|
12
|
+
* `parseCLIArgs` is pure: it takes the argv slice and an environment record, and answers with a discriminated result
|
|
13
|
+
* (`help` / `version` / `browse`) or throws {@link CLIArgsError}. Reading `process.argv` / `process.env` is the bin's
|
|
14
|
+
* job (./cli.ts), which keeps every rejection path testable without a subprocess.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Center longitude when `--lon` is omitted — the world view mapscii opens on.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_LON = 0
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Center latitude when `--lat` is omitted.
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_LAT = 0
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Zoom when `--zoom` is omitted. z2 shows a full hemisphere in a typical terminal, so a planet archive opens on
|
|
29
|
+
* something recognizable rather than a single ocean tile.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_ZOOM = 2
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Widest zoom any tile archive uses.
|
|
35
|
+
*/
|
|
36
|
+
const MIN_ZOOM = 0
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Deepest zoom the Web-Mercator tile pyramid is defined for. The archive's own `maxZoom` clamps further at runtime;
|
|
40
|
+
* this is only the range a flag value must fall inside to be meaningful at all.
|
|
41
|
+
*/
|
|
42
|
+
const MAX_ZOOM = 24
|
|
43
|
+
|
|
44
|
+
// True geographic bounds, not Web-Mercator's ±85.05113: the flag accepts any real latitude, and the browser clamps
|
|
45
|
+
// the CENTER to the projection's MERCATOR_LATITUDE_LIMIT itself (see ./browser.ts) — rejecting 87 here would refuse a
|
|
46
|
+
// value the viewport handles fine.
|
|
47
|
+
const MIN_LAT = -90
|
|
48
|
+
const MAX_LAT = 90
|
|
49
|
+
const MIN_LON = -180
|
|
50
|
+
const MAX_LON = 180
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A view request: an archive to read and where to open it.
|
|
54
|
+
*/
|
|
55
|
+
export interface BrowseArgs {
|
|
56
|
+
mode: "browse"
|
|
57
|
+
/**
|
|
58
|
+
* Path to the PMTiles archive.
|
|
59
|
+
*/
|
|
60
|
+
tiles: string
|
|
61
|
+
lat: number
|
|
62
|
+
lon: number
|
|
63
|
+
/**
|
|
64
|
+
* Integer zoom level. The renderer draws whole tile-pyramid levels, so a fractional flag value is rounded here rather
|
|
65
|
+
* than carried as a lie through the viewport.
|
|
66
|
+
*/
|
|
67
|
+
zoom: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type CLIArgs = { mode: "help" } | { mode: "version" } | BrowseArgs
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A rejected command line. The message is user-facing: it says what was wrong AND what to pass instead, since the bin
|
|
74
|
+
* prints it verbatim to stderr.
|
|
75
|
+
*/
|
|
76
|
+
export class CLIArgsError extends Error {
|
|
77
|
+
override name = "CLIArgsError"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Environment keys the CLI reads. Passed in rather than read here so the parser stays pure.
|
|
82
|
+
*/
|
|
83
|
+
export interface CLIEnvironment {
|
|
84
|
+
MAILWOMAN_TILES?: string | undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `--help` output. It doubles as the package's key reference, so the bindings listed here and the ones ./input.ts
|
|
89
|
+
* decodes are the same list said twice — a key added there without a line here is a key nobody finds.
|
|
90
|
+
*/
|
|
91
|
+
export const HELP_TEXT = `map-tui — the whole world in your terminal
|
|
92
|
+
|
|
93
|
+
Usage
|
|
94
|
+
npx @mailwoman/map-tui --tiles <archive.pmtiles> [options]
|
|
95
|
+
|
|
96
|
+
Options
|
|
97
|
+
--tiles <path|url> PMTiles archive — local path or https:// URL (default: $MAILWOMAN_TILES)
|
|
98
|
+
--lat <degrees> Initial center latitude, -90..90 (default: ${DEFAULT_LAT})
|
|
99
|
+
--lon <degrees> Initial center longitude, -180..180 (default: ${DEFAULT_LON})
|
|
100
|
+
--zoom <level> Initial zoom, ${MIN_ZOOM}..${MAX_ZOOM} (default: ${DEFAULT_ZOOM})
|
|
101
|
+
-h, --help Print this help and exit
|
|
102
|
+
-v, --version Print the package version and exit
|
|
103
|
+
|
|
104
|
+
Keys
|
|
105
|
+
arrows, hjkl Pan
|
|
106
|
+
+, =, a Zoom in
|
|
107
|
+
-, _, z Zoom out
|
|
108
|
+
q, Esc, Ctrl+C Quit
|
|
109
|
+
|
|
110
|
+
Mouse
|
|
111
|
+
Wheel zooms toward the pointer, drag pans, click centers.
|
|
112
|
+
|
|
113
|
+
Tiles are never bundled with this package. Download a planet or region
|
|
114
|
+
archive from https://protomaps.com/downloads and point --tiles at it.
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reads one numeric flag, rejecting anything `Number` would quietly accept as garbage (empty string, whitespace,
|
|
119
|
+
* `Infinity`) as well as out-of-range values.
|
|
120
|
+
*/
|
|
121
|
+
function numericFlag(name: string, raw: string | undefined, fallback: number, min: number, max: number): number {
|
|
122
|
+
if (raw == null) return fallback
|
|
123
|
+
|
|
124
|
+
const value = Number(raw.trim())
|
|
125
|
+
|
|
126
|
+
if (!Number.isFinite(value) || !raw.trim().length) {
|
|
127
|
+
throw new CLIArgsError(`--${name} expects a number, got ${JSON.stringify(raw)}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (value < min || value > max) {
|
|
131
|
+
throw new CLIArgsError(`--${name} must be between ${min} and ${max}, got ${value}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return value
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Flags whose value is a number, and may therefore start with a dash.
|
|
139
|
+
*/
|
|
140
|
+
const NUMERIC_FLAGS = new Set(["--lat", "--lon", "--zoom"])
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Joins `--lon -122.6` into `--lon=-122.6` before `parseArgs` sees it.
|
|
144
|
+
*
|
|
145
|
+
* `node:util`'s parser refuses a separate value that starts with a dash — it cannot tell a negative number from a
|
|
146
|
+
* mistyped flag, and says so ("argument is ambiguous"). Half the planet has a negative longitude, so the space-form has
|
|
147
|
+
* to work. The join is conditional on the next token parsing as a finite number, which leaves a genuinely missing value
|
|
148
|
+
* (`--lon --zoom 3`) to `parseArgs` and its own error.
|
|
149
|
+
*/
|
|
150
|
+
function joinNegativeNumbers(argv: readonly string[]): string[] {
|
|
151
|
+
const joined: string[] = []
|
|
152
|
+
|
|
153
|
+
for (let index = 0; index < argv.length; index++) {
|
|
154
|
+
const token = argv[index]!
|
|
155
|
+
const next = argv[index + 1]
|
|
156
|
+
|
|
157
|
+
if (NUMERIC_FLAGS.has(token) && next?.startsWith("-") && Number.isFinite(Number(next))) {
|
|
158
|
+
joined.push(`${token}=${next}`)
|
|
159
|
+
index += 1
|
|
160
|
+
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
joined.push(token)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return joined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface ParsedFlags {
|
|
171
|
+
tiles?: string | undefined
|
|
172
|
+
lat?: string | undefined
|
|
173
|
+
lon?: string | undefined
|
|
174
|
+
zoom?: string | undefined
|
|
175
|
+
help?: boolean | undefined
|
|
176
|
+
version?: boolean | undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* `node:util`'s own rejections (unknown flag, missing value) name the flag but not the remedy, so they're re-thrown as
|
|
181
|
+
* a {@link CLIArgsError} pointing at `--help`.
|
|
182
|
+
*/
|
|
183
|
+
function readFlags(argv: readonly string[]): ParsedFlags {
|
|
184
|
+
try {
|
|
185
|
+
const { values } = parseArguments({
|
|
186
|
+
args: joinNegativeNumbers(argv),
|
|
187
|
+
options: {
|
|
188
|
+
tiles: { type: "string" },
|
|
189
|
+
lat: { type: "string" },
|
|
190
|
+
lon: { type: "string" },
|
|
191
|
+
zoom: { type: "string" },
|
|
192
|
+
help: { type: "boolean", short: "h" },
|
|
193
|
+
version: { type: "boolean", short: "v" },
|
|
194
|
+
},
|
|
195
|
+
allowPositionals: false,
|
|
196
|
+
strict: true,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return values
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new CLIArgsError(`${errorMessage(error)}\nRun \`map-tui --help\` for the supported flags.`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Parses a `map-tui` command line.
|
|
207
|
+
*
|
|
208
|
+
* @throws {CLIArgsError} On an unknown flag, an unparseable or out-of-range number, or a missing archive path.
|
|
209
|
+
*/
|
|
210
|
+
export function parseCLIArgs(argv: readonly string[], environment: CLIEnvironment = {}): CLIArgs {
|
|
211
|
+
const values = readFlags(argv)
|
|
212
|
+
|
|
213
|
+
if (values.help) return { mode: "help" }
|
|
214
|
+
|
|
215
|
+
if (values.version) return { mode: "version" }
|
|
216
|
+
|
|
217
|
+
const tiles = (values.tiles ?? environment.MAILWOMAN_TILES ?? "").trim()
|
|
218
|
+
|
|
219
|
+
if (!tiles.length) {
|
|
220
|
+
throw new CLIArgsError(
|
|
221
|
+
"No tile archive: pass --tiles <archive.pmtiles> or set MAILWOMAN_TILES.\n" +
|
|
222
|
+
"Planet and region archives: https://protomaps.com/downloads"
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
mode: "browse",
|
|
228
|
+
tiles,
|
|
229
|
+
lat: numericFlag("lat", values.lat, DEFAULT_LAT, MIN_LAT, MAX_LAT),
|
|
230
|
+
lon: numericFlag("lon", values.lon, DEFAULT_LON, MIN_LON, MAX_LON),
|
|
231
|
+
zoom: Math.round(numericFlag("zoom", values.zoom, DEFAULT_ZOOM, MIN_ZOOM, MAX_ZOOM)),
|
|
232
|
+
}
|
|
233
|
+
}
|
package/lib/cli.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @copyright Sister Software.
|
|
5
|
+
* @license AGPL-3.0
|
|
6
|
+
* @author Teffen Ellis, et al.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The `map-tui` bin — `npx @mailwoman/map-tui` opens a PMTiles archive as a full-screen terminal map.
|
|
11
|
+
*
|
|
12
|
+
* This file owns everything process-shaped so the rest of the package stays testable without one: argv and the
|
|
13
|
+
* environment (parsed by ./cli-args.ts), the archive handle, signal handlers, and the exit code. `MapBrowser` takes
|
|
14
|
+
* streams rather than reaching for `process` itself, which is what lets the PTY smoke test drive the real bin and a
|
|
15
|
+
* unit test drive the parser with neither.
|
|
16
|
+
*
|
|
17
|
+
* Signal handling is not optional here. This is a raw-mode app on the alternate screen with mouse reporting on, and
|
|
18
|
+
* there is no framework underneath to put any of that back — a process killed between `start` and `restore` leaves the
|
|
19
|
+
* user with an unusable shell. So `restore` is wired to SIGINT, SIGTERM and `exit`, and it is idempotent for exactly
|
|
20
|
+
* that reason.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { errorMessage } from "@mailwoman/core/errors/schema"
|
|
24
|
+
import { createRequire } from "@mailwoman/core/module/resolvers"
|
|
25
|
+
|
|
26
|
+
import { MapBrowser } from "#browser"
|
|
27
|
+
import { type CLIArgs, CLIArgsError, HELP_TEXT, parseCLIArgs } from "#cli-args"
|
|
28
|
+
import { TileSource } from "#tile-source"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Exit code for a command line that could not be parsed.
|
|
32
|
+
*/
|
|
33
|
+
const EXIT_USAGE = 1
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reads the package's own version.
|
|
37
|
+
*
|
|
38
|
+
* Self-reference (`@mailwoman/map-tui/...`, which the package's `exports` publishes) rather than a path relative to
|
|
39
|
+
* this file: the bin runs from `out/` when installed and from the workspace root in development, and only the package
|
|
40
|
+
* graph knows which. `createRequire` parses the JSON itself, so no reader here has to.
|
|
41
|
+
*/
|
|
42
|
+
function readVersion(): string {
|
|
43
|
+
const require = createRequire(import.meta.url)
|
|
44
|
+
const manifest = require("@mailwoman/map-tui/package.json") as { version?: string }
|
|
45
|
+
|
|
46
|
+
return manifest.version ?? "0.0.0"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Opens the archive, translating the filesystem's error into something that names the flag that was wrong.
|
|
51
|
+
*/
|
|
52
|
+
async function openTiles(path: string): Promise<TileSource> {
|
|
53
|
+
try {
|
|
54
|
+
return await TileSource.open(path)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new CLIArgsError(
|
|
57
|
+
`Could not open the tile archive at ${path}: ${errorMessage(error)}\n` +
|
|
58
|
+
"Pass --tiles <archive.pmtiles>, or download one from https://protomaps.com/downloads"
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Runs the interactive browser against an already-open archive, resolving with its exit code.
|
|
65
|
+
*/
|
|
66
|
+
async function browse(source: TileSource, args: { lat: number; lon: number; zoom: number }): Promise<number> {
|
|
67
|
+
const browser = new MapBrowser({
|
|
68
|
+
source,
|
|
69
|
+
input: process.stdin,
|
|
70
|
+
output: process.stdout,
|
|
71
|
+
lat: args.lat,
|
|
72
|
+
lon: args.lon,
|
|
73
|
+
zoom: args.zoom,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const onSignal = (): void => browser.requestExit(130)
|
|
77
|
+
const onExit = (): void => browser.restore()
|
|
78
|
+
|
|
79
|
+
process.on("SIGINT", onSignal)
|
|
80
|
+
process.on("SIGTERM", onSignal)
|
|
81
|
+
process.on("exit", onExit)
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
return await browser.run()
|
|
85
|
+
} finally {
|
|
86
|
+
browser.restore()
|
|
87
|
+
process.off("SIGINT", onSignal)
|
|
88
|
+
process.off("SIGTERM", onSignal)
|
|
89
|
+
process.off("exit", onExit)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function main(): Promise<number> {
|
|
94
|
+
let args: CLIArgs
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
/**
|
|
98
|
+
* `parseCLIArgs` keeps argv and the environment read on this one line, and nowhere else in the package —
|
|
99
|
+
* `@mailwoman/core/env`'s typed readers would pull core's data-backed environment schema behind a CLI whose whole
|
|
100
|
+
* premise is `npx`, so the bin passes the raw records to the pure parser instead.
|
|
101
|
+
*/
|
|
102
|
+
// oxlint-disable-next-line sister-software/no-process-globals -- see above.
|
|
103
|
+
args = parseCLIArgs(process.argv.slice(2), process.env)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (!(error instanceof CLIArgsError)) throw error
|
|
106
|
+
|
|
107
|
+
process.stderr.write(`${error.message}\n`)
|
|
108
|
+
|
|
109
|
+
return EXIT_USAGE
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (args.mode === "help") {
|
|
113
|
+
process.stdout.write(HELP_TEXT)
|
|
114
|
+
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (args.mode === "version") {
|
|
119
|
+
process.stdout.write(`${readVersion()}\n`)
|
|
120
|
+
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let opened: TileSource
|
|
125
|
+
|
|
126
|
+
// A bad `--tiles` path is a usage error, not a crash — the guard stays around the open, and ownership passes to the
|
|
127
|
+
// `using` declaration only once the open succeeded.
|
|
128
|
+
try {
|
|
129
|
+
opened = await openTiles(args.tiles)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
process.stderr.write(`${errorMessage(error)}\n`)
|
|
132
|
+
|
|
133
|
+
return EXIT_USAGE
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await using source = opened
|
|
137
|
+
|
|
138
|
+
return await browse(source, args)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
process.exitCode = await main()
|
|
@@ -11,15 +11,13 @@
|
|
|
11
11
|
* per cell. The braille dither/luminance work is asciify's — `FrameRasterizer` subclasses `AsciifyTerminal` with a
|
|
12
12
|
* no-op sink purely to reach its protected `_computeBrailleCells`, `_cellChars`, `_cellColors`, so this module never
|
|
13
13
|
* re-implements the dot math. `frameToANSILines` and `overlayText` then work on the plain `MapFrame` value, with no
|
|
14
|
-
* further asciify dependency.
|
|
14
|
+
* further asciify dependency; `blitFrame` is the path back the other way, for callers driving a live terminal.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { AsciifyTerminal } from "@sister.software/asciify/tui"
|
|
17
|
+
import { AsciifyTerminal, SGR_RESET } from "@sister.software/asciify/tui"
|
|
18
18
|
|
|
19
|
-
import type { RGBAGrid } from "
|
|
20
|
-
import type { RGB } from "
|
|
21
|
-
|
|
22
|
-
const SGR_RESET = "\u001B[0m"
|
|
19
|
+
import type { RGBAGrid } from "#raster"
|
|
20
|
+
import type { RGB } from "#style"
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
23
|
* A rendered braille frame: one codepoint and one packed color per cell, row-major.
|
|
@@ -168,3 +166,32 @@ export function overlayText(
|
|
|
168
166
|
export function rgbToPacked(color: RGB): number {
|
|
169
167
|
return (color[0] << 16) | (color[1] << 8) | color[2]
|
|
170
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
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.
|
|
173
|
+
*/
|
|
174
|
+
const SPACE_CODEPOINT = 0x20
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Writes a frame's cells into an `AsciifyTerminal`'s current frame. Call `flush()` afterwards to emit the damage.
|
|
178
|
+
*
|
|
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.
|
|
182
|
+
*/
|
|
183
|
+
export function blitFrame(terminal: AsciifyTerminal, frame: MapFrame): void {
|
|
184
|
+
for (let row = 0; row < frame.rows; row++) {
|
|
185
|
+
for (let column = 0; column < frame.columns; column++) {
|
|
186
|
+
const cellIndex = row * frame.columns + column
|
|
187
|
+
const char = frame.chars[cellIndex]!
|
|
188
|
+
const color = frame.colors[cellIndex]!
|
|
189
|
+
|
|
190
|
+
terminal.setCell(column, row, char === 0 ? SPACE_CODEPOINT : char, [
|
|
191
|
+
(color >> 16) & 0xff,
|
|
192
|
+
(color >> 8) & 0xff,
|
|
193
|
+
color & 0xff,
|
|
194
|
+
])
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export * from "#frame"
|
|
8
|
+
export * from "#mercator"
|
|
9
|
+
export * from "#mvt"
|
|
10
|
+
export * from "#raster"
|
|
11
|
+
export * from "#renderer"
|
|
12
|
+
export * from "#style"
|
|
13
|
+
export * from "#tile-source"
|