@teammates/consolonia 0.2.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 +48 -0
- package/dist/__tests__/ansi.test.d.ts +1 -0
- package/dist/__tests__/ansi.test.js +520 -0
- package/dist/__tests__/chat-view.test.d.ts +4 -0
- package/dist/__tests__/chat-view.test.js +480 -0
- package/dist/__tests__/drawing.test.d.ts +4 -0
- package/dist/__tests__/drawing.test.js +426 -0
- package/dist/__tests__/input.test.d.ts +5 -0
- package/dist/__tests__/input.test.js +911 -0
- package/dist/__tests__/layout.test.d.ts +4 -0
- package/dist/__tests__/layout.test.js +689 -0
- package/dist/__tests__/pixel.test.d.ts +1 -0
- package/dist/__tests__/pixel.test.js +674 -0
- package/dist/__tests__/render.test.d.ts +1 -0
- package/dist/__tests__/render.test.js +400 -0
- package/dist/__tests__/styled.test.d.ts +4 -0
- package/dist/__tests__/styled.test.js +149 -0
- package/dist/__tests__/widgets.test.d.ts +5 -0
- package/dist/__tests__/widgets.test.js +924 -0
- package/dist/ansi/esc.d.ts +61 -0
- package/dist/ansi/esc.js +85 -0
- package/dist/ansi/output.d.ts +66 -0
- package/dist/ansi/output.js +192 -0
- package/dist/ansi/strip.d.ts +16 -0
- package/dist/ansi/strip.js +74 -0
- package/dist/app.d.ts +68 -0
- package/dist/app.js +297 -0
- package/dist/drawing/clip.d.ts +23 -0
- package/dist/drawing/clip.js +67 -0
- package/dist/drawing/context.d.ts +77 -0
- package/dist/drawing/context.js +275 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +63 -0
- package/dist/input/escape-matcher.d.ts +27 -0
- package/dist/input/escape-matcher.js +253 -0
- package/dist/input/events.d.ts +49 -0
- package/dist/input/events.js +17 -0
- package/dist/input/index.d.ts +15 -0
- package/dist/input/index.js +14 -0
- package/dist/input/matcher.d.ts +23 -0
- package/dist/input/matcher.js +14 -0
- package/dist/input/mouse-matcher.d.ts +27 -0
- package/dist/input/mouse-matcher.js +142 -0
- package/dist/input/paste-matcher.d.ts +23 -0
- package/dist/input/paste-matcher.js +104 -0
- package/dist/input/processor.d.ts +51 -0
- package/dist/input/processor.js +145 -0
- package/dist/input/raw-mode.d.ts +13 -0
- package/dist/input/raw-mode.js +24 -0
- package/dist/input/text-matcher.d.ts +14 -0
- package/dist/input/text-matcher.js +32 -0
- package/dist/layout/box.d.ts +33 -0
- package/dist/layout/box.js +92 -0
- package/dist/layout/column.d.ts +21 -0
- package/dist/layout/column.js +90 -0
- package/dist/layout/control.d.ts +73 -0
- package/dist/layout/control.js +215 -0
- package/dist/layout/row.d.ts +21 -0
- package/dist/layout/row.js +95 -0
- package/dist/layout/stack.d.ts +18 -0
- package/dist/layout/stack.js +64 -0
- package/dist/layout/types.d.ts +27 -0
- package/dist/layout/types.js +4 -0
- package/dist/pixel/background.d.ts +16 -0
- package/dist/pixel/background.js +16 -0
- package/dist/pixel/box-pattern.d.ts +38 -0
- package/dist/pixel/box-pattern.js +57 -0
- package/dist/pixel/buffer.d.ts +25 -0
- package/dist/pixel/buffer.js +51 -0
- package/dist/pixel/color.d.ts +48 -0
- package/dist/pixel/color.js +92 -0
- package/dist/pixel/foreground.d.ts +31 -0
- package/dist/pixel/foreground.js +64 -0
- package/dist/pixel/pixel.d.ts +21 -0
- package/dist/pixel/pixel.js +38 -0
- package/dist/pixel/symbol.d.ts +38 -0
- package/dist/pixel/symbol.js +192 -0
- package/dist/render/regions.d.ts +54 -0
- package/dist/render/regions.js +102 -0
- package/dist/render/render-target.d.ts +42 -0
- package/dist/render/render-target.js +118 -0
- package/dist/styled.d.ts +113 -0
- package/dist/styled.js +176 -0
- package/dist/widgets/border.d.ts +34 -0
- package/dist/widgets/border.js +121 -0
- package/dist/widgets/chat-view.d.ts +239 -0
- package/dist/widgets/chat-view.js +993 -0
- package/dist/widgets/interview.d.ts +87 -0
- package/dist/widgets/interview.js +187 -0
- package/dist/widgets/markdown.d.ts +87 -0
- package/dist/widgets/markdown.js +611 -0
- package/dist/widgets/panel.d.ts +19 -0
- package/dist/widgets/panel.js +35 -0
- package/dist/widgets/scroll-view.d.ts +43 -0
- package/dist/widgets/scroll-view.js +182 -0
- package/dist/widgets/styled-text.d.ts +38 -0
- package/dist/widgets/styled-text.js +183 -0
- package/dist/widgets/syntax.d.ts +37 -0
- package/dist/widgets/syntax.js +670 -0
- package/dist/widgets/text-input.d.ts +121 -0
- package/dist/widgets/text-input.js +618 -0
- package/dist/widgets/text.d.ts +34 -0
- package/dist/widgets/text.js +168 -0
- package/package.json +45 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Phase 5: Drawing Context (ClipStack + DrawingContext).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { ClipStack } from "../drawing/clip.js";
|
|
6
|
+
import { DrawingContext } from "../drawing/context.js";
|
|
7
|
+
import { BOX_CHARS, DOWN, LEFT, RIGHT, UP } from "../pixel/box-pattern.js";
|
|
8
|
+
import { PixelBuffer } from "../pixel/buffer.js";
|
|
9
|
+
import { BLUE, GREEN, RED, WHITE } from "../pixel/color.js";
|
|
10
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
11
|
+
/** Read the character text at (x, y) from a buffer. */
|
|
12
|
+
function charAt(buf, x, y) {
|
|
13
|
+
return buf.get(x, y).foreground.symbol.text;
|
|
14
|
+
}
|
|
15
|
+
/** Read the box pattern at (x, y) from a buffer. */
|
|
16
|
+
function patternAt(buf, x, y) {
|
|
17
|
+
return buf.get(x, y).foreground.symbol.pattern;
|
|
18
|
+
}
|
|
19
|
+
/** Read the background color at (x, y). */
|
|
20
|
+
function bgAt(buf, x, y) {
|
|
21
|
+
return buf.get(x, y).background.color;
|
|
22
|
+
}
|
|
23
|
+
/** Read the foreground color at (x, y). */
|
|
24
|
+
function fgAt(buf, x, y) {
|
|
25
|
+
return buf.get(x, y).foreground.color;
|
|
26
|
+
}
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
28
|
+
// ClipStack
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
30
|
+
describe("ClipStack", () => {
|
|
31
|
+
it("empty stack: contains() returns true for any point", () => {
|
|
32
|
+
const cs = new ClipStack();
|
|
33
|
+
expect(cs.contains(0, 0)).toBe(true);
|
|
34
|
+
expect(cs.contains(100, 200)).toBe(true);
|
|
35
|
+
expect(cs.contains(-5, -10)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it("single push: only points inside the rect pass", () => {
|
|
38
|
+
const cs = new ClipStack();
|
|
39
|
+
cs.push({ x: 2, y: 3, width: 5, height: 4 });
|
|
40
|
+
// Inside
|
|
41
|
+
expect(cs.contains(2, 3)).toBe(true);
|
|
42
|
+
expect(cs.contains(6, 6)).toBe(true); // x=6 < 2+5=7, y=6 < 3+4=7
|
|
43
|
+
// On exclusive boundary (right/bottom edge)
|
|
44
|
+
expect(cs.contains(7, 3)).toBe(false);
|
|
45
|
+
expect(cs.contains(2, 7)).toBe(false);
|
|
46
|
+
// Outside
|
|
47
|
+
expect(cs.contains(1, 3)).toBe(false);
|
|
48
|
+
expect(cs.contains(2, 2)).toBe(false);
|
|
49
|
+
expect(cs.contains(10, 10)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
it("nested push: intersection narrows the visible area", () => {
|
|
52
|
+
const cs = new ClipStack();
|
|
53
|
+
cs.push({ x: 0, y: 0, width: 10, height: 10 });
|
|
54
|
+
cs.push({ x: 5, y: 5, width: 10, height: 10 });
|
|
55
|
+
// Intersection is { x: 5, y: 5, width: 5, height: 5 }
|
|
56
|
+
expect(cs.contains(5, 5)).toBe(true);
|
|
57
|
+
expect(cs.contains(9, 9)).toBe(true);
|
|
58
|
+
// In the first rect but not the intersection
|
|
59
|
+
expect(cs.contains(0, 0)).toBe(false);
|
|
60
|
+
expect(cs.contains(4, 4)).toBe(false);
|
|
61
|
+
// In the second rect but not the intersection
|
|
62
|
+
expect(cs.contains(10, 10)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
it("pop restores previous clip", () => {
|
|
65
|
+
const cs = new ClipStack();
|
|
66
|
+
cs.push({ x: 0, y: 0, width: 10, height: 10 });
|
|
67
|
+
cs.push({ x: 5, y: 5, width: 10, height: 10 });
|
|
68
|
+
// After nested push, (0,0) is clipped
|
|
69
|
+
expect(cs.contains(0, 0)).toBe(false);
|
|
70
|
+
cs.pop();
|
|
71
|
+
// After pop, we're back to the first clip: (0,0) is visible
|
|
72
|
+
expect(cs.contains(0, 0)).toBe(true);
|
|
73
|
+
expect(cs.contains(9, 9)).toBe(true);
|
|
74
|
+
cs.pop();
|
|
75
|
+
// After popping all, everything is visible
|
|
76
|
+
expect(cs.contains(0, 0)).toBe(true);
|
|
77
|
+
expect(cs.contains(100, 100)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
it("pop on empty stack throws", () => {
|
|
80
|
+
const cs = new ClipStack();
|
|
81
|
+
expect(() => cs.pop()).toThrow("ClipStack underflow");
|
|
82
|
+
});
|
|
83
|
+
it("degenerate (zero area) clip rejects everything", () => {
|
|
84
|
+
const cs = new ClipStack();
|
|
85
|
+
// Push a non-overlapping rect pair to get a null intersection
|
|
86
|
+
cs.push({ x: 0, y: 0, width: 5, height: 5 });
|
|
87
|
+
cs.push({ x: 10, y: 10, width: 5, height: 5 });
|
|
88
|
+
expect(cs.contains(0, 0)).toBe(false);
|
|
89
|
+
expect(cs.contains(3, 3)).toBe(false);
|
|
90
|
+
expect(cs.contains(12, 12)).toBe(false);
|
|
91
|
+
expect(cs.contains(50, 50)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it("zero-width rect rejects everything", () => {
|
|
94
|
+
const cs = new ClipStack();
|
|
95
|
+
cs.push({ x: 5, y: 5, width: 0, height: 10 });
|
|
96
|
+
expect(cs.contains(5, 5)).toBe(false);
|
|
97
|
+
expect(cs.contains(5, 10)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
it("current() returns null when empty, rect when pushed", () => {
|
|
100
|
+
const cs = new ClipStack();
|
|
101
|
+
expect(cs.current()).toBeNull();
|
|
102
|
+
const rect = { x: 1, y: 2, width: 3, height: 4 };
|
|
103
|
+
cs.push(rect);
|
|
104
|
+
expect(cs.current()).toEqual(rect);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
108
|
+
// DrawingContext
|
|
109
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
110
|
+
describe("DrawingContext", () => {
|
|
111
|
+
// ── drawChar ───────────────────────────────────────────────────────
|
|
112
|
+
describe("drawChar", () => {
|
|
113
|
+
it("writes character to buffer at correct position", () => {
|
|
114
|
+
const buf = new PixelBuffer(10, 10);
|
|
115
|
+
const ctx = new DrawingContext(buf);
|
|
116
|
+
ctx.drawChar(3, 4, "A", { fg: WHITE });
|
|
117
|
+
expect(charAt(buf, 3, 4)).toBe("A");
|
|
118
|
+
// Adjacent cell should still be default space
|
|
119
|
+
expect(charAt(buf, 2, 4)).toBe(" ");
|
|
120
|
+
});
|
|
121
|
+
it("writes wide character with continuation in next cell", () => {
|
|
122
|
+
const buf = new PixelBuffer(10, 10);
|
|
123
|
+
const ctx = new DrawingContext(buf);
|
|
124
|
+
// CJK character U+4E16 (world) is width 2
|
|
125
|
+
ctx.drawChar(2, 1, "\u4E16", { fg: WHITE });
|
|
126
|
+
expect(charAt(buf, 2, 1)).toBe("\u4E16");
|
|
127
|
+
expect(buf.get(2, 1).foreground.symbol.width).toBe(2);
|
|
128
|
+
// Continuation cell gets empty string
|
|
129
|
+
expect(charAt(buf, 3, 1)).toBe("");
|
|
130
|
+
});
|
|
131
|
+
it("applies foreground color", () => {
|
|
132
|
+
const buf = new PixelBuffer(10, 10);
|
|
133
|
+
const ctx = new DrawingContext(buf);
|
|
134
|
+
ctx.drawChar(0, 0, "X", { fg: RED });
|
|
135
|
+
expect(fgAt(buf, 0, 0)).toEqual(RED);
|
|
136
|
+
});
|
|
137
|
+
it("applies background color", () => {
|
|
138
|
+
const buf = new PixelBuffer(10, 10);
|
|
139
|
+
const ctx = new DrawingContext(buf);
|
|
140
|
+
ctx.drawChar(0, 0, "X", { fg: WHITE, bg: BLUE });
|
|
141
|
+
expect(bgAt(buf, 0, 0)).toEqual(BLUE);
|
|
142
|
+
});
|
|
143
|
+
it("out-of-bounds write is silently ignored", () => {
|
|
144
|
+
const buf = new PixelBuffer(5, 5);
|
|
145
|
+
const ctx = new DrawingContext(buf);
|
|
146
|
+
// Should not throw
|
|
147
|
+
ctx.drawChar(10, 10, "X", { fg: WHITE });
|
|
148
|
+
ctx.drawChar(-1, -1, "Y", { fg: WHITE });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
// ── drawText ──────────────────────────────────────────────────────
|
|
152
|
+
describe("drawText", () => {
|
|
153
|
+
it("writes string advancing by charWidth for each char", () => {
|
|
154
|
+
const buf = new PixelBuffer(20, 5);
|
|
155
|
+
const ctx = new DrawingContext(buf);
|
|
156
|
+
ctx.drawText(1, 0, "Hello", { fg: WHITE });
|
|
157
|
+
expect(charAt(buf, 1, 0)).toBe("H");
|
|
158
|
+
expect(charAt(buf, 2, 0)).toBe("e");
|
|
159
|
+
expect(charAt(buf, 3, 0)).toBe("l");
|
|
160
|
+
expect(charAt(buf, 4, 0)).toBe("l");
|
|
161
|
+
expect(charAt(buf, 5, 0)).toBe("o");
|
|
162
|
+
// Before and after should be untouched
|
|
163
|
+
expect(charAt(buf, 0, 0)).toBe(" ");
|
|
164
|
+
expect(charAt(buf, 6, 0)).toBe(" ");
|
|
165
|
+
});
|
|
166
|
+
it("handles tabs as 4 spaces", () => {
|
|
167
|
+
const buf = new PixelBuffer(20, 5);
|
|
168
|
+
const ctx = new DrawingContext(buf);
|
|
169
|
+
ctx.drawText(0, 0, "A\tB", { fg: WHITE });
|
|
170
|
+
expect(charAt(buf, 0, 0)).toBe("A");
|
|
171
|
+
// Tab = 4 spaces at positions 1,2,3,4
|
|
172
|
+
expect(charAt(buf, 1, 0)).toBe(" ");
|
|
173
|
+
expect(charAt(buf, 2, 0)).toBe(" ");
|
|
174
|
+
expect(charAt(buf, 3, 0)).toBe(" ");
|
|
175
|
+
expect(charAt(buf, 4, 0)).toBe(" ");
|
|
176
|
+
// B at position 5
|
|
177
|
+
expect(charAt(buf, 5, 0)).toBe("B");
|
|
178
|
+
});
|
|
179
|
+
it("handles wide characters in text", () => {
|
|
180
|
+
const buf = new PixelBuffer(20, 5);
|
|
181
|
+
const ctx = new DrawingContext(buf);
|
|
182
|
+
// Mix of ASCII and CJK
|
|
183
|
+
ctx.drawText(0, 0, "A\u4E16B", { fg: WHITE });
|
|
184
|
+
expect(charAt(buf, 0, 0)).toBe("A");
|
|
185
|
+
expect(charAt(buf, 1, 0)).toBe("\u4E16"); // wide char at 1
|
|
186
|
+
expect(charAt(buf, 2, 0)).toBe(""); // continuation
|
|
187
|
+
expect(charAt(buf, 3, 0)).toBe("B"); // B after the 2-wide char
|
|
188
|
+
});
|
|
189
|
+
it("skips control characters", () => {
|
|
190
|
+
const buf = new PixelBuffer(20, 5);
|
|
191
|
+
const ctx = new DrawingContext(buf);
|
|
192
|
+
ctx.drawText(0, 0, "A\x01B", { fg: WHITE });
|
|
193
|
+
expect(charAt(buf, 0, 0)).toBe("A");
|
|
194
|
+
expect(charAt(buf, 1, 0)).toBe("B");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
// ── fillRect ──────────────────────────────────────────────────────
|
|
198
|
+
describe("fillRect", () => {
|
|
199
|
+
it("fills the specified rect with background color", () => {
|
|
200
|
+
const buf = new PixelBuffer(10, 10);
|
|
201
|
+
const ctx = new DrawingContext(buf);
|
|
202
|
+
ctx.fillRect({ x: 2, y: 3, width: 3, height: 2 }, RED);
|
|
203
|
+
// Inside the rect
|
|
204
|
+
expect(bgAt(buf, 2, 3)).toEqual(RED);
|
|
205
|
+
expect(bgAt(buf, 4, 4)).toEqual(RED);
|
|
206
|
+
// Outside
|
|
207
|
+
expect(bgAt(buf, 1, 3).a).toBe(0); // transparent
|
|
208
|
+
expect(bgAt(buf, 5, 3).a).toBe(0);
|
|
209
|
+
expect(bgAt(buf, 2, 2).a).toBe(0);
|
|
210
|
+
expect(bgAt(buf, 2, 5).a).toBe(0);
|
|
211
|
+
});
|
|
212
|
+
it("fills all cells in the rect", () => {
|
|
213
|
+
const buf = new PixelBuffer(5, 5);
|
|
214
|
+
const ctx = new DrawingContext(buf);
|
|
215
|
+
ctx.fillRect({ x: 0, y: 0, width: 5, height: 5 }, GREEN);
|
|
216
|
+
for (let y = 0; y < 5; y++) {
|
|
217
|
+
for (let x = 0; x < 5; x++) {
|
|
218
|
+
expect(bgAt(buf, x, y)).toEqual(GREEN);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
// ── drawBox ───────────────────────────────────────────────────────
|
|
224
|
+
describe("drawBox", () => {
|
|
225
|
+
it("creates box-drawing chars at edges with correct patterns", () => {
|
|
226
|
+
const buf = new PixelBuffer(10, 10);
|
|
227
|
+
const ctx = new DrawingContext(buf);
|
|
228
|
+
ctx.drawBox({ x: 1, y: 1, width: 4, height: 3 });
|
|
229
|
+
// Top-left corner: DOWN | RIGHT
|
|
230
|
+
expect(patternAt(buf, 1, 1)).toBe(DOWN | RIGHT);
|
|
231
|
+
expect(charAt(buf, 1, 1)).toBe(BOX_CHARS[DOWN | RIGHT]);
|
|
232
|
+
// Top-right corner: DOWN | LEFT
|
|
233
|
+
expect(patternAt(buf, 4, 1)).toBe(DOWN | LEFT);
|
|
234
|
+
expect(charAt(buf, 4, 1)).toBe(BOX_CHARS[DOWN | LEFT]);
|
|
235
|
+
// Bottom-left corner: UP | RIGHT
|
|
236
|
+
expect(patternAt(buf, 1, 3)).toBe(UP | RIGHT);
|
|
237
|
+
// Bottom-right corner: UP | LEFT
|
|
238
|
+
expect(patternAt(buf, 4, 3)).toBe(UP | LEFT);
|
|
239
|
+
// Top edge (horizontal): LEFT | RIGHT
|
|
240
|
+
expect(patternAt(buf, 2, 1)).toBe(LEFT | RIGHT);
|
|
241
|
+
expect(patternAt(buf, 3, 1)).toBe(LEFT | RIGHT);
|
|
242
|
+
// Left edge (vertical): UP | DOWN
|
|
243
|
+
expect(patternAt(buf, 1, 2)).toBe(UP | DOWN);
|
|
244
|
+
// Right edge (vertical): UP | DOWN
|
|
245
|
+
expect(patternAt(buf, 4, 2)).toBe(UP | DOWN);
|
|
246
|
+
});
|
|
247
|
+
it("single cell box draws a cross", () => {
|
|
248
|
+
const buf = new PixelBuffer(5, 5);
|
|
249
|
+
const ctx = new DrawingContext(buf);
|
|
250
|
+
ctx.drawBox({ x: 2, y: 2, width: 1, height: 1 });
|
|
251
|
+
expect(patternAt(buf, 2, 2)).toBe(UP | RIGHT | DOWN | LEFT);
|
|
252
|
+
});
|
|
253
|
+
it("single column box draws vertical line", () => {
|
|
254
|
+
const buf = new PixelBuffer(5, 5);
|
|
255
|
+
const ctx = new DrawingContext(buf);
|
|
256
|
+
ctx.drawBox({ x: 1, y: 0, width: 1, height: 3 });
|
|
257
|
+
expect(patternAt(buf, 1, 0)).toBe(DOWN);
|
|
258
|
+
expect(patternAt(buf, 1, 1)).toBe(UP | DOWN);
|
|
259
|
+
expect(patternAt(buf, 1, 2)).toBe(UP);
|
|
260
|
+
});
|
|
261
|
+
it("single row box draws horizontal line", () => {
|
|
262
|
+
const buf = new PixelBuffer(5, 5);
|
|
263
|
+
const ctx = new DrawingContext(buf);
|
|
264
|
+
ctx.drawBox({ x: 0, y: 1, width: 3, height: 1 });
|
|
265
|
+
expect(patternAt(buf, 0, 1)).toBe(RIGHT);
|
|
266
|
+
expect(patternAt(buf, 1, 1)).toBe(LEFT | RIGHT);
|
|
267
|
+
expect(patternAt(buf, 2, 1)).toBe(LEFT);
|
|
268
|
+
});
|
|
269
|
+
it("two adjacent boxes share a junction (T or cross)", () => {
|
|
270
|
+
const buf = new PixelBuffer(10, 10);
|
|
271
|
+
const ctx = new DrawingContext(buf);
|
|
272
|
+
// Two boxes sharing an edge: box1 right edge = box2 left edge
|
|
273
|
+
ctx.drawBox({ x: 0, y: 0, width: 4, height: 3 });
|
|
274
|
+
ctx.drawBox({ x: 3, y: 0, width: 4, height: 3 });
|
|
275
|
+
// Shared top corner (3,0): box1 has DOWN|LEFT, box2 has DOWN|RIGHT -> T junction
|
|
276
|
+
expect(patternAt(buf, 3, 0)).toBe(DOWN | LEFT | RIGHT);
|
|
277
|
+
// Shared middle edge (3,1): both contribute UP|DOWN -> merged vertical
|
|
278
|
+
expect(patternAt(buf, 3, 1)).toBe(UP | DOWN);
|
|
279
|
+
// Shared bottom corner (3,2): box1 has UP|LEFT, box2 has UP|RIGHT -> T junction
|
|
280
|
+
expect(patternAt(buf, 3, 2)).toBe(UP | LEFT | RIGHT);
|
|
281
|
+
});
|
|
282
|
+
it("zero-size box produces no output", () => {
|
|
283
|
+
const buf = new PixelBuffer(5, 5);
|
|
284
|
+
const ctx = new DrawingContext(buf);
|
|
285
|
+
ctx.drawBox({ x: 0, y: 0, width: 0, height: 3 });
|
|
286
|
+
// All cells should remain default
|
|
287
|
+
for (let y = 0; y < 5; y++) {
|
|
288
|
+
for (let x = 0; x < 5; x++) {
|
|
289
|
+
expect(patternAt(buf, x, y)).toBe(0);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
// ── drawHLine / drawVLine ─────────────────────────────────────────
|
|
295
|
+
describe("drawHLine", () => {
|
|
296
|
+
it("draws horizontal line with LEFT|RIGHT pattern", () => {
|
|
297
|
+
const buf = new PixelBuffer(10, 5);
|
|
298
|
+
const ctx = new DrawingContext(buf);
|
|
299
|
+
ctx.drawHLine(2, 1, 5);
|
|
300
|
+
for (let x = 2; x < 7; x++) {
|
|
301
|
+
expect(patternAt(buf, x, 1)).toBe(LEFT | RIGHT);
|
|
302
|
+
}
|
|
303
|
+
// Adjacent cells untouched
|
|
304
|
+
expect(patternAt(buf, 1, 1)).toBe(0);
|
|
305
|
+
expect(patternAt(buf, 7, 1)).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
describe("drawVLine", () => {
|
|
309
|
+
it("draws vertical line with UP|DOWN pattern", () => {
|
|
310
|
+
const buf = new PixelBuffer(5, 10);
|
|
311
|
+
const ctx = new DrawingContext(buf);
|
|
312
|
+
ctx.drawVLine(1, 2, 4);
|
|
313
|
+
for (let y = 2; y < 6; y++) {
|
|
314
|
+
expect(patternAt(buf, 1, y)).toBe(UP | DOWN);
|
|
315
|
+
}
|
|
316
|
+
// Adjacent cells untouched
|
|
317
|
+
expect(patternAt(buf, 1, 1)).toBe(0);
|
|
318
|
+
expect(patternAt(buf, 1, 6)).toBe(0);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
describe("drawHLine + drawVLine crossing produces cross pattern", () => {
|
|
322
|
+
it("crossing lines merge into a cross", () => {
|
|
323
|
+
const buf = new PixelBuffer(10, 10);
|
|
324
|
+
const ctx = new DrawingContext(buf);
|
|
325
|
+
ctx.drawHLine(0, 3, 8);
|
|
326
|
+
ctx.drawVLine(4, 0, 8);
|
|
327
|
+
// Intersection at (4, 3) should be a cross
|
|
328
|
+
expect(patternAt(buf, 4, 3)).toBe(UP | RIGHT | DOWN | LEFT);
|
|
329
|
+
expect(charAt(buf, 4, 3)).toBe(BOX_CHARS[UP | RIGHT | DOWN | LEFT]);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
// ── Clipping ──────────────────────────────────────────────────────
|
|
333
|
+
describe("pushClip + drawText", () => {
|
|
334
|
+
it("text outside clip is not written", () => {
|
|
335
|
+
const buf = new PixelBuffer(20, 5);
|
|
336
|
+
const ctx = new DrawingContext(buf);
|
|
337
|
+
// Clip to columns 2..6 (width 5)
|
|
338
|
+
ctx.pushClip({ x: 2, y: 0, width: 5, height: 5 });
|
|
339
|
+
ctx.drawText(0, 0, "ABCDEFGHIJ", { fg: WHITE });
|
|
340
|
+
ctx.popClip();
|
|
341
|
+
// Positions 0 and 1 are outside clip -> should be untouched
|
|
342
|
+
expect(charAt(buf, 0, 0)).toBe(" ");
|
|
343
|
+
expect(charAt(buf, 1, 0)).toBe(" ");
|
|
344
|
+
// Positions 2-6 are inside clip -> should have chars C-G
|
|
345
|
+
expect(charAt(buf, 2, 0)).toBe("C");
|
|
346
|
+
expect(charAt(buf, 3, 0)).toBe("D");
|
|
347
|
+
expect(charAt(buf, 4, 0)).toBe("E");
|
|
348
|
+
expect(charAt(buf, 5, 0)).toBe("F");
|
|
349
|
+
expect(charAt(buf, 6, 0)).toBe("G");
|
|
350
|
+
// Position 7+ is outside clip
|
|
351
|
+
expect(charAt(buf, 7, 0)).toBe(" ");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
// ── Translate ─────────────────────────────────────────────────────
|
|
355
|
+
describe("pushTranslate", () => {
|
|
356
|
+
it("drawing at (0,0) after pushTranslate(5,3) writes to buffer at (5,3)", () => {
|
|
357
|
+
const buf = new PixelBuffer(20, 20);
|
|
358
|
+
const ctx = new DrawingContext(buf);
|
|
359
|
+
ctx.pushTranslate(5, 3);
|
|
360
|
+
ctx.drawChar(0, 0, "Z", { fg: WHITE });
|
|
361
|
+
ctx.popTranslate();
|
|
362
|
+
expect(charAt(buf, 5, 3)).toBe("Z");
|
|
363
|
+
expect(charAt(buf, 0, 0)).toBe(" "); // original (0,0) untouched
|
|
364
|
+
});
|
|
365
|
+
it("nested translates accumulate", () => {
|
|
366
|
+
const buf = new PixelBuffer(20, 20);
|
|
367
|
+
const ctx = new DrawingContext(buf);
|
|
368
|
+
ctx.pushTranslate(2, 1);
|
|
369
|
+
ctx.pushTranslate(3, 4);
|
|
370
|
+
ctx.drawChar(0, 0, "Q", { fg: WHITE });
|
|
371
|
+
ctx.popTranslate();
|
|
372
|
+
ctx.popTranslate();
|
|
373
|
+
// Total offset: (2+3, 1+4) = (5, 5)
|
|
374
|
+
expect(charAt(buf, 5, 5)).toBe("Q");
|
|
375
|
+
});
|
|
376
|
+
it("popTranslate restores previous offset", () => {
|
|
377
|
+
const buf = new PixelBuffer(20, 20);
|
|
378
|
+
const ctx = new DrawingContext(buf);
|
|
379
|
+
ctx.pushTranslate(5, 5);
|
|
380
|
+
ctx.pushTranslate(2, 2);
|
|
381
|
+
ctx.popTranslate();
|
|
382
|
+
// Back to (5, 5) offset
|
|
383
|
+
ctx.drawChar(0, 0, "R", { fg: WHITE });
|
|
384
|
+
ctx.popTranslate();
|
|
385
|
+
expect(charAt(buf, 5, 5)).toBe("R");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
// ── Translate + Clip ──────────────────────────────────────────────
|
|
389
|
+
describe("pushTranslate + pushClip: both work together", () => {
|
|
390
|
+
it("clip operates in buffer (world) coordinates", () => {
|
|
391
|
+
const buf = new PixelBuffer(20, 20);
|
|
392
|
+
const ctx = new DrawingContext(buf);
|
|
393
|
+
// Translate so local (0,0) maps to buffer (5, 5)
|
|
394
|
+
ctx.pushTranslate(5, 5);
|
|
395
|
+
// Clip in world coords: only buffer (5..9, 5..9) is visible
|
|
396
|
+
ctx.pushClip({ x: 5, y: 5, width: 5, height: 5 });
|
|
397
|
+
// Draw at local (0,0) -> buffer (5,5) -> inside clip -> visible
|
|
398
|
+
ctx.drawChar(0, 0, "A", { fg: WHITE });
|
|
399
|
+
// Draw at local (4,4) -> buffer (9,9) -> inside clip -> visible
|
|
400
|
+
ctx.drawChar(4, 4, "B", { fg: WHITE });
|
|
401
|
+
// Draw at local (5,5) -> buffer (10,10) -> outside clip -> not written
|
|
402
|
+
ctx.drawChar(5, 5, "C", { fg: WHITE });
|
|
403
|
+
ctx.popClip();
|
|
404
|
+
ctx.popTranslate();
|
|
405
|
+
expect(charAt(buf, 5, 5)).toBe("A");
|
|
406
|
+
expect(charAt(buf, 9, 9)).toBe("B");
|
|
407
|
+
expect(charAt(buf, 10, 10)).toBe(" "); // clipped
|
|
408
|
+
});
|
|
409
|
+
it("fillRect respects both translate and clip", () => {
|
|
410
|
+
const buf = new PixelBuffer(20, 20);
|
|
411
|
+
const ctx = new DrawingContext(buf);
|
|
412
|
+
ctx.pushTranslate(3, 3);
|
|
413
|
+
ctx.pushClip({ x: 3, y: 3, width: 4, height: 4 }); // world 3..6
|
|
414
|
+
// Local rect (0,0,10,10) -> world (3,3,10,10), clipped to (3,3,4,4)
|
|
415
|
+
ctx.fillRect({ x: 0, y: 0, width: 10, height: 10 }, RED);
|
|
416
|
+
ctx.popClip();
|
|
417
|
+
ctx.popTranslate();
|
|
418
|
+
// Inside clipped area
|
|
419
|
+
expect(bgAt(buf, 3, 3)).toEqual(RED);
|
|
420
|
+
expect(bgAt(buf, 6, 6)).toEqual(RED);
|
|
421
|
+
// Outside clipped area
|
|
422
|
+
expect(bgAt(buf, 7, 7).a).toBe(0);
|
|
423
|
+
expect(bgAt(buf, 2, 2).a).toBe(0);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|