prowm/renderer.ts
2025-02-06 02:21:37 +01:00

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);
}
}
}