compressed-context 0.1.0 → 0.1.1
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/package.json +64 -64
- package/src/compression/active-summary-cache.ts +50 -50
- package/src/compression/adopt.ts +57 -57
- package/src/compression/auto.ts +372 -372
- package/src/compression/babel.ts +85 -85
- package/src/compression/block.ts +25 -25
- package/src/compression/cache-history.ts +75 -75
- package/src/compression/compaction.ts +189 -189
- package/src/compression/decompress-tool.ts +74 -74
- package/src/compression/decompress.ts +292 -292
- package/src/compression/deleted-sessions.ts +24 -24
- package/src/compression/distill.ts +220 -220
- package/src/compression/events.ts +275 -275
- package/src/compression/markdown.ts +53 -53
- package/src/compression/normalize.ts +114 -114
- package/src/compression/render.ts +70 -70
- package/src/compression/schema.ts +84 -84
- package/src/compression/select.ts +123 -123
- package/src/compression/session-message.ts +112 -112
- package/src/compression/settings-rpc.ts +111 -111
- package/src/compression/settings.ts +175 -175
- package/src/compression/state.ts +248 -248
- package/src/compression/summarize.ts +595 -595
- package/src/compression/summary.ts +73 -73
- package/src/compression/tool.ts +142 -142
- package/src/compression/transcript-store.ts +258 -258
- package/src/compression/transcript.ts +63 -63
- package/src/compression/validate.ts +81 -81
- package/src/i18n/en.ts +93 -93
- package/src/i18n/index.ts +28 -28
- package/src/i18n/types.ts +89 -89
- package/src/i18n/zh.ts +93 -93
- package/src/index.ts +108 -108
- package/src/shared/blocks.ts +17 -17
- package/src/shared/cache.ts +199 -199
- package/src/shared/format.ts +23 -23
- package/src/shared/media.ts +184 -184
- package/src/shared/parse.ts +25 -25
- package/src/shared/parts.ts +72 -72
- package/src/shared/rpc.ts +145 -145
- package/src/shared/scan.ts +32 -32
- package/src/shared/settings.ts +471 -471
- package/src/shared/stats.ts +142 -142
- package/src/shared/tokens.ts +259 -259
- package/src/tui/chart/cache-chart.ts +237 -237
- package/src/tui/chart/color.ts +58 -58
- package/src/tui/index.tsx +111 -111
- package/src/tui/panel/cache.tsx +203 -203
- package/src/tui/panel/data.ts +105 -105
- package/src/tui/panel/index.tsx +176 -176
- package/src/tui/panel/summaries.tsx +192 -192
- package/src/tui/settings/dialogs.tsx +215 -215
- package/src/tui/settings/options.ts +238 -238
- package/src/tui/settings.tsx +418 -418
- package/src/tui/types.ts +4 -4
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
import { mix, toHex, toRgb, type Rgb } from "./color.ts"
|
|
2
|
-
|
|
3
|
-
/** 侧栏里折线图的默认尺寸:28 列 × 10 行(点阵 56 × 40)。 */
|
|
4
|
-
export const CHART_WIDTH = 28
|
|
5
|
-
export const CHART_HEIGHT = 10
|
|
6
|
-
|
|
7
|
-
/** 一个字符格:Braille 字符或空占位;空格的 color 为空串。 */
|
|
8
|
-
export interface ChartCell {
|
|
9
|
-
readonly char: string
|
|
10
|
-
readonly color: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** 命中率渐变色需要的三个主题色。 */
|
|
14
|
-
export interface ChartColors {
|
|
15
|
-
readonly success: string
|
|
16
|
-
readonly warning: string
|
|
17
|
-
readonly error: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Braille 点阵位映射:每个字符 2 列 × 4 行。
|
|
22
|
-
* 数组下标是 [dy][dx],值是 U+2800 的位。
|
|
23
|
-
*/
|
|
24
|
-
const BRAILLE_BITS = [
|
|
25
|
-
[0x01, 0x08],
|
|
26
|
-
[0x02, 0x10],
|
|
27
|
-
[0x04, 0x20],
|
|
28
|
-
[0x40, 0x80],
|
|
29
|
-
] as const
|
|
30
|
-
|
|
31
|
-
/** 空 cell 用不换行空格,防止 TUI 把连续空格吞掉。 */
|
|
32
|
-
const EMPTY_CELL = "\u00A0"
|
|
33
|
-
|
|
34
|
-
/** 网格线字符;只在没有折线点的 cell 上显示。 */
|
|
35
|
-
const GRID_CELL = "·"
|
|
36
|
-
|
|
37
|
-
/** 一条 y 轴刻度:行号 + 文本(`100%` / ` 75%` …)。 */
|
|
38
|
-
export interface ChartAxisLabel {
|
|
39
|
-
readonly row: number
|
|
40
|
-
readonly text: string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 输入:图表高度(行)+ 标签步长(默认 10%)+ 网格步长(默认 20%)。
|
|
45
|
-
* 输出:100/90/…/10 的刻度行号与文本,以及 80/60/40/20 的网格行。
|
|
46
|
-
* 预期:10 行时 10% 正好一行;行不够时同行的低刻度会被丢弃,避免标签重叠。
|
|
47
|
-
*/
|
|
48
|
-
export function chartAxis(
|
|
49
|
-
height: number,
|
|
50
|
-
labelStep = 10,
|
|
51
|
-
gridStep = 20,
|
|
52
|
-
): {
|
|
53
|
-
readonly labels: readonly ChartAxisLabel[]
|
|
54
|
-
readonly gridRows: readonly number[]
|
|
55
|
-
} {
|
|
56
|
-
const rows = Math.max(1, Math.floor(height))
|
|
57
|
-
const safeLabel = Math.max(1, Math.floor(labelStep))
|
|
58
|
-
const safeGrid = Math.max(1, Math.floor(gridStep))
|
|
59
|
-
const seen = new Set<number>()
|
|
60
|
-
const labels: ChartAxisLabel[] = []
|
|
61
|
-
for (let value = 100; value > 0; value -= safeLabel) {
|
|
62
|
-
const row = Math.min(rows - 1, Math.max(0, Math.round(((100 - value) / 100) * rows)))
|
|
63
|
-
if (seen.has(row)) continue
|
|
64
|
-
seen.add(row)
|
|
65
|
-
labels.push({ row, text: `${value.toString().padStart(3, " ")}%` })
|
|
66
|
-
}
|
|
67
|
-
const gridRows: number[] = []
|
|
68
|
-
for (let value = 100 - safeGrid; value > 0; value -= safeGrid) {
|
|
69
|
-
const row = Math.min(rows - 1, Math.max(0, Math.round(((100 - value) / 100) * rows)))
|
|
70
|
-
if (!gridRows.includes(row)) gridRows.push(row)
|
|
71
|
-
}
|
|
72
|
-
return { labels, gridRows }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 输入:样本数 + 绘图区宽度。
|
|
77
|
-
* 输出:长度等于宽度的 x 轴标签行;每个点标 `1`…`n`。
|
|
78
|
-
* 预期:标签居中在点的列上;宽度不够时后面的标签会覆盖前面的,属于可接受降级。
|
|
79
|
-
*/
|
|
80
|
-
export function xAxisLabels(count: number, width: number): string {
|
|
81
|
-
const columns = Math.max(1, Math.floor(width))
|
|
82
|
-
const chars = Array.from({ length: columns }, () => " ")
|
|
83
|
-
if (count <= 0) return chars.join("")
|
|
84
|
-
for (let index = 0; index < count; index += 1) {
|
|
85
|
-
const label = (index + 1).toString()
|
|
86
|
-
const position =
|
|
87
|
-
count === 1 ? Math.floor((columns - 1) / 2) : Math.round((index * (columns - 1)) / (count - 1))
|
|
88
|
-
const start = Math.min(columns - label.length, Math.max(0, position - Math.floor(label.length / 2)))
|
|
89
|
-
for (let offset = 0; offset < label.length; offset += 1) {
|
|
90
|
-
const target = start + offset
|
|
91
|
-
if (target >= 0 && target < columns) chars[target] = label.charAt(offset)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return chars.join("")
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 输入:任意百分比;输出:夹在 0..100 的有限值。 */
|
|
98
|
-
function clampPercent(value: number): number {
|
|
99
|
-
if (!Number.isFinite(value)) return 0
|
|
100
|
-
return Math.min(100, Math.max(0, value))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 输入:命中率 + 主题色。
|
|
105
|
-
* 输出:hex 颜色;0% 红、50% 黄、100% 绿,中间线性插值。
|
|
106
|
-
*/
|
|
107
|
-
export function hitRateColor(pct: number, colors: ChartColors): string {
|
|
108
|
-
const red = toRgb(colors.error) ?? ([239, 68, 68] as Rgb)
|
|
109
|
-
const yellow = toRgb(colors.warning) ?? ([234, 179, 8] as Rgb)
|
|
110
|
-
const green = toRgb(colors.success) ?? ([34, 197, 94] as Rgb)
|
|
111
|
-
const ratio = clampPercent(pct) / 100
|
|
112
|
-
const rgb: Rgb =
|
|
113
|
-
ratio >= 0.5 ? mix(yellow, green, (ratio - 0.5) * 2) : mix(red, yellow, ratio * 2)
|
|
114
|
-
return toHex(rgb)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 输入:点阵坐标;输出:无;预期:越界点忽略。 */
|
|
118
|
-
function plot(points: Set<number>, width: number, height: number, x: number, y: number): void {
|
|
119
|
-
if (x < 0 || x >= width || y < 0 || y >= height) return
|
|
120
|
-
points.add(y * width + x)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** 输入:两个点阵坐标;输出:无;预期:Bresenham 直线写入 points。 */
|
|
124
|
-
function line(
|
|
125
|
-
points: Set<number>,
|
|
126
|
-
width: number,
|
|
127
|
-
height: number,
|
|
128
|
-
x0: number,
|
|
129
|
-
y0: number,
|
|
130
|
-
x1: number,
|
|
131
|
-
y1: number,
|
|
132
|
-
): void {
|
|
133
|
-
const dx = Math.abs(x1 - x0)
|
|
134
|
-
const sx = x0 < x1 ? 1 : -1
|
|
135
|
-
const dy = -Math.abs(y1 - y0)
|
|
136
|
-
const sy = y0 < y1 ? 1 : -1
|
|
137
|
-
let error = dx + dy
|
|
138
|
-
let x = x0
|
|
139
|
-
let y = y0
|
|
140
|
-
for (;;) {
|
|
141
|
-
plot(points, width, height, x, y)
|
|
142
|
-
if (x === x1 && y === y1) return
|
|
143
|
-
const doubled = 2 * error
|
|
144
|
-
if (doubled >= dy) {
|
|
145
|
-
error += dy
|
|
146
|
-
x += sx
|
|
147
|
-
}
|
|
148
|
-
if (doubled <= dx) {
|
|
149
|
-
error += dx
|
|
150
|
-
y += sy
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** 输入:列号 + 宽度 + 原始值;输出:该列中心插值出的百分比。 */
|
|
156
|
-
function valueAtColumn(column: number, width: number, values: readonly number[]): number {
|
|
157
|
-
if (values.length === 1) return clampPercent(values[0] ?? 0)
|
|
158
|
-
const ratio = (column + 0.5) / width
|
|
159
|
-
const position = ratio * (values.length - 1)
|
|
160
|
-
const left = Math.floor(position)
|
|
161
|
-
const right = Math.min(values.length - 1, left + 1)
|
|
162
|
-
const fraction = position - left
|
|
163
|
-
const a = clampPercent(values[left] ?? 0)
|
|
164
|
-
const b = clampPercent(values[right] ?? a)
|
|
165
|
-
return a + (b - a) * fraction
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 输入:命中率序列(旧 → 新)、宽、高、逐列颜色函数。
|
|
170
|
-
* 输出:按行排列的 Braille 折线;空 cell 是 NBSP。
|
|
171
|
-
* 预期:0% 在底行,100% 在顶行;单点画水平线;空数组返回空。
|
|
172
|
-
*/
|
|
173
|
-
export function renderBrailleChart(
|
|
174
|
-
values: readonly number[],
|
|
175
|
-
width: number = CHART_WIDTH,
|
|
176
|
-
height: number = CHART_HEIGHT,
|
|
177
|
-
color: (pct: number) => string = () => "",
|
|
178
|
-
gridRows: readonly number[] = [],
|
|
179
|
-
gridColor = "",
|
|
180
|
-
): readonly (readonly ChartCell[])[] {
|
|
181
|
-
if (values.length === 0) return []
|
|
182
|
-
const columns = Math.max(1, Math.floor(width))
|
|
183
|
-
const rows = Math.max(1, Math.floor(height))
|
|
184
|
-
const dotWidth = columns * 2
|
|
185
|
-
const dotHeight = rows * 4
|
|
186
|
-
const points = new Set<number>()
|
|
187
|
-
// 上下各留 1 个点阵行,避免 0%/100% 贴边看起来越过刻度。
|
|
188
|
-
const top = 1
|
|
189
|
-
const bottom = Math.max(top, dotHeight - 2)
|
|
190
|
-
const yOf = (value: number): number =>
|
|
191
|
-
Math.min(
|
|
192
|
-
bottom,
|
|
193
|
-
Math.max(top, Math.round(top + (1 - clampPercent(value) / 100) * (bottom - top))),
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
if (values.length === 1) {
|
|
197
|
-
const value = values[0] ?? 0
|
|
198
|
-
line(points, dotWidth, dotHeight, 0, yOf(value), dotWidth - 1, yOf(value))
|
|
199
|
-
} else {
|
|
200
|
-
for (let index = 0; index < values.length - 1; index += 1) {
|
|
201
|
-
const x0 = Math.round((index * (dotWidth - 1)) / (values.length - 1))
|
|
202
|
-
const x1 = Math.round(((index + 1) * (dotWidth - 1)) / (values.length - 1))
|
|
203
|
-
line(
|
|
204
|
-
points,
|
|
205
|
-
dotWidth,
|
|
206
|
-
dotHeight,
|
|
207
|
-
x0,
|
|
208
|
-
yOf(values[index] ?? 0),
|
|
209
|
-
x1,
|
|
210
|
-
yOf(values[index + 1] ?? 0),
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const result: ChartCell[][] = []
|
|
216
|
-
for (let row = 0; row < rows; row += 1) {
|
|
217
|
-
const cells: ChartCell[] = []
|
|
218
|
-
for (let column = 0; column < columns; column += 1) {
|
|
219
|
-
let bits = 0
|
|
220
|
-
for (let dy = 0; dy < 4; dy += 1) {
|
|
221
|
-
for (let dx = 0; dx < 2; dx += 1) {
|
|
222
|
-
if (points.has((row * 4 + dy) * dotWidth + (column * 2 + dx))) {
|
|
223
|
-
bits |= BRAILLE_BITS[dy]?.[dx] ?? 0
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const pct = valueAtColumn(column, columns, values)
|
|
228
|
-
const grid = bits === 0 && gridRows.includes(row)
|
|
229
|
-
cells.push({
|
|
230
|
-
char: bits === 0 ? (grid ? GRID_CELL : EMPTY_CELL) : String.fromCharCode(0x2800 + bits),
|
|
231
|
-
color: bits === 0 ? (grid ? gridColor : "") : color(pct),
|
|
232
|
-
})
|
|
233
|
-
}
|
|
234
|
-
result.push(cells)
|
|
235
|
-
}
|
|
236
|
-
return result
|
|
237
|
-
}
|
|
1
|
+
import { mix, toHex, toRgb, type Rgb } from "./color.ts"
|
|
2
|
+
|
|
3
|
+
/** 侧栏里折线图的默认尺寸:28 列 × 10 行(点阵 56 × 40)。 */
|
|
4
|
+
export const CHART_WIDTH = 28
|
|
5
|
+
export const CHART_HEIGHT = 10
|
|
6
|
+
|
|
7
|
+
/** 一个字符格:Braille 字符或空占位;空格的 color 为空串。 */
|
|
8
|
+
export interface ChartCell {
|
|
9
|
+
readonly char: string
|
|
10
|
+
readonly color: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 命中率渐变色需要的三个主题色。 */
|
|
14
|
+
export interface ChartColors {
|
|
15
|
+
readonly success: string
|
|
16
|
+
readonly warning: string
|
|
17
|
+
readonly error: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Braille 点阵位映射:每个字符 2 列 × 4 行。
|
|
22
|
+
* 数组下标是 [dy][dx],值是 U+2800 的位。
|
|
23
|
+
*/
|
|
24
|
+
const BRAILLE_BITS = [
|
|
25
|
+
[0x01, 0x08],
|
|
26
|
+
[0x02, 0x10],
|
|
27
|
+
[0x04, 0x20],
|
|
28
|
+
[0x40, 0x80],
|
|
29
|
+
] as const
|
|
30
|
+
|
|
31
|
+
/** 空 cell 用不换行空格,防止 TUI 把连续空格吞掉。 */
|
|
32
|
+
const EMPTY_CELL = "\u00A0"
|
|
33
|
+
|
|
34
|
+
/** 网格线字符;只在没有折线点的 cell 上显示。 */
|
|
35
|
+
const GRID_CELL = "·"
|
|
36
|
+
|
|
37
|
+
/** 一条 y 轴刻度:行号 + 文本(`100%` / ` 75%` …)。 */
|
|
38
|
+
export interface ChartAxisLabel {
|
|
39
|
+
readonly row: number
|
|
40
|
+
readonly text: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 输入:图表高度(行)+ 标签步长(默认 10%)+ 网格步长(默认 20%)。
|
|
45
|
+
* 输出:100/90/…/10 的刻度行号与文本,以及 80/60/40/20 的网格行。
|
|
46
|
+
* 预期:10 行时 10% 正好一行;行不够时同行的低刻度会被丢弃,避免标签重叠。
|
|
47
|
+
*/
|
|
48
|
+
export function chartAxis(
|
|
49
|
+
height: number,
|
|
50
|
+
labelStep = 10,
|
|
51
|
+
gridStep = 20,
|
|
52
|
+
): {
|
|
53
|
+
readonly labels: readonly ChartAxisLabel[]
|
|
54
|
+
readonly gridRows: readonly number[]
|
|
55
|
+
} {
|
|
56
|
+
const rows = Math.max(1, Math.floor(height))
|
|
57
|
+
const safeLabel = Math.max(1, Math.floor(labelStep))
|
|
58
|
+
const safeGrid = Math.max(1, Math.floor(gridStep))
|
|
59
|
+
const seen = new Set<number>()
|
|
60
|
+
const labels: ChartAxisLabel[] = []
|
|
61
|
+
for (let value = 100; value > 0; value -= safeLabel) {
|
|
62
|
+
const row = Math.min(rows - 1, Math.max(0, Math.round(((100 - value) / 100) * rows)))
|
|
63
|
+
if (seen.has(row)) continue
|
|
64
|
+
seen.add(row)
|
|
65
|
+
labels.push({ row, text: `${value.toString().padStart(3, " ")}%` })
|
|
66
|
+
}
|
|
67
|
+
const gridRows: number[] = []
|
|
68
|
+
for (let value = 100 - safeGrid; value > 0; value -= safeGrid) {
|
|
69
|
+
const row = Math.min(rows - 1, Math.max(0, Math.round(((100 - value) / 100) * rows)))
|
|
70
|
+
if (!gridRows.includes(row)) gridRows.push(row)
|
|
71
|
+
}
|
|
72
|
+
return { labels, gridRows }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 输入:样本数 + 绘图区宽度。
|
|
77
|
+
* 输出:长度等于宽度的 x 轴标签行;每个点标 `1`…`n`。
|
|
78
|
+
* 预期:标签居中在点的列上;宽度不够时后面的标签会覆盖前面的,属于可接受降级。
|
|
79
|
+
*/
|
|
80
|
+
export function xAxisLabels(count: number, width: number): string {
|
|
81
|
+
const columns = Math.max(1, Math.floor(width))
|
|
82
|
+
const chars = Array.from({ length: columns }, () => " ")
|
|
83
|
+
if (count <= 0) return chars.join("")
|
|
84
|
+
for (let index = 0; index < count; index += 1) {
|
|
85
|
+
const label = (index + 1).toString()
|
|
86
|
+
const position =
|
|
87
|
+
count === 1 ? Math.floor((columns - 1) / 2) : Math.round((index * (columns - 1)) / (count - 1))
|
|
88
|
+
const start = Math.min(columns - label.length, Math.max(0, position - Math.floor(label.length / 2)))
|
|
89
|
+
for (let offset = 0; offset < label.length; offset += 1) {
|
|
90
|
+
const target = start + offset
|
|
91
|
+
if (target >= 0 && target < columns) chars[target] = label.charAt(offset)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return chars.join("")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 输入:任意百分比;输出:夹在 0..100 的有限值。 */
|
|
98
|
+
function clampPercent(value: number): number {
|
|
99
|
+
if (!Number.isFinite(value)) return 0
|
|
100
|
+
return Math.min(100, Math.max(0, value))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 输入:命中率 + 主题色。
|
|
105
|
+
* 输出:hex 颜色;0% 红、50% 黄、100% 绿,中间线性插值。
|
|
106
|
+
*/
|
|
107
|
+
export function hitRateColor(pct: number, colors: ChartColors): string {
|
|
108
|
+
const red = toRgb(colors.error) ?? ([239, 68, 68] as Rgb)
|
|
109
|
+
const yellow = toRgb(colors.warning) ?? ([234, 179, 8] as Rgb)
|
|
110
|
+
const green = toRgb(colors.success) ?? ([34, 197, 94] as Rgb)
|
|
111
|
+
const ratio = clampPercent(pct) / 100
|
|
112
|
+
const rgb: Rgb =
|
|
113
|
+
ratio >= 0.5 ? mix(yellow, green, (ratio - 0.5) * 2) : mix(red, yellow, ratio * 2)
|
|
114
|
+
return toHex(rgb)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 输入:点阵坐标;输出:无;预期:越界点忽略。 */
|
|
118
|
+
function plot(points: Set<number>, width: number, height: number, x: number, y: number): void {
|
|
119
|
+
if (x < 0 || x >= width || y < 0 || y >= height) return
|
|
120
|
+
points.add(y * width + x)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 输入:两个点阵坐标;输出:无;预期:Bresenham 直线写入 points。 */
|
|
124
|
+
function line(
|
|
125
|
+
points: Set<number>,
|
|
126
|
+
width: number,
|
|
127
|
+
height: number,
|
|
128
|
+
x0: number,
|
|
129
|
+
y0: number,
|
|
130
|
+
x1: number,
|
|
131
|
+
y1: number,
|
|
132
|
+
): void {
|
|
133
|
+
const dx = Math.abs(x1 - x0)
|
|
134
|
+
const sx = x0 < x1 ? 1 : -1
|
|
135
|
+
const dy = -Math.abs(y1 - y0)
|
|
136
|
+
const sy = y0 < y1 ? 1 : -1
|
|
137
|
+
let error = dx + dy
|
|
138
|
+
let x = x0
|
|
139
|
+
let y = y0
|
|
140
|
+
for (;;) {
|
|
141
|
+
plot(points, width, height, x, y)
|
|
142
|
+
if (x === x1 && y === y1) return
|
|
143
|
+
const doubled = 2 * error
|
|
144
|
+
if (doubled >= dy) {
|
|
145
|
+
error += dy
|
|
146
|
+
x += sx
|
|
147
|
+
}
|
|
148
|
+
if (doubled <= dx) {
|
|
149
|
+
error += dx
|
|
150
|
+
y += sy
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 输入:列号 + 宽度 + 原始值;输出:该列中心插值出的百分比。 */
|
|
156
|
+
function valueAtColumn(column: number, width: number, values: readonly number[]): number {
|
|
157
|
+
if (values.length === 1) return clampPercent(values[0] ?? 0)
|
|
158
|
+
const ratio = (column + 0.5) / width
|
|
159
|
+
const position = ratio * (values.length - 1)
|
|
160
|
+
const left = Math.floor(position)
|
|
161
|
+
const right = Math.min(values.length - 1, left + 1)
|
|
162
|
+
const fraction = position - left
|
|
163
|
+
const a = clampPercent(values[left] ?? 0)
|
|
164
|
+
const b = clampPercent(values[right] ?? a)
|
|
165
|
+
return a + (b - a) * fraction
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 输入:命中率序列(旧 → 新)、宽、高、逐列颜色函数。
|
|
170
|
+
* 输出:按行排列的 Braille 折线;空 cell 是 NBSP。
|
|
171
|
+
* 预期:0% 在底行,100% 在顶行;单点画水平线;空数组返回空。
|
|
172
|
+
*/
|
|
173
|
+
export function renderBrailleChart(
|
|
174
|
+
values: readonly number[],
|
|
175
|
+
width: number = CHART_WIDTH,
|
|
176
|
+
height: number = CHART_HEIGHT,
|
|
177
|
+
color: (pct: number) => string = () => "",
|
|
178
|
+
gridRows: readonly number[] = [],
|
|
179
|
+
gridColor = "",
|
|
180
|
+
): readonly (readonly ChartCell[])[] {
|
|
181
|
+
if (values.length === 0) return []
|
|
182
|
+
const columns = Math.max(1, Math.floor(width))
|
|
183
|
+
const rows = Math.max(1, Math.floor(height))
|
|
184
|
+
const dotWidth = columns * 2
|
|
185
|
+
const dotHeight = rows * 4
|
|
186
|
+
const points = new Set<number>()
|
|
187
|
+
// 上下各留 1 个点阵行,避免 0%/100% 贴边看起来越过刻度。
|
|
188
|
+
const top = 1
|
|
189
|
+
const bottom = Math.max(top, dotHeight - 2)
|
|
190
|
+
const yOf = (value: number): number =>
|
|
191
|
+
Math.min(
|
|
192
|
+
bottom,
|
|
193
|
+
Math.max(top, Math.round(top + (1 - clampPercent(value) / 100) * (bottom - top))),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if (values.length === 1) {
|
|
197
|
+
const value = values[0] ?? 0
|
|
198
|
+
line(points, dotWidth, dotHeight, 0, yOf(value), dotWidth - 1, yOf(value))
|
|
199
|
+
} else {
|
|
200
|
+
for (let index = 0; index < values.length - 1; index += 1) {
|
|
201
|
+
const x0 = Math.round((index * (dotWidth - 1)) / (values.length - 1))
|
|
202
|
+
const x1 = Math.round(((index + 1) * (dotWidth - 1)) / (values.length - 1))
|
|
203
|
+
line(
|
|
204
|
+
points,
|
|
205
|
+
dotWidth,
|
|
206
|
+
dotHeight,
|
|
207
|
+
x0,
|
|
208
|
+
yOf(values[index] ?? 0),
|
|
209
|
+
x1,
|
|
210
|
+
yOf(values[index + 1] ?? 0),
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const result: ChartCell[][] = []
|
|
216
|
+
for (let row = 0; row < rows; row += 1) {
|
|
217
|
+
const cells: ChartCell[] = []
|
|
218
|
+
for (let column = 0; column < columns; column += 1) {
|
|
219
|
+
let bits = 0
|
|
220
|
+
for (let dy = 0; dy < 4; dy += 1) {
|
|
221
|
+
for (let dx = 0; dx < 2; dx += 1) {
|
|
222
|
+
if (points.has((row * 4 + dy) * dotWidth + (column * 2 + dx))) {
|
|
223
|
+
bits |= BRAILLE_BITS[dy]?.[dx] ?? 0
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const pct = valueAtColumn(column, columns, values)
|
|
228
|
+
const grid = bits === 0 && gridRows.includes(row)
|
|
229
|
+
cells.push({
|
|
230
|
+
char: bits === 0 ? (grid ? GRID_CELL : EMPTY_CELL) : String.fromCharCode(0x2800 + bits),
|
|
231
|
+
color: bits === 0 ? (grid ? gridColor : "") : color(pct),
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
result.push(cells)
|
|
235
|
+
}
|
|
236
|
+
return result
|
|
237
|
+
}
|
package/src/tui/chart/color.ts
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
/** 颜色三元组,分量 0-255。 */
|
|
2
|
-
export type Rgb = readonly [number, number, number]
|
|
3
|
-
|
|
4
|
-
/** 输入:0-255 分量;输出:两位十六进制。 */
|
|
5
|
-
function channel(value: number): string {
|
|
6
|
-
return Math.round(value).toString(16).padStart(2, "0")
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** 输入:颜色值;输出:RGB;不是 hex 返回 undefined。 */
|
|
10
|
-
export function parseHex(value: string): Rgb | undefined {
|
|
11
|
-
const match = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(value.trim())
|
|
12
|
-
const raw = match?.[1]
|
|
13
|
-
if (raw === undefined) return undefined
|
|
14
|
-
if (raw.length === 3 || raw.length === 4) {
|
|
15
|
-
return [
|
|
16
|
-
parseInt(raw.charAt(0).repeat(2), 16),
|
|
17
|
-
parseInt(raw.charAt(1).repeat(2), 16),
|
|
18
|
-
parseInt(raw.charAt(2).repeat(2), 16),
|
|
19
|
-
]
|
|
20
|
-
}
|
|
21
|
-
return [
|
|
22
|
-
parseInt(raw.slice(0, 2), 16),
|
|
23
|
-
parseInt(raw.slice(2, 4), 16),
|
|
24
|
-
parseInt(raw.slice(4, 6), 16),
|
|
25
|
-
]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 输入:两个颜色 + 0-1 比例;输出:线性插值后的颜色。 */
|
|
29
|
-
export function mix(from: Rgb, to: Rgb, ratio: number): Rgb {
|
|
30
|
-
return [
|
|
31
|
-
from[0] + (to[0] - from[0]) * ratio,
|
|
32
|
-
from[1] + (to[1] - from[1]) * ratio,
|
|
33
|
-
from[2] + (to[2] - from[2]) * ratio,
|
|
34
|
-
]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 输入:RGB;输出:hex 字符串。 */
|
|
38
|
-
export function toHex(rgb: Rgb): string {
|
|
39
|
-
return `#${channel(rgb[0])}${channel(rgb[1])}${channel(rgb[2])}`
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 输入:颜色值(hex 字符串或 OpenTUI RGBA);输出:RGB。 */
|
|
43
|
-
export function toRgb(value: unknown): Rgb | undefined {
|
|
44
|
-
if (typeof value === "string") return parseHex(value)
|
|
45
|
-
if (typeof value === "object" && value !== null) {
|
|
46
|
-
const color = value as { readonly r?: unknown; readonly g?: unknown; readonly b?: unknown }
|
|
47
|
-
if (typeof color.r === "number" && typeof color.g === "number" && typeof color.b === "number") {
|
|
48
|
-
// OpenTUI 的 RGBA.r/g/b 是 0-1 归一化值,这里转成 0-255。
|
|
49
|
-
return [color.r * 255, color.g * 255, color.b * 255]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return undefined
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** 输入:主题色 + 回退 RGB;输出:hex 字符串。 */
|
|
56
|
-
export function themeHex(value: unknown, fallback: Rgb): string {
|
|
57
|
-
return toHex(toRgb(value) ?? fallback)
|
|
58
|
-
}
|
|
1
|
+
/** 颜色三元组,分量 0-255。 */
|
|
2
|
+
export type Rgb = readonly [number, number, number]
|
|
3
|
+
|
|
4
|
+
/** 输入:0-255 分量;输出:两位十六进制。 */
|
|
5
|
+
function channel(value: number): string {
|
|
6
|
+
return Math.round(value).toString(16).padStart(2, "0")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** 输入:颜色值;输出:RGB;不是 hex 返回 undefined。 */
|
|
10
|
+
export function parseHex(value: string): Rgb | undefined {
|
|
11
|
+
const match = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(value.trim())
|
|
12
|
+
const raw = match?.[1]
|
|
13
|
+
if (raw === undefined) return undefined
|
|
14
|
+
if (raw.length === 3 || raw.length === 4) {
|
|
15
|
+
return [
|
|
16
|
+
parseInt(raw.charAt(0).repeat(2), 16),
|
|
17
|
+
parseInt(raw.charAt(1).repeat(2), 16),
|
|
18
|
+
parseInt(raw.charAt(2).repeat(2), 16),
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
return [
|
|
22
|
+
parseInt(raw.slice(0, 2), 16),
|
|
23
|
+
parseInt(raw.slice(2, 4), 16),
|
|
24
|
+
parseInt(raw.slice(4, 6), 16),
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 输入:两个颜色 + 0-1 比例;输出:线性插值后的颜色。 */
|
|
29
|
+
export function mix(from: Rgb, to: Rgb, ratio: number): Rgb {
|
|
30
|
+
return [
|
|
31
|
+
from[0] + (to[0] - from[0]) * ratio,
|
|
32
|
+
from[1] + (to[1] - from[1]) * ratio,
|
|
33
|
+
from[2] + (to[2] - from[2]) * ratio,
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 输入:RGB;输出:hex 字符串。 */
|
|
38
|
+
export function toHex(rgb: Rgb): string {
|
|
39
|
+
return `#${channel(rgb[0])}${channel(rgb[1])}${channel(rgb[2])}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 输入:颜色值(hex 字符串或 OpenTUI RGBA);输出:RGB。 */
|
|
43
|
+
export function toRgb(value: unknown): Rgb | undefined {
|
|
44
|
+
if (typeof value === "string") return parseHex(value)
|
|
45
|
+
if (typeof value === "object" && value !== null) {
|
|
46
|
+
const color = value as { readonly r?: unknown; readonly g?: unknown; readonly b?: unknown }
|
|
47
|
+
if (typeof color.r === "number" && typeof color.g === "number" && typeof color.b === "number") {
|
|
48
|
+
// OpenTUI 的 RGBA.r/g/b 是 0-1 归一化值,这里转成 0-255。
|
|
49
|
+
return [color.r * 255, color.g * 255, color.b * 255]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 输入:主题色 + 回退 RGB;输出:hex 字符串。 */
|
|
56
|
+
export function themeHex(value: unknown, fallback: Rgb): string {
|
|
57
|
+
return toHex(toRgb(value) ?? fallback)
|
|
58
|
+
}
|