mirror of
https://github.com/TextPlace/CoreTextPlace.git
synced 2026-01-15 01:42:34 +00:00
refactor: extract render entrypoint & add cropRender function
This commit is contained in:
parent
d97546f47c
commit
c703b1870a
16 changed files with 630 additions and 181 deletions
|
|
@ -4,5 +4,10 @@
|
||||||
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
|
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
|
||||||
{
|
{
|
||||||
"formatter": "prettier",
|
"formatter": "prettier",
|
||||||
"format_on_save": "on"
|
"format_on_save": "on",
|
||||||
|
"languages": {
|
||||||
|
"TypeScript": {
|
||||||
|
"language_servers": ["vtsls", "!deno"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ import { defineBuildConfig } from "obuild/config";
|
||||||
|
|
||||||
export default defineBuildConfig({
|
export default defineBuildConfig({
|
||||||
entries: ["src/mod.ts"],
|
entries: ["src/mod.ts"],
|
||||||
});
|
}) as ReturnType<typeof defineBuildConfig>;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@textplace/core",
|
"name": "@textplace/core",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"description": "The core logic of TextPlace.",
|
"description": "The core logic of TextPlace.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/TextPlace/CoreTextPlace"
|
"url": "https://github.com/TextPlace/CoreTextPlace"
|
||||||
|
|
@ -12,7 +13,8 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "obuild"
|
"build": "obuild",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|
|
||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
|
@ -13,16 +13,16 @@ importers:
|
||||||
version: '@jsr/std__cli@1.0.24'
|
version: '@jsr/std__cli@1.0.24'
|
||||||
devDependencies:
|
devDependencies:
|
||||||
obuild:
|
obuild:
|
||||||
specifier: ^0.4.8
|
specifier: ^0.4.3
|
||||||
version: 0.4.8(typescript@5.9.3)
|
version: 0.4.8(typescript@5.9.3)
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.7.4
|
specifier: ^3.7.2
|
||||||
version: 3.7.4
|
version: 3.7.4
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.15
|
specifier: ^4.0.14
|
||||||
version: 4.0.15(jiti@2.6.1)
|
version: 4.0.15(jiti@2.6.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
import type { SectionData, SectionPosition } from "../types/section.ts";
|
import type { SectionData, SectionPosition } from "../types/section";
|
||||||
import type {
|
import type { BoardConfig, BoardData, CharacterPosition } from "../types/board";
|
||||||
BoardConfig,
|
import { applyChange, createSection } from "./section";
|
||||||
BoardData,
|
import type { BoardChange } from "../types/change";
|
||||||
CharacterPosition,
|
|
||||||
FullBoard,
|
|
||||||
} from "../types/board.ts";
|
|
||||||
import { applyChange, createSection } from "./section.ts";
|
|
||||||
import type { BoardChange } from "../types/change.ts";
|
|
||||||
|
|
||||||
export function createBoard(config: BoardConfig): BoardData {
|
export function createBoard(config: BoardConfig): BoardData {
|
||||||
return { config, sections: [] };
|
return { config, sections: [] };
|
||||||
|
|
@ -39,15 +34,17 @@ export function getSectionOnBoard(
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
} = { readOnly: false },
|
} = { readOnly: false },
|
||||||
): SectionData {
|
): SectionData {
|
||||||
let section: SectionData;
|
if (!board.sections[sy]) {
|
||||||
if (!board.sections[sy] && !options.readOnly) board.sections[sy] = [];
|
if (options.readOnly) return createSection({ sx, sy }, board.config);
|
||||||
|
board.sections[sy] = [];
|
||||||
if (!board.sections[sy]?.[sx]) {
|
|
||||||
section = createSection({ sx, sy }, board.config);
|
|
||||||
if (!options.readOnly) board.sections[sy][sx] = section;
|
|
||||||
} else {
|
|
||||||
section = board.sections[sy][sx];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const row = board.sections[sy];
|
||||||
|
const existing = row[sx];
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const section = createSection({ sx, sy }, board.config);
|
||||||
|
if (!options.readOnly) row[sx] = section;
|
||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,59 +53,3 @@ export function applyChangeOnBoard(change: BoardChange, board: BoardData) {
|
||||||
const section = getSectionOnBoard(sPos, board);
|
const section = getSectionOnBoard(sPos, board);
|
||||||
applyChange(change, section);
|
applyChange(change, section);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderFullBoard(data: BoardData): FullBoard {
|
|
||||||
const totalLineCount = data.config.sectionHeight * data.config.ySections;
|
|
||||||
const lineLength = data.config.sectionWidth * data.config.xSections;
|
|
||||||
|
|
||||||
const chLines: string[][] = Array(totalLineCount);
|
|
||||||
const colorLines: string[][] = Array(totalLineCount);
|
|
||||||
const bgColorLines: string[][] = Array(totalLineCount);
|
|
||||||
const widthLines: number[][] = Array(totalLineCount);
|
|
||||||
|
|
||||||
for (let y = 0; y < totalLineCount; y++) {
|
|
||||||
const chLine: string[] = [];
|
|
||||||
const colorLine: string[] = [];
|
|
||||||
const bgColorLine: string[] = [];
|
|
||||||
const widthLine: number[] = [];
|
|
||||||
|
|
||||||
let charsToSkip = 0;
|
|
||||||
|
|
||||||
for (let x = 0; x < lineLength; x++) {
|
|
||||||
if (charsToSkip > 0) {
|
|
||||||
charsToSkip--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sPos = locateSection({ x, y }, data.config);
|
|
||||||
const section = getSectionOnBoard(sPos, data, { readOnly: true });
|
|
||||||
const xInSection = x % data.config.sectionWidth;
|
|
||||||
const yInSection = y % data.config.sectionHeight;
|
|
||||||
|
|
||||||
const cCh = section.ch[yInSection][xInSection];
|
|
||||||
const cCo = section.color[yInSection][xInSection];
|
|
||||||
const cBg = section.bgColor[yInSection][xInSection];
|
|
||||||
const cWd = section.width[yInSection][xInSection];
|
|
||||||
|
|
||||||
chLine.push(cCh);
|
|
||||||
colorLine.push(cCo);
|
|
||||||
bgColorLine.push(cBg);
|
|
||||||
widthLine.push(cWd);
|
|
||||||
charsToSkip += cWd - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
chLines[y] = chLine;
|
|
||||||
colorLines[y] = colorLine;
|
|
||||||
bgColorLines[y] = bgColorLine;
|
|
||||||
widthLines[y] = widthLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
w: lineLength,
|
|
||||||
h: totalLineCount,
|
|
||||||
ch: chLines.flat(),
|
|
||||||
color: colorLines.flat(),
|
|
||||||
bg_color: bgColorLines.flat(),
|
|
||||||
width: widthLines.flat(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
127
src/logic/render.ts
Normal file
127
src/logic/render.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import type { BoardData, BoardRegion } from "../types/board";
|
||||||
|
import type { BoardRender } from "../types/render";
|
||||||
|
import { getSectionOnBoard, locateSection } from "./board";
|
||||||
|
|
||||||
|
export function render(data: BoardData): BoardRender {
|
||||||
|
const totalLineCount = data.config.sectionHeight * data.config.ySections;
|
||||||
|
const lineLength = data.config.sectionWidth * data.config.xSections;
|
||||||
|
|
||||||
|
const chLines: string[][] = Array(totalLineCount);
|
||||||
|
const colorLines: string[][] = Array(totalLineCount);
|
||||||
|
const bgColorLines: string[][] = Array(totalLineCount);
|
||||||
|
const widthLines: number[][] = Array(totalLineCount);
|
||||||
|
|
||||||
|
for (let y = 0; y < totalLineCount; y++) {
|
||||||
|
const chLine: string[] = [];
|
||||||
|
const colorLine: string[] = [];
|
||||||
|
const bgColorLine: string[] = [];
|
||||||
|
const widthLine: number[] = [];
|
||||||
|
|
||||||
|
let charsToSkip = 0;
|
||||||
|
|
||||||
|
for (let x = 0; x < lineLength; x++) {
|
||||||
|
if (charsToSkip > 0) {
|
||||||
|
charsToSkip--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sPos = locateSection({ x, y }, data.config);
|
||||||
|
const section = getSectionOnBoard(sPos, data, { readOnly: true });
|
||||||
|
const xInSection = x % data.config.sectionWidth;
|
||||||
|
const yInSection = y % data.config.sectionHeight;
|
||||||
|
|
||||||
|
const cCh = section.ch[yInSection]?.[xInSection] ?? " ";
|
||||||
|
const cCo = section.color[yInSection]?.[xInSection] ?? "";
|
||||||
|
const cBg = section.bgColor[yInSection]?.[xInSection] ?? "";
|
||||||
|
const cWd = section.width[yInSection]?.[xInSection] ?? 1;
|
||||||
|
|
||||||
|
chLine.push(cCh);
|
||||||
|
colorLine.push(cCo);
|
||||||
|
bgColorLine.push(cBg);
|
||||||
|
widthLine.push(cWd);
|
||||||
|
charsToSkip += cWd - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
chLines[y] = chLine;
|
||||||
|
colorLines[y] = colorLine;
|
||||||
|
bgColorLines[y] = bgColorLine;
|
||||||
|
widthLines[y] = widthLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
w: lineLength,
|
||||||
|
h: totalLineCount,
|
||||||
|
ch: chLines.flat(),
|
||||||
|
color: colorLines.flat(),
|
||||||
|
bg_color: bgColorLines.flat(),
|
||||||
|
width: widthLines.flat(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cropRender(
|
||||||
|
render: BoardRender,
|
||||||
|
region: BoardRegion,
|
||||||
|
): BoardRender {
|
||||||
|
const ch: string[] = [];
|
||||||
|
const color: string[] = [];
|
||||||
|
const bg_color: string[] = [];
|
||||||
|
const width: number[] = [];
|
||||||
|
|
||||||
|
const regionEndX = region.x + region.width;
|
||||||
|
const regionEndY = region.y + region.height;
|
||||||
|
|
||||||
|
let srcIdx = 0;
|
||||||
|
let displayX = 0;
|
||||||
|
let displayY = 0;
|
||||||
|
|
||||||
|
while (srcIdx < render.ch.length) {
|
||||||
|
const cCh = render.ch[srcIdx];
|
||||||
|
const cCo = render.color[srcIdx];
|
||||||
|
const cBg = render.bg_color[srcIdx];
|
||||||
|
const cWd = render.width[srcIdx];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof cCh !== "string" ||
|
||||||
|
typeof cCo !== "string" ||
|
||||||
|
typeof cBg !== "string" ||
|
||||||
|
typeof cWd !== "number"
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid render data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const charEndX = displayX + cWd;
|
||||||
|
|
||||||
|
if (displayY >= region.y && displayY < regionEndY) {
|
||||||
|
// Check if this character overlaps with the crop region horizontally
|
||||||
|
if (charEndX > region.x && displayX < regionEndX) {
|
||||||
|
// Clamp the width to fit within the crop region
|
||||||
|
const clampedStartX = Math.max(displayX, region.x);
|
||||||
|
const clampedEndX = Math.min(charEndX, regionEndX);
|
||||||
|
const clampedWidth = clampedEndX - clampedStartX;
|
||||||
|
|
||||||
|
ch.push(cCh);
|
||||||
|
color.push(cCo);
|
||||||
|
bg_color.push(cBg);
|
||||||
|
width.push(clampedWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance display position
|
||||||
|
displayX += cWd;
|
||||||
|
if (displayX >= render.w) {
|
||||||
|
displayX = 0;
|
||||||
|
displayY++;
|
||||||
|
}
|
||||||
|
|
||||||
|
srcIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
w: region.width,
|
||||||
|
h: region.height,
|
||||||
|
ch,
|
||||||
|
color,
|
||||||
|
bg_color,
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getCharacterWidth } from "../mod.ts";
|
import { getCharacterWidth } from "../mod";
|
||||||
import type { BoardConfig } from "../types/board.ts";
|
import type { BoardConfig } from "../types/board";
|
||||||
import type { BoardChange } from "../types/change.ts";
|
import type { BoardChange } from "../types/change";
|
||||||
import type { SectionData, SectionPosition } from "../types/section.ts";
|
import type { SectionData, SectionPosition } from "../types/section";
|
||||||
|
|
||||||
export function createSection(
|
export function createSection(
|
||||||
{ sx, sy }: SectionPosition,
|
{ sx, sy }: SectionPosition,
|
||||||
|
|
@ -16,18 +16,20 @@ export function createSection(
|
||||||
const offsetX = sx * boardConfig.sectionWidth;
|
const offsetX = sx * boardConfig.sectionWidth;
|
||||||
const offsetY = sy * boardConfig.sectionHeight;
|
const offsetY = sy * boardConfig.sectionHeight;
|
||||||
|
|
||||||
const ch: string[][] = Array(boardConfig.sectionHeight).fill([]).map(() =>
|
const ch: string[][] = Array(boardConfig.sectionHeight)
|
||||||
Array(boardConfig.sectionWidth).fill(boardConfig.defaultCh)
|
.fill([])
|
||||||
);
|
.map(() => Array(boardConfig.sectionWidth).fill(boardConfig.defaultCh));
|
||||||
const color: string[][] = Array(boardConfig.sectionHeight).fill([]).map(() =>
|
const color: string[][] = Array(boardConfig.sectionHeight)
|
||||||
Array(boardConfig.sectionWidth).fill(boardConfig.defaultColor)
|
.fill([])
|
||||||
);
|
.map(() => Array(boardConfig.sectionWidth).fill(boardConfig.defaultColor));
|
||||||
const bgColor: string[][] = Array(boardConfig.sectionHeight).fill([]).map(
|
const bgColor: string[][] = Array(boardConfig.sectionHeight)
|
||||||
() => Array(boardConfig.sectionWidth).fill(boardConfig.defaultBgColor),
|
.fill([])
|
||||||
);
|
.map(() =>
|
||||||
const width: number[][] = Array(boardConfig.sectionHeight).fill([]).map(() =>
|
Array(boardConfig.sectionWidth).fill(boardConfig.defaultBgColor),
|
||||||
Array(boardConfig.sectionWidth).fill(boardConfig.defaultWidth)
|
);
|
||||||
);
|
const width: number[][] = Array(boardConfig.sectionHeight)
|
||||||
|
.fill([])
|
||||||
|
.map(() => Array(boardConfig.sectionWidth).fill(boardConfig.defaultWidth));
|
||||||
|
|
||||||
return { offsetX, offsetY, ch, color, bgColor, width };
|
return { offsetX, offsetY, ch, color, bgColor, width };
|
||||||
}
|
}
|
||||||
|
|
@ -36,9 +38,23 @@ export function applyChange(change: BoardChange, section: SectionData) {
|
||||||
const xInSection = change.x - section.offsetX;
|
const xInSection = change.x - section.offsetX;
|
||||||
const yInSection = change.y - section.offsetY;
|
const yInSection = change.y - section.offsetY;
|
||||||
|
|
||||||
const validX = xInSection >= 0 && xInSection < section.ch[0].length;
|
const row0 = section.ch[0];
|
||||||
|
const validX =
|
||||||
|
xInSection >= 0 && row0 !== undefined && xInSection < row0.length;
|
||||||
const validY = yInSection >= 0 && yInSection < section.ch.length;
|
const validY = yInSection >= 0 && yInSection < section.ch.length;
|
||||||
if (!validX || !validY) {
|
|
||||||
|
const chRow = section.ch[yInSection];
|
||||||
|
const widthRow = section.width[yInSection];
|
||||||
|
const colorRow = section.color[yInSection];
|
||||||
|
const bgColorRow = section.bgColor[yInSection];
|
||||||
|
|
||||||
|
const hasRowsForY =
|
||||||
|
chRow !== undefined &&
|
||||||
|
widthRow !== undefined &&
|
||||||
|
colorRow !== undefined &&
|
||||||
|
bgColorRow !== undefined;
|
||||||
|
|
||||||
|
if (!validX || !validY || !hasRowsForY) {
|
||||||
throw new Error("Change does not belong to this section");
|
throw new Error("Change does not belong to this section");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,13 +62,13 @@ export function applyChange(change: BoardChange, section: SectionData) {
|
||||||
const chWidth = getCharacterWidth(change.ch);
|
const chWidth = getCharacterWidth(change.ch);
|
||||||
const xCharacterOffset = xInSection % chWidth;
|
const xCharacterOffset = xInSection % chWidth;
|
||||||
const offsetAdjustedXInSection = xInSection - xCharacterOffset;
|
const offsetAdjustedXInSection = xInSection - xCharacterOffset;
|
||||||
section.ch[yInSection][offsetAdjustedXInSection] = change.ch;
|
chRow[offsetAdjustedXInSection] = change.ch;
|
||||||
section.width[yInSection][offsetAdjustedXInSection] = chWidth;
|
widthRow[offsetAdjustedXInSection] = chWidth;
|
||||||
}
|
}
|
||||||
if (change.color) {
|
if (change.color) {
|
||||||
section.color[yInSection][xInSection] = change.color;
|
colorRow[xInSection] = change.color;
|
||||||
}
|
}
|
||||||
if (change.bg_color) {
|
if (change.bg_color) {
|
||||||
section.bgColor[yInSection][xInSection] = change.bg_color;
|
bgColorRow[xInSection] = change.bg_color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
src/mod.ts
12
src/mod.ts
|
|
@ -1,7 +1,7 @@
|
||||||
export * from "./types/board.ts";
|
export * from "./types/board";
|
||||||
export * from "./types/section.ts";
|
export * from "./types/section";
|
||||||
export * from "./types/change.ts";
|
export * from "./types/change";
|
||||||
|
|
||||||
export * from "./logic/board.ts";
|
export * from "./logic/board";
|
||||||
export * from "./logic/section.ts";
|
export * from "./logic/section";
|
||||||
export * from "./logic/character.ts";
|
export * from "./logic/character";
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,5 @@
|
||||||
import type { SectionData } from "./section.ts";
|
import type { SectionData } from "./section.ts";
|
||||||
|
|
||||||
/**
|
|
||||||
* A compact form to represent the whole game board.
|
|
||||||
*
|
|
||||||
* Note that this form is not designed for manipulation. It's designed for transmission and rendering, and can not be converted back to `BoardData` as all "over-shadowed" characters are removed.
|
|
||||||
*/
|
|
||||||
export interface FullBoard {
|
|
||||||
/** The total width of the board, in display characters (`ch`). */
|
|
||||||
w: number;
|
|
||||||
/** The total height of the board, in `ch`. */
|
|
||||||
h: number;
|
|
||||||
|
|
||||||
/** Compact array of characters on board. */
|
|
||||||
ch: string[];
|
|
||||||
/** Compact array of color, for each character. */
|
|
||||||
color: string[];
|
|
||||||
/** Compact array of background color, for each character. */
|
|
||||||
bg_color: string[];
|
|
||||||
/** Compact array of width indicator for each character. */
|
|
||||||
width: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A structure defining a character position on board.
|
* A structure defining a character position on board.
|
||||||
*
|
*
|
||||||
|
|
@ -33,6 +12,14 @@ export interface CharacterPosition {
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A structure defining a region on board.
|
||||||
|
*/
|
||||||
|
export interface BoardRegion extends CharacterPosition {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BoardConfig {
|
export interface BoardConfig {
|
||||||
xSections: number;
|
xSections: number;
|
||||||
ySections: number;
|
ySections: number;
|
||||||
|
|
|
||||||
20
src/types/render.ts
Normal file
20
src/types/render.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* A compact form to represent a render of a board or a part of.
|
||||||
|
*
|
||||||
|
* Note that this form is not designed for manipulation. It's designed for transmission and rendering, and can not be converted back to `BoardData` as all "over-shadowed" characters are removed.
|
||||||
|
*/
|
||||||
|
export interface BoardRender {
|
||||||
|
/** The total width of the render, in display characters (`ch`). */
|
||||||
|
w: number;
|
||||||
|
/** The total height of the render, in `ch`. */
|
||||||
|
h: number;
|
||||||
|
|
||||||
|
/** Compact array of characters on board. */
|
||||||
|
ch: string[];
|
||||||
|
/** Compact array of color, for each character. */
|
||||||
|
color: string[];
|
||||||
|
/** Compact array of background color, for each character. */
|
||||||
|
bg_color: string[];
|
||||||
|
/** Compact array of width indicator for each character. */
|
||||||
|
width: number[];
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
import {
|
import { createBoard, getSectionOnBoard } from "../src/logic/board";
|
||||||
createBoard,
|
import type { BoardData } from "../src/types/board";
|
||||||
getSectionOnBoard,
|
import { locateSection } from "../src/logic/board";
|
||||||
renderFullBoard,
|
import { applyChangeOnBoard } from "../src/logic/board";
|
||||||
} from "../src/logic/board.ts";
|
|
||||||
import type { BoardData } from "../src/types/board.ts";
|
|
||||||
import { checkFullBoard } from "./checkFullBoard.ts";
|
|
||||||
import { locateSection } from "../src/logic/board.ts";
|
|
||||||
import { applyChangeOnBoard } from "../src/logic/board.ts";
|
|
||||||
|
|
||||||
describe("board", () => {
|
describe("board", () => {
|
||||||
let board: BoardData | undefined;
|
let board: BoardData | undefined;
|
||||||
|
|
@ -50,23 +45,23 @@ describe("board", () => {
|
||||||
applyChangeOnBoard({ x: 4, y: 3, ch: "D" }, board!);
|
applyChangeOnBoard({ x: 4, y: 3, ch: "D" }, board!);
|
||||||
applyChangeOnBoard({ x: 5, y: 3, ch: "E" }, board!);
|
applyChangeOnBoard({ x: 5, y: 3, ch: "E" }, board!);
|
||||||
|
|
||||||
expect(board!.sections[0][0].ch[0][0]).toBe("A");
|
expect(board!.sections[0]![0]!.ch[0]![0]).toBe("A");
|
||||||
expect(board!.sections[0][1].ch[0][0]).toBe("B");
|
expect(board!.sections[0]![1]!.ch[0]![0]).toBe("B");
|
||||||
expect(board!.sections[1][0].ch[0][0]).toBe("C");
|
expect(board!.sections[1]![0]!.ch[0]![0]).toBe("C");
|
||||||
expect(board!.sections[1][1].ch[0]).toEqual(["D", "E", " ", " "]);
|
expect(board!.sections[1]![1]!.ch[0]).toEqual(["D", "E", " ", " "]);
|
||||||
|
|
||||||
applyChangeOnBoard({ x: 0, y: 1, ch: "你" }, board!);
|
applyChangeOnBoard({ x: 0, y: 1, ch: "你" }, board!);
|
||||||
applyChangeOnBoard({ x: 4, y: 2, ch: "好" }, board!);
|
applyChangeOnBoard({ x: 4, y: 2, ch: "好" }, board!);
|
||||||
applyChangeOnBoard({ x: 0, y: 4, ch: "嗎" }, board!);
|
applyChangeOnBoard({ x: 0, y: 4, ch: "嗎" }, board!);
|
||||||
applyChangeOnBoard({ x: 4, y: 4, ch: "嘛" }, board!);
|
applyChangeOnBoard({ x: 4, y: 4, ch: "嘛" }, board!);
|
||||||
|
|
||||||
expect(board!.sections[0][0].ch[1][0]).toBe("你");
|
expect(board!.sections[0]![0]!.ch[1]![0]).toBe("你");
|
||||||
expect(board!.sections[0][1].ch[2][0]).toBe("好");
|
expect(board!.sections[0]![1]!.ch[2]![0]).toBe("好");
|
||||||
expect(board!.sections[1][0].ch[1][0]).toBe("嗎");
|
expect(board!.sections[1]![0]!.ch[1]![0]).toBe("嗎");
|
||||||
expect(board!.sections[1][1].ch[1]).toEqual(["嘛", " ", " ", " "]);
|
expect(board!.sections[1]![1]!.ch[1]).toEqual(["嘛", " ", " ", " "]);
|
||||||
|
|
||||||
applyChangeOnBoard({ x: 5, y: 4, ch: "啊" }, board!);
|
applyChangeOnBoard({ x: 5, y: 4, ch: "啊" }, board!);
|
||||||
expect(board!.sections[1][1].ch[1]).toEqual(["啊", " ", " ", " "]);
|
expect(board!.sections[1]![1]!.ch[1]).toEqual(["啊", " ", " ", " "]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getSectionOnBoard: existing section", () => {
|
it("getSectionOnBoard: existing section", () => {
|
||||||
|
|
@ -76,8 +71,8 @@ describe("board", () => {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
});
|
});
|
||||||
expect(section.ch[0]).toEqual(["D", "E", " ", " "]);
|
expect(section.ch[0]).toEqual(["D", "E", " ", " "]);
|
||||||
expect(section.color[0][0]).toBe("F");
|
expect(section.color[0]![0]).toBe("F");
|
||||||
expect(section.bgColor[0][0]).toBe("0");
|
expect(section.bgColor[0]![0]).toBe("0");
|
||||||
expect(section.width[0]).toEqual([1, 1, 1, 1]);
|
expect(section.width[0]).toEqual([1, 1, 1, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -87,10 +82,10 @@ describe("board", () => {
|
||||||
const section = getSectionOnBoard({ sx: 1, sy: 2 }, board!, {
|
const section = getSectionOnBoard({ sx: 1, sy: 2 }, board!, {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
});
|
});
|
||||||
expect(section.ch[0][0]).toBe(" ");
|
expect(section.ch[0]![0]).toBe(" ");
|
||||||
expect(section.color[0][0]).toBe("F");
|
expect(section.color[0]![0]).toBe("F");
|
||||||
expect(section.bgColor[0][0]).toBe("0");
|
expect(section.bgColor[0]![0]).toBe("0");
|
||||||
expect(section.width[0][0]).toBe(1);
|
expect(section.width[0]![0]).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getSectionOnBoard: non-existing section", () => {
|
it("getSectionOnBoard: non-existing section", () => {
|
||||||
|
|
@ -99,23 +94,16 @@ describe("board", () => {
|
||||||
const section = getSectionOnBoard({ sx: 2, sy: 1 }, board!, {
|
const section = getSectionOnBoard({ sx: 2, sy: 1 }, board!, {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
});
|
});
|
||||||
expect(section.ch[0][0]).toBe(" ");
|
expect(section.ch[0]![0]).toBe(" ");
|
||||||
expect(section.color[0][0]).toBe("F");
|
expect(section.color[0]![0]).toBe("F");
|
||||||
expect(section.bgColor[0][0]).toBe("0");
|
expect(section.bgColor[0]![0]).toBe("0");
|
||||||
expect(section.width[0][0]).toBe(1);
|
expect(section.width[0]![0]).toBe(1);
|
||||||
});
|
|
||||||
|
|
||||||
it("renderFullBoard", () => {
|
|
||||||
expect(board).toBeDefined();
|
|
||||||
|
|
||||||
const rendered = renderFullBoard(board!);
|
|
||||||
checkFullBoard(rendered);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("on-demand creation: only changed sections are saved", () => {
|
it("on-demand creation: only changed sections are saved", () => {
|
||||||
expect(board).toBeDefined();
|
expect(board).toBeDefined();
|
||||||
|
|
||||||
expect(board!.sections[2]).toBeUndefined();
|
expect(board!.sections[2]).toBeUndefined();
|
||||||
expect(board!.sections[0][2]).toBeUndefined();
|
expect(board!.sections[0]![2]).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { it, expect } from "vitest";
|
import { it, expect } from "vitest";
|
||||||
|
|
||||||
import { getCharacterWidth } from "../src/mod.ts";
|
import { getCharacterWidth } from "../src/mod";
|
||||||
|
|
||||||
it("getCharacterWidth ASCII", () => {
|
it("getCharacterWidth ASCII", () => {
|
||||||
expect(getCharacterWidth("a")).toBe(1);
|
expect(getCharacterWidth("a")).toBe(1);
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,42 @@
|
||||||
import { getCharacterWidth } from "../src/logic/character.ts";
|
import { getCharacterWidth } from "../src/logic/character";
|
||||||
import type { FullBoard } from "../src/types/board.ts";
|
import type { BoardRender } from "../src/types/render";
|
||||||
|
|
||||||
function isCorrectWidth(cWd: number, cCh: string): boolean {
|
|
||||||
return getCharacterWidth(cCh) === cWd;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidColor(color: string): boolean {
|
function isValidColor(color: string): boolean {
|
||||||
return /^[0-9A-F]$/.test(color);
|
return /^[0-9A-F]$/.test(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkFullBoard(board: FullBoard) {
|
interface Options {
|
||||||
|
/**
|
||||||
|
* Whether to allow the width of a character to be narrower than the "correct" width.
|
||||||
|
*
|
||||||
|
* For partial renders, some wide characters may be clipped to a narrower width. This option allows for that.
|
||||||
|
*/
|
||||||
|
allowsNarrowerWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkBoardRender(render: BoardRender, options?: Options) {
|
||||||
let chLine = "";
|
let chLine = "";
|
||||||
let colorLine = "";
|
let colorLine = "";
|
||||||
let bgColorLine = "";
|
let bgColorLine = "";
|
||||||
let widthLine = "";
|
let widthLine = "";
|
||||||
let lines = 0;
|
let lines = 0;
|
||||||
const ch = [...board.ch];
|
const ch = [...render.ch];
|
||||||
const chLength = ch.length;
|
const chLength = ch.length;
|
||||||
|
|
||||||
let unsafeCurrentOffset = 0;
|
let unsafeCurrentOffset = 0;
|
||||||
|
|
||||||
|
function isCorrectWidth(cWd: number, cCh: string): boolean {
|
||||||
|
const correctWidth = getCharacterWidth(cCh);
|
||||||
|
return options?.allowsNarrowerWidth
|
||||||
|
? cWd <= correctWidth && cWd > 0
|
||||||
|
: cWd === correctWidth;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < chLength; i++) {
|
for (let i = 0; i < chLength; i++) {
|
||||||
const cCh = ch[i];
|
const cCh = ch[i];
|
||||||
const cCo = board.color[i];
|
const cCo = render.color[i];
|
||||||
const cBg = board.bg_color[i];
|
const cBg = render.bg_color[i];
|
||||||
const cWd = board.width[i];
|
const cWd = render.width[i];
|
||||||
|
|
||||||
const printSituation = () => {
|
const printSituation = () => {
|
||||||
console.error(
|
console.error(
|
||||||
|
|
@ -68,7 +80,7 @@ export function checkFullBoard(board: FullBoard) {
|
||||||
widthLine += String(cWd).padEnd(cWd);
|
widthLine += String(cWd).padEnd(cWd);
|
||||||
unsafeCurrentOffset += cCh.length;
|
unsafeCurrentOffset += cCh.length;
|
||||||
|
|
||||||
if (colorLine.length === board.w) {
|
if (colorLine.length === render.w) {
|
||||||
lines++;
|
lines++;
|
||||||
chLine = "";
|
chLine = "";
|
||||||
colorLine = "";
|
colorLine = "";
|
||||||
|
|
@ -77,5 +89,5 @@ export function checkFullBoard(board: FullBoard) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lines !== board.h) throw new Error("board height error");
|
if (lines !== render.h) throw new Error("board height error");
|
||||||
}
|
}
|
||||||
307
tests/render.test.ts
Normal file
307
tests/render.test.ts
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { render, cropRender } from "../src/logic/render";
|
||||||
|
import { checkBoardRender } from "./checkBoardRender";
|
||||||
|
import { applyChangeOnBoard, createBoard } from "../src/logic/board";
|
||||||
|
import type { BoardData } from "../src/types/board";
|
||||||
|
import type { BoardRender } from "../src/types/render";
|
||||||
|
|
||||||
|
const board: BoardData = createBoard({
|
||||||
|
xSections: 1,
|
||||||
|
ySections: 1,
|
||||||
|
sectionWidth: 10,
|
||||||
|
sectionHeight: 5,
|
||||||
|
defaultCh: " ",
|
||||||
|
defaultColor: "F",
|
||||||
|
defaultBgColor: "0",
|
||||||
|
defaultWidth: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add CJK characters (width 2)
|
||||||
|
applyChangeOnBoard({ x: 0, y: 0, ch: "你" }, board);
|
||||||
|
applyChangeOnBoard({ x: 2, y: 0, ch: "好" }, board);
|
||||||
|
applyChangeOnBoard({ x: 4, y: 0, ch: "世" }, board);
|
||||||
|
applyChangeOnBoard({ x: 6, y: 0, ch: "界" }, board);
|
||||||
|
|
||||||
|
// Add cell with non-default foreground color
|
||||||
|
applyChangeOnBoard({ x: 0, y: 1, ch: "A" }, board);
|
||||||
|
applyChangeOnBoard({ x: 0, y: 1, color: "C" }, board);
|
||||||
|
|
||||||
|
// Add cell with non-default background color
|
||||||
|
applyChangeOnBoard({ x: 2, y: 1, ch: "B" }, board);
|
||||||
|
applyChangeOnBoard({ x: 2, y: 1, bg_color: "A" }, board);
|
||||||
|
|
||||||
|
// Add cell with both non-default foreground and background colors
|
||||||
|
applyChangeOnBoard({ x: 4, y: 1, ch: "C" }, board);
|
||||||
|
applyChangeOnBoard({ x: 4, y: 1, color: "9" }, board);
|
||||||
|
applyChangeOnBoard({ x: 4, y: 1, bg_color: "E" }, board);
|
||||||
|
|
||||||
|
// Add CJK character with custom colors
|
||||||
|
applyChangeOnBoard({ x: 0, y: 2, ch: "中" }, board);
|
||||||
|
applyChangeOnBoard({ x: 0, y: 2, color: "D" }, board);
|
||||||
|
applyChangeOnBoard({ x: 0, y: 2, bg_color: "B" }, board);
|
||||||
|
|
||||||
|
describe("render", () => {
|
||||||
|
it("render", () => {
|
||||||
|
expect(board).toBeDefined();
|
||||||
|
|
||||||
|
const rendered = render(board!);
|
||||||
|
checkBoardRender(rendered);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cropRender", () => {
|
||||||
|
// Helper to create a simple render for testing
|
||||||
|
function createTestRender(): BoardRender {
|
||||||
|
// Create a 10x5 board with:
|
||||||
|
// Row 0: "你好世界 " (4 CJK chars = 8 display cols + 2 spaces)
|
||||||
|
// Row 1: "A B C " (ASCII with spaces)
|
||||||
|
// Row 2: "中 " (1 CJK char + 8 spaces)
|
||||||
|
// Row 3: " " (all spaces)
|
||||||
|
// Row 4: " " (all spaces)
|
||||||
|
return render(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("basic cropping with width-1 characters", () => {
|
||||||
|
it("crops a region containing only width-1 characters", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop row 1 (ASCII chars), columns 0-5
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 1, width: 5, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(5);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("crops middle section of width-1 characters", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop row 1, columns 1-4 (should get " B C")
|
||||||
|
const cropped = cropRender(rendered, { x: 1, y: 1, width: 4, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(4);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("wide characters (width 2)", () => {
|
||||||
|
it("includes wide character entirely within crop region", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop row 0, columns 0-4 (should include "你好")
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 0, width: 4, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(4);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
expect(cropped.ch).toContain("你");
|
||||||
|
expect(cropped.ch).toContain("好");
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles wide character at crop start boundary (character starts before region)", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop starting at x=1 - "你" starts at x=0 and extends to x=2
|
||||||
|
// The crop should include "你" with clamped width of 1
|
||||||
|
const cropped = cropRender(rendered, { x: 1, y: 0, width: 3, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(3);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
// "你" should be included but with width clamped to 1 (only 1 col visible)
|
||||||
|
expect(cropped.ch).toContain("你");
|
||||||
|
checkBoardRender(cropped, { allowsNarrowerWidth: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles wide character at crop end boundary (character extends beyond region)", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop ending at x=3 - "好" starts at x=2 and extends to x=4
|
||||||
|
// The crop should include "好" with clamped width of 1
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 0, width: 3, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(3);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
// "你" should be included with full width, "好" should be clamped
|
||||||
|
expect(cropped.ch).toContain("你");
|
||||||
|
expect(cropped.ch).toContain("好");
|
||||||
|
checkBoardRender(cropped, { allowsNarrowerWidth: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles wide character with both boundaries clamped", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop from x=1 to x=2 (width 1) - only partial view of "你"
|
||||||
|
const cropped = cropRender(rendered, { x: 1, y: 0, width: 1, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(1);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
checkBoardRender(cropped, { allowsNarrowerWidth: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes wide character entirely outside crop region", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop row 0, columns 8-10 (should only get spaces, no CJK)
|
||||||
|
const cropped = cropRender(rendered, { x: 8, y: 0, width: 2, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(2);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
// Should only contain spaces
|
||||||
|
expect(cropped.ch.every((c) => c === " ")).toBe(true);
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("multi-row cropping", () => {
|
||||||
|
it("crops multiple rows correctly", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop rows 0-2, columns 0-4
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 0, width: 4, height: 3 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(4);
|
||||||
|
expect(cropped.h).toBe(3);
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("crops rows from middle of board", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop rows 1-3, columns 2-6
|
||||||
|
const cropped = cropRender(rendered, { x: 2, y: 1, width: 4, height: 3 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(4);
|
||||||
|
expect(cropped.h).toBe(3);
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles wide characters across multiple rows", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop rows 0 and 2 which both have CJK characters
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 0, width: 4, height: 3 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(4);
|
||||||
|
expect(cropped.h).toBe(3);
|
||||||
|
expect(cropped.ch).toContain("你");
|
||||||
|
expect(cropped.ch).toContain("中");
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("full board crop", () => {
|
||||||
|
it("returns equivalent data when cropping entire board", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
const cropped = cropRender(rendered, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: rendered.w,
|
||||||
|
height: rendered.h,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(rendered.w);
|
||||||
|
expect(cropped.h).toBe(rendered.h);
|
||||||
|
expect(cropped.ch).toEqual(rendered.ch);
|
||||||
|
expect(cropped.color).toEqual(rendered.color);
|
||||||
|
expect(cropped.bg_color).toEqual(rendered.bg_color);
|
||||||
|
expect(cropped.width).toEqual(rendered.width);
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("edge cases", () => {
|
||||||
|
it("handles zero-width region", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 0, width: 0, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(0);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
expect(cropped.ch).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles zero-height region", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 0, width: 5, height: 0 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(5);
|
||||||
|
expect(cropped.h).toBe(0);
|
||||||
|
expect(cropped.ch).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles single cell crop", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 1, width: 1, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(1);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
expect(cropped.ch.length).toBe(1);
|
||||||
|
expect(cropped.ch[0]).toBe("A");
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles crop at bottom-right corner", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
const cropped = cropRender(rendered, { x: 8, y: 4, width: 2, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(2);
|
||||||
|
expect(cropped.h).toBe(1);
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles crop region starting beyond first row", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 3, width: 5, height: 2 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(5);
|
||||||
|
expect(cropped.h).toBe(2);
|
||||||
|
// Should only contain spaces (rows 3-4 are empty)
|
||||||
|
expect(cropped.ch.every((c) => c === " ")).toBe(true);
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("color preservation", () => {
|
||||||
|
it("preserves foreground color when cropping", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop to get cell with custom foreground color (A at 0,1 with color C)
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 1, width: 1, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.color[0]).toBe("C");
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves background color when cropping", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop to get cell with custom background color (B at 2,1 with bg_color A)
|
||||||
|
const cropped = cropRender(rendered, { x: 2, y: 1, width: 1, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.bg_color[0]).toBe("A");
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves both colors on CJK character when cropping", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop to get CJK with custom colors (中 at 0,2 with color D, bg_color B)
|
||||||
|
const cropped = cropRender(rendered, { x: 0, y: 2, width: 2, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.ch[0]).toBe("中");
|
||||||
|
expect(cropped.color[0]).toBe("D");
|
||||||
|
expect(cropped.bg_color[0]).toBe("B");
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("consecutive wide characters", () => {
|
||||||
|
it("handles crop in middle of consecutive wide characters", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Row 0 has: 你(0-1)好(2-3)世(4-5)界(6-7)
|
||||||
|
// Crop from x=2 to x=6 should get 好世
|
||||||
|
const cropped = cropRender(rendered, { x: 2, y: 0, width: 4, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(4);
|
||||||
|
expect(cropped.ch).toContain("好");
|
||||||
|
expect(cropped.ch).toContain("世");
|
||||||
|
checkBoardRender(cropped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles crop splitting multiple wide characters", () => {
|
||||||
|
const rendered = createTestRender();
|
||||||
|
// Crop from x=1 to x=7 - should clip 你 at start and 界 at end
|
||||||
|
const cropped = cropRender(rendered, { x: 1, y: 0, width: 6, height: 1 });
|
||||||
|
|
||||||
|
expect(cropped.w).toBe(6);
|
||||||
|
checkBoardRender(cropped, { allowsNarrowerWidth: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
import { applyChange, createSection } from "../src/logic/section.ts";
|
import { applyChange, createSection } from "../src/logic/section";
|
||||||
import type { SectionData } from "../src/types/section.ts";
|
import type { SectionData } from "../src/types/section";
|
||||||
|
|
||||||
describe("section", () => {
|
describe("section", () => {
|
||||||
let section: SectionData | undefined;
|
let section: SectionData | undefined;
|
||||||
|
|
|
||||||
44
tsconfig.json
Normal file
44
tsconfig.json
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
// Visit https://aka.ms/tsconfig to read more about this file
|
||||||
|
"compilerOptions": {
|
||||||
|
// File Layout
|
||||||
|
// "rootDir": "./src",
|
||||||
|
// "outDir": "./dist",
|
||||||
|
|
||||||
|
// Environment Settings
|
||||||
|
// See also https://aka.ms/tsconfig/module
|
||||||
|
"module": "ES2022",
|
||||||
|
"target": "ES2022",
|
||||||
|
"types": [],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
// For nodejs:
|
||||||
|
// "lib": ["esnext"],
|
||||||
|
// "types": ["node"],
|
||||||
|
// and npm install -D @types/node
|
||||||
|
|
||||||
|
// Other Outputs
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
|
||||||
|
// Stricter Typechecking Options
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
|
||||||
|
// Style Options
|
||||||
|
// "noImplicitReturns": true,
|
||||||
|
// "noImplicitOverride": true,
|
||||||
|
// "noUnusedLocals": true,
|
||||||
|
// "noUnusedParameters": true,
|
||||||
|
// "noFallthroughCasesInSwitch": true,
|
||||||
|
// "noPropertyAccessFromIndexSignature": true,
|
||||||
|
|
||||||
|
// Recommended Options
|
||||||
|
"strict": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue