# domain/converters/learning.converter.ts

> 169 lines of code and 40 definitions.

Tree: Site tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/tree#file-domain-converters-learning-converter-ts
Source text: https://banes-lab.com/assets/sources/source.14f16a9abe89fd0c3e3f3b4b7e6cc8840ef80cd8670906bd81a634cd60e52c54.generated.txt

## Definitions

- `placeStops` (lexical_declaration, line 114)
- `shiftBlock` (lexical_declaration, line 131)
- `layoutLearning` (lexical_declaration, line 156, exported)
- `Sized` (interface_declaration, line 17)
- `wrapLabel` (lexical_declaration, line 28)
- `row` (lexical_declaration, line 30)
- `widthOf` (lexical_declaration, line 47, exported)
- `sizeBlock` (lexical_declaration, line 51)
- `measured` (lexical_declaration, line 52)
- `rows` (lexical_declaration, line 53)
- `widest` (lexical_declaration, line 67)
- `stops` (lexical_declaration, line 68)
- `stacked` (lexical_declaration, line 69)
- `link` (lexical_declaration, line 70)
- `cellOf` (lexical_declaration, line 83)
- `rest` (lexical_declaration, line 86)
- `step` (lexical_declaration, line 87)
- `rx` (lexical_declaration, line 88)
- `ry` (lexical_declaration, line 89)
- `swap` (lexical_declaration, line 95)
- `sideFor` (lexical_declaration, line 106)
- `order` (lexical_declaration, line 115)
- `inner` (lexical_declaration, line 116)
- `laid` (lexical_declaration, line 118)
- `entryY` (lexical_declaration, line 148)
- `aside` (lexical_declaration, line 152)
- `sized` (lexical_declaration, line 157, exported)
- `cell` (lexical_declaration, line 158, exported)
- `side` (lexical_declaration, line 159, exported)
- `placed` (lexical_declaration, line 160, exported)
- `spot` (lexical_declaration, line 162, exported)
- `cx` (lexical_declaration, line 163, exported)
- `cy` (lexical_declaration, line 164, exported)
- `x` (lexical_declaration, line 165, exported)
- `y` (lexical_declaration, line 166, exported)
- `flipped` (lexical_declaration, line 167, exported)
- `left` (lexical_declaration, line 170, exported)
- `top` (lexical_declaration, line 171, exported)
- `right` (lexical_declaration, line 172, exported)
- `bottom` (lexical_declaration, line 173, exported)

## Source

```typescript
import {
    BLOCK_GAP,
    BLOCK_HEAD,
    BLOCK_PAD,
    CHAR_WIDTH,
    LINE_HEIGHT,
    NODE_GAP,
    PAD_X,
    PAD_Y,
    SIDE_GAP,
    WORD_SPACE,
    WRAP_WIDTH,
} from "#configuration/constants/learning.constants";
import type { LaidBlock, LaidStop, LearningBlock, LearningModel } from "#types/learning.types";
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(WORD_SPACE)) {
        if (row.length === 0) {
            row = word;
        } else if (row.length + word.length + 1 > WRAP_WIDTH) {
            rows.push(row);
            row = word;
        } else {
            row = `${row}${WORD_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,
    };
};
```
