# presentation/components/learning.fragment.component.ts

> 134 lines of code and 32 definitions.

Tree: Site tree
Language: typescript
Layer: product
Canonical: https://banes-lab.com/anatomy/tree#file-presentation-components-learning-fragment-component-ts
Source text: https://banes-lab.com/assets/sources/source.43a8dc26f90eb7aa72fe7e39c85bd6340468c42dec7bc4843e53b5f23018624e.generated.txt

## Definitions

- `isRecord` (lexical_declaration, line 21)
- `lightRequired` (lexical_declaration, line 105)
- `measurable` (lexical_declaration, line 41)
- `isGeometry` (lexical_declaration, line 25)
- `isBox` (lexical_declaration, line 33)
- `isDashed` (lexical_declaration, line 37)
- `paceAt` (lexical_declaration, line 96, exported)
- `attachPrerequisites` (lexical_declaration, line 116, exported)
- `framedOf` (lexical_declaration, line 45, exported)
- `show` (lexical_declaration, line 120, exported)
- `hide` (lexical_declaration, line 123, exported)
- `geometry` (lexical_declaration, line 46, exported)
- `style` (lexical_declaration, line 47, exported)
- `viewBox` (lexical_declaration, line 48, exported)
- `box` (lexical_declaration, line 49, exported)
- `samplesOf` (lexical_declaration, line 56)
- `at` (lexical_declaration, line 58)
- `marksOf` (lexical_declaration, line 64, exported)
- `samples` (lexical_declaration, line 69, exported)
- `best` (lexical_declaration, line 71, exported)
- `nearest` (lexical_declaration, line 72, exported)
- `gap` (lexical_declaration, line 74, exported)
- `spansOf` (lexical_declaration, line 84, exported)
- `spans` (lexical_declaration, line 85, exported)
- `index` (lexical_declaration, line 86, exported)
- `before` (lexical_declaration, line 87, exported)
- `here` (lexical_declaration, line 88, exported)
- `span` (lexical_declaration, line 97, exported)
- `widestOf` (lexical_declaration, line 101, exported)
- `nodes` (lexical_declaration, line 117, exported)
- `detachers` (lexical_declaration, line 118, exported)
- `target` (lexical_declaration, line 119, exported)

## Used by

- [presentation/components/learning.component.ts](https://banes-lab.com/source/tree/presentation/components/learning.component.ts.md)
- [presentation/widgets/home.widget.ts](https://banes-lab.com/source/tree/presentation/widgets/home.widget.ts.md)

## Source

```typescript
import {
    FAST_PACE,
    FOCUSED_CLASS,
    HIDE_EVENTS,
    REQUIRED_CLASS,
    SAMPLE_STEP,
    SHOW_EVENTS,
    SLOW_PACE,
} from "#configuration/constants/learning.constants";
import type {
    Framed,
    Geometry,
    LearningMark,
    LearningSample,
    LearningSpan,
    LearningView,
    PlacedStop,
} from "#types/learning.types";
import type { Disposer } from "#types/base.types";

const isRecord = function isRecord(value: unknown): value is Record<string, unknown> {
    return typeof value === "object" && value !== null;
};

const isGeometry = function isGeometry(value: unknown): value is Geometry {
    return (
        isRecord(value) &&
        typeof value["getTotalLength"] === "function" &&
        typeof value["getPointAtLength"] === "function"
    );
};

const isBox = function isBox(value: unknown): boolean {
    return isRecord(value) && typeof value["width"] === "number" && typeof value["height"] === "number";
};

const isDashed = function isDashed(value: unknown): boolean {
    return isRecord(value) && "strokeDasharray" in value;
};

const measurable = function measurable(box: unknown, style: unknown): boolean {
    return isBox(box) && isDashed(style);
};

export const framedOf = function framedOf(view: LearningView): Framed | null {
    const geometry: unknown = view.trace;
    const style: unknown = isRecord(view.trace) ? view.trace["style"] : null;
    const viewBox: unknown = isRecord(view.vector) ? view.vector["viewBox"] : null;
    const box: unknown = isRecord(viewBox) ? viewBox["baseVal"] : null;
    if (!isGeometry(geometry) || !isRecord(box) || !isRecord(style) || !measurable(box, style)) {
        return null;
    }
    return { box, geometry, style };
};

const samplesOf = function samplesOf(geometry: Geometry, total: number): readonly LearningSample[] {
    const samples: LearningSample[] = [];
    for (let at = 0; at <= total; at += SAMPLE_STEP) {
        samples.push({ at, point: geometry.getPointAtLength(at) });
    }
    return samples;
};

export const marksOf = function marksOf(
    geometry: Geometry,
    stops: readonly PlacedStop[],
    total: number,
): LearningMark[] {
    const samples = samplesOf(geometry, total);
    return stops.map((stop) => {
        let best = 0;
        let nearest = Infinity;
        for (const { at, point } of samples) {
            const gap = Math.hypot(point.x - stop.cx, point.y - stop.cy);
            if (gap < nearest) {
                nearest = gap;
                best = at;
            }
        }
        return { at: best, lit: false, stop };
    });
};

export const spansOf = function spansOf(marks: readonly LearningMark[]): LearningSpan[] {
    const spans: LearningSpan[] = [];
    for (let index = 1; index < marks.length; index += 1) {
        const before = marks[index - 1];
        const here = marks[index];
        if (before !== undefined && here !== undefined) {
            spans.push({ fast: before.stop.block !== here.stop.block, from: before.at, to: here.at });
        }
    }
    return spans;
};

export const paceAt = function paceAt(spans: readonly LearningSpan[], at: number): number {
    const span = spans.find((held) => at >= held.from && at < held.to);
    return span?.fast === true ? FAST_PACE : SLOW_PACE;
};

export const widestOf = function widestOf(stops: readonly PlacedStop[]): number {
    return stops.reduce((held, stop) => Math.max(held, stop.width), 0);
};

const lightRequired = function lightRequired(
    stop: PlacedStop,
    nodes: ReadonlyMap<string, Element>,
    shown: boolean,
): void {
    stop.node.classList.toggle(FOCUSED_CLASS, shown);
    for (const id of stop.requires) {
        nodes.get(id)?.classList.toggle(REQUIRED_CLASS, shown);
    }
};

export const attachPrerequisites = function attachPrerequisites(view: LearningView): Disposer {
    const nodes = new Map(view.stops.map((stop) => [stop.id, stop.node]));
    const detachers = view.stops.map((stop): Disposer => {
        const target = stop.node.parentElement ?? stop.node;
        const show = (): void => {
            lightRequired(stop, nodes, true);
        };
        const hide = (): void => {
            lightRequired(stop, nodes, false);
        };
        for (const name of SHOW_EVENTS) {
            target.addEventListener(name, show);
        }
        for (const name of HIDE_EVENTS) {
            target.addEventListener(name, hide);
        }
        return () => {
            for (const name of SHOW_EVENTS) {
                target.removeEventListener(name, show);
            }
            for (const name of HIDE_EVENTS) {
                target.removeEventListener(name, hide);
            }
            hide();
        };
    });
    return () => {
        for (const detach of detachers) {
            detach();
        }
    };
};
```
