253 lines
7.0 KiB
TypeScript
253 lines
7.0 KiB
TypeScript
import { Board, Column, Task } from "./rasterizer.ts";
|
|
|
|
export class Renderer {
|
|
public constructor(
|
|
private board: Board,
|
|
) {}
|
|
|
|
public render(): string {
|
|
const layout = new Layouter(this.board);
|
|
return new TextPlotter(this.board, layout).plot();
|
|
}
|
|
}
|
|
|
|
export class TextPlotter {
|
|
private plotStr: string[];
|
|
private width: number;
|
|
|
|
public constructor(
|
|
private board: Board,
|
|
private layout: Layouter,
|
|
) {
|
|
const { w, h } = layout.boardBox();
|
|
this.plotStr = new Array(w * h).fill(" ");
|
|
this.width = w;
|
|
}
|
|
|
|
public plot(): string {
|
|
this.plotBoard();
|
|
const box = this.layout.boardBox();
|
|
let result = "";
|
|
for (let y = 0; y < box.h; ++y) {
|
|
for (let x = 0; x < box.w; ++x) {
|
|
result += this.plotStr[y * this.width + x];
|
|
}
|
|
result += "\n";
|
|
}
|
|
return result.slice(0, result.length - 1);
|
|
}
|
|
|
|
private plotBoard() {
|
|
const box = this.layout.boardBox();
|
|
this.plotBox(box);
|
|
const { x, y } = box;
|
|
this.plotText(this.board.title, Pos(x + 1, y + 1));
|
|
for (const [idx, column] of this.board.columns.entries()) {
|
|
this.plotColumn(column, idx);
|
|
}
|
|
}
|
|
|
|
private plotColumn(column: Column, idx: number) {
|
|
const box = this.layout.columnBox(column);
|
|
this.plotBox(box);
|
|
const { x, y } = box;
|
|
this.plotText("." + idx.toString(), Pos(x + 2, y + 1));
|
|
this.plotText(column.title, Pos(x + 2, y + 2));
|
|
for (const task of column.tasks) {
|
|
this.plotTask(task);
|
|
}
|
|
}
|
|
|
|
private plotTask(task: Task) {
|
|
const box = this.layout.taskBox(task);
|
|
this.plotBox(box);
|
|
const { x, y } = box;
|
|
this.plotText("#" + task.id.toString(), Pos(x + 2, y + 1));
|
|
this.plotText(task.content, Pos(x + 2, y + 2));
|
|
for (const subTask of task.subTasks) {
|
|
this.plotTask(subTask);
|
|
}
|
|
}
|
|
|
|
private plotText(text: string, { x, y }: Pos) {
|
|
for (let i = 0; i < text.length; ++i) {
|
|
this.set(text[i], Pos(x + i, y));
|
|
}
|
|
}
|
|
|
|
private plotBox({ x, y, w, h }: Box) {
|
|
this.set("+", Pos(x, y));
|
|
this.set("+", Pos(x + w - 1, y));
|
|
this.set("+", Pos(x, y + h - 1));
|
|
this.set("+", Pos(x + w - 1, y + h - 1));
|
|
for (let i = 1; i < w - 1; ++i) {
|
|
this.set("-", Pos(x + i, y));
|
|
this.set("-", Pos(x + i, y + h - 1));
|
|
}
|
|
for (let i = 1; i < h - 1; ++i) {
|
|
this.set("|", Pos(x, y + i));
|
|
this.set("|", Pos(x + w - 1, y + i));
|
|
}
|
|
}
|
|
|
|
private set(ch: string, { x, y }: Pos) {
|
|
this.plotStr[y * this.width + x] = ch;
|
|
}
|
|
}
|
|
|
|
type Size = { w: number; h: number };
|
|
|
|
const Size = (w: number, h: number): Size => ({ w, h });
|
|
|
|
type Pos = { x: number; y: number };
|
|
|
|
const Pos = (x: number, y: number): Pos => ({ x, y });
|
|
|
|
type Box = { x: number; y: number; w: number; h: number };
|
|
|
|
const Box = (
|
|
x: number,
|
|
y: number,
|
|
w: number,
|
|
h: number,
|
|
): Box => ({ x, y, w, h });
|
|
|
|
export class Layouter {
|
|
private _boardBox: Box;
|
|
private columnBoxes = new Map<number, Box>();
|
|
private taskBoxes = new Map<number, Box>();
|
|
private sizer: Sizer;
|
|
|
|
public constructor(
|
|
private board: Board,
|
|
) {
|
|
this.sizer = new Sizer(this.board);
|
|
this._boardBox = this.layoutBoard();
|
|
}
|
|
|
|
public boardBox(): Box {
|
|
return this._boardBox;
|
|
}
|
|
|
|
public columnBox(column: Column): Box {
|
|
return this.columnBoxes.get(column.id)!;
|
|
}
|
|
|
|
public taskBox(task: Task): Box {
|
|
return this.taskBoxes.get(task.id)!;
|
|
}
|
|
|
|
private layoutBoard(): Box {
|
|
const size = this.sizer.boardSize();
|
|
this.board.columns
|
|
.reduce(
|
|
({ x }, column) => this.layoutColumn(column, Pos(x + 1, 2)),
|
|
Pos(1, 0),
|
|
);
|
|
return Box(0, 0, size.w, size.h);
|
|
}
|
|
|
|
private layoutColumn(column: Column, { x, y }: Pos): Pos {
|
|
const { w, h } = this.sizer.columnSize(column);
|
|
column.tasks
|
|
.reduce(
|
|
({ y }, task) => this.layoutTask(task, Pos(2 + x, y)),
|
|
Pos(2 + x, 3 + y),
|
|
);
|
|
this.columnBoxes.set(column.id, Box(x, y, w, h));
|
|
return Pos(x + w, y + h);
|
|
}
|
|
|
|
private layoutTask(task: Task, { x, y }: Pos): Pos {
|
|
const { w, h } = this.sizer.taskSize(task);
|
|
task.subTasks
|
|
.reduce(
|
|
({ y }, task) => this.layoutTask(task, Pos(2 + x, y)),
|
|
Pos(2 + x, 3 + y),
|
|
);
|
|
this.taskBoxes.set(task.id, Box(x, y, w, h));
|
|
return Pos(x + w, y + h);
|
|
}
|
|
}
|
|
|
|
class Sizer {
|
|
private _boardSize: Size;
|
|
private columnSizes = new Map<number, Size>();
|
|
private taskSizes = new Map<number, Size>();
|
|
|
|
public constructor(
|
|
private board: Board,
|
|
) {
|
|
this._boardSize = this.sizeBoard();
|
|
}
|
|
|
|
public boardSize(): Size {
|
|
return this._boardSize;
|
|
}
|
|
|
|
public columnSize(column: Column): Size {
|
|
return this.columnSizes.get(column.id)!;
|
|
}
|
|
|
|
public taskSize(task: Task): Size {
|
|
return this.taskSizes.get(task.id)!;
|
|
}
|
|
|
|
private sizeBoard(): Size {
|
|
const size = this.board.columns
|
|
.map((column) => this.sizeColumn(column))
|
|
.reduce(
|
|
(board, column) =>
|
|
Size(
|
|
board.w + column.w + 1,
|
|
Math.max(board.h, 3 + column.h),
|
|
),
|
|
Size(3, 3),
|
|
);
|
|
for (const column of this.board.columns) {
|
|
this.columnSizes.get(column.id)!.h = size.h - 3;
|
|
}
|
|
return Size(Math.max(size.w, 2 + this.board.title.length), size.h);
|
|
}
|
|
|
|
private sizeColumn(column: Column): Size {
|
|
const tasks = column.tasks
|
|
.map((task) => this.sizeTask(task))
|
|
.reduce(
|
|
(acc, task) => Size(Math.max(acc.w, task.w), acc.h + task.h),
|
|
Size(0, 0),
|
|
);
|
|
const size = Size(
|
|
4 + Math.max(column.title.length + 1, tasks.w),
|
|
4 + tasks.h,
|
|
);
|
|
this.columnSizes.set(column.id, size);
|
|
for (const task of column.tasks) {
|
|
this.expandTask(task, size.w - 4);
|
|
}
|
|
return size;
|
|
}
|
|
|
|
private sizeTask(task: Task): Size {
|
|
const subTasks = task.subTasks
|
|
.map((task) => this.sizeTask(task))
|
|
.reduce(
|
|
(acc, task) => Size(Math.max(acc.w, task.w), acc.h + task.h),
|
|
Size(0, 0),
|
|
);
|
|
const size = Size(
|
|
4 + Math.max(task.content.length + 1, subTasks.w),
|
|
4 + subTasks.h,
|
|
);
|
|
this.taskSizes.set(task.id, size);
|
|
return size;
|
|
}
|
|
|
|
private expandTask(task: Task, w: number) {
|
|
this.taskSizes.get(task.id)!.w = w;
|
|
for (const subTask of task.subTasks) {
|
|
this.expandTask(subTask, w - 4);
|
|
}
|
|
}
|
|
}
|