domain/converters/learning.converter.ts
domain/converters/learning.converter.ts is a file in Bane's Lab Site. 169 lines of code and 40 definitions.
import {
BLOCK_GAP,
BLOCK_HEAD,
BLOCK_PAD,
CHAR_WIDTH,
LINE_HEIGHT,
NODE_GAP,
PAD_X,
PAD_Y,
SIDE_GAP,
WRAP_WIDTH,
} from "#configuration/constants/learning.constants";
import type { LaidBlock, LaidStop, LearningBlock, LearningModel } from "#types/learning.types";
import { SPACE } from "#configuration/constants/document.constants";
import { linkFor } from "#configuration/icons/home.icons";
interface Sized {
readonly accent: string;
readonly code: string;
readonly height: number;
readonly icon: string;
readonly label: string;
readonly owner: string;
readonly stops: readonly LaidStop[];
readonly width: number;
}
const wrapLabel = function wrapLabel(text: string): readonly string[] {
const rows: string[] = [];
let row = "";
for (const word of text.split(SPACE)) {
if (row.length === 0) {
row = word;
} else if (row.length + word.length + 1 > WRAP_WIDTH) {
rows.push(row);
row = word;
} else {
row = `${row}${SPACE}${word}`;
}
}
if (row.length > 0) {
rows.push(row);
}
return rows;
};
export const widthOf = function widthOf(rows: readonly string[]): number {
return rows.reduce((widest, row) => Math.max(widest, row.length), 0) * CHAR_WIDTH;
};
const sizeBlock = function sizeBlock(block: LearningBlock): Sized {
const measured = block.stops.map((stop) => {
const rows = wrapLabel(stop.label);
return {
cx: 0,
cy: 0,
height: rows.length * LINE_HEIGHT + PAD_Y * 2,
id: stop.id,
path: stop.path,
requires: stop.requires,
rows,
width: widthOf(rows) + PAD_X * 2,
x: 0,
y: 0,
};
});
const widest = measured.reduce((held, stop) => Math.max(held, stop.width), widthOf([block.label]) + PAD_X * 2);
const stops = measured.map((stop) => ({ ...stop, width: widest }));
const stacked = stops.reduce((held, stop) => held + stop.height + NODE_GAP, BLOCK_HEAD + BLOCK_PAD);
const link = linkFor(block.owner);
return {
accent: link.accent,
code: block.code,
height: stacked - NODE_GAP + BLOCK_PAD,
icon: link.icon,
label: block.label,
owner: block.owner,
stops,
width: widest + BLOCK_PAD * 2,
};
};
const cellOf = function cellOf(index: number, side: number): { readonly x: number; readonly y: number } {
let x = 0;
let y = 0;
let rest = index;
for (let step = 1; step < side; step *= 2) {
const rx = 1 & Math.floor(rest / 2);
const ry = 1 & (rest ^ rx);
if (ry === 0) {
if (rx === 1) {
x = step - 1 - x;
y = step - 1 - y;
}
const swap = x;
x = y;
y = swap;
}
x += step * rx;
y += step * ry;
rest = Math.floor(rest / 4);
}
return { x, y };
};
const sideFor = function sideFor(count: number): number {
let side = 2;
while (side * side < count) {
side *= 2;
}
return side;
};
const placeStops = function placeStops(block: Sized, x: number, y: number, flipped: boolean): readonly LaidStop[] {
const order = flipped ? [...block.stops].reverse() : block.stops;
let inner = y + BLOCK_HEAD + BLOCK_PAD;
const placed = order.map((stop) => {
const laid = {
...stop,
cx: x + BLOCK_PAD + stop.width / 2,
cy: inner + stop.height / 2,
x: x + BLOCK_PAD,
y: inner,
};
inner += stop.height + NODE_GAP;
return laid;
});
return flipped ? placed.reverse() : placed;
};
const shiftBlock = function shiftBlock(block: LaidBlock, dx: number, dy: number): LaidBlock {
return {
...block,
cx: block.cx + dx,
cy: block.cy + dy,
stops: block.stops.map((stop) => ({
...stop,
cx: stop.cx + dx,
cy: stop.cy + dy,
x: stop.x + dx,
y: stop.y + dy,
})),
x: block.x + dx,
y: block.y + dy,
};
};
const entryY = function entryY(before: LaidBlock | undefined, block: Sized, x: number, cy: number): number {
if (before === undefined) {
return cy;
}
const aside = x >= before.x + before.width || x + block.width <= before.x;
return aside ? (before.stops.at(-1)?.cy ?? before.cy) : before.cy;
};
export const layoutLearning = function layoutLearning(blocks: readonly LearningBlock[]): LearningModel {
const sized = blocks.map(sizeBlock);
const cell = sized.reduce((held, block) => Math.max(held, block.width, block.height), 0) + SIDE_GAP;
const side = sideFor(sized.length);
const placed: LaidBlock[] = [];
for (const [index, block] of sized.entries()) {
const spot = cellOf(index, side);
const cx = spot.x * cell + cell / 2;
const cy = spot.y * cell + cell / 2;
const x = cx - block.width / 2;
const y = cy - block.height / 2;
const flipped = entryY(placed.at(-1), block, x, cy) > cy;
placed.push({ ...block, cx, cy, stops: placeStops(block, x, y, flipped), x, y });
}
const left = Math.min(...placed.map((block) => block.x));
const top = Math.min(...placed.map((block) => block.y));
const right = Math.max(...placed.map((block) => block.x + block.width));
const bottom = Math.max(...placed.map((block) => block.y + block.height));
return {
blocks: placed.map((block) => shiftBlock(block, BLOCK_GAP - left, BLOCK_GAP - top)),
height: bottom - top + BLOCK_GAP * 2,
width: right - left + BLOCK_GAP * 2,
};
};