Refactor/reacrhitecture to fsd+ #49

Merged
ilia merged 70 commits from refactor/reacrhitecture-to-fsd+ into main 2026-06-03 09:55:47 +00:00
Showing only changes of commit 3e568685b3 - Show all commits
@@ -9,43 +9,6 @@ import {
*/
const DEFAULT_RENDER_SIZE_PX = 16;
/**
* Internal shape of pretext's PreparedTextWithSegments — only `segments` is in
* pretext's public TS type; the numeric arrays exist at runtime but are not in
* the published signature. Verified against node_modules/@chenglou/pretext/src/layout.ts.
*/
interface PretextInternals {
/**
* Per-segment text.
*/
segments: string[];
/**
* Per-segment full width in pixels.
*/
widths: number[];
/**
* Per-segment per-grapheme advance widths, or null when the segment is a single grapheme.
*/
breakableFitAdvances: (number[] | null)[];
/**
* Per-segment line-end fit advance.
*/
lineEndFitAdvances: number[];
/**
* Per-segment line-end paint advance.
*/
lineEndPaintAdvances: number[];
}
/**
* Asserts pretext's runtime shape. The public TS type exposes only `segments`;
* the numeric arrays exist at runtime but are absent from the published signature.
* Centralizing the cast keeps the engine body free of `as any`.
*/
function asPretextInternals(prepared: PreparedTextWithSegments): PretextInternals {
return prepared as unknown as PretextInternals;
}
/**
* Per-grapheme data computed during dual-font layout. Internal to the engine;
* consumed by computeLineRenderModel to derive the per-frame render model.
@@ -114,6 +77,10 @@ export interface ComparisonResult {
* of font A and font B. This guarantees that both fonts wrap at exactly the same
* positions, making side-by-side or slider comparison visually coherent.
*
* Relies on pretext's published structural fields on `PreparedTextWithSegments`
* (`widths`, `breakableFitAdvances`, `lineEndFitAdvances`, `lineEndPaintAdvances`)
* which are exposed via the `PreparedCore` intersection in `@chenglou/pretext@0.0.6`.
*
* **Two-level caching strategy**
* 1. Font-change cache (`#preparedA`, `#preparedB`, `#unifiedPrepared`): rebuilt only
* when `text`, `fontA`, or `fontB` changes. `prepareWithSegments` is expensive
@@ -129,9 +96,9 @@ export class DualFontLayout {
#segmenter: Intl.Segmenter;
// Cached prepared data
#preparedA: PretextInternals | null = null;
#preparedB: PretextInternals | null = null;
#unifiedPrepared: PretextInternals | null = null;
#preparedA: PreparedTextWithSegments | null = null;
#preparedB: PreparedTextWithSegments | null = null;
#unifiedPrepared: PreparedTextWithSegments | null = null;
#lastText = '';
#lastFontA = '';
@@ -192,8 +159,8 @@ export class DualFontLayout {
// 1. Prepare (or use cache)
if (isFontChange) {
this.#preparedA = asPretextInternals(prepareWithSegments(text, fontA));
this.#preparedB = asPretextInternals(prepareWithSegments(text, fontB));
this.#preparedA = prepareWithSegments(text, fontA);
this.#preparedB = prepareWithSegments(text, fontB);
this.#unifiedPrepared = this.#createUnifiedPrepared(this.#preparedA, this.#preparedB, spacingPx);
this.#lastText = text;
@@ -207,13 +174,7 @@ export class DualFontLayout {
return { lines: [], totalHeight: 0 };
}
// pretext's `layoutWithLines` is typed against its public surface; pass the
// runtime-internal shape through with one boundary cast.
const { lines, height } = layoutWithLines(
this.#unifiedPrepared as unknown as PreparedTextWithSegments,
width,
lineHeight,
);
const { lines, height } = layoutWithLines(this.#unifiedPrepared, width, lineHeight);
// 3. Map results back to both fonts
const preparedA = this.#preparedA;
@@ -284,11 +245,11 @@ export class DualFontLayout {
* across both fonts, with `spacingPx` added to model letter-spacing.
*/
#createUnifiedPrepared(
a: PretextInternals,
b: PretextInternals,
a: PreparedTextWithSegments,
b: PreparedTextWithSegments,
spacingPx: number = 0,
): PretextInternals {
const unified: PretextInternals = { ...a };
): PreparedTextWithSegments {
const unified: PreparedTextWithSegments = { ...a };
unified.widths = a.widths.map((w, i) => Math.max(w, b.widths[i]) + spacingPx);
unified.lineEndFitAdvances = a.lineEndFitAdvances.map((w, i) =>