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