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; 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; * Per-grapheme data computed during dual-font layout. Internal to the engine;
* consumed by computeLineRenderModel to derive the per-frame render model. * 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 * 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. * 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** * **Two-level caching strategy**
* 1. Font-change cache (`#preparedA`, `#preparedB`, `#unifiedPrepared`): rebuilt only * 1. Font-change cache (`#preparedA`, `#preparedB`, `#unifiedPrepared`): rebuilt only
* when `text`, `fontA`, or `fontB` changes. `prepareWithSegments` is expensive * when `text`, `fontA`, or `fontB` changes. `prepareWithSegments` is expensive
@@ -129,9 +96,9 @@ export class DualFontLayout {
#segmenter: Intl.Segmenter; #segmenter: Intl.Segmenter;
// Cached prepared data // Cached prepared data
#preparedA: PretextInternals | null = null; #preparedA: PreparedTextWithSegments | null = null;
#preparedB: PretextInternals | null = null; #preparedB: PreparedTextWithSegments | null = null;
#unifiedPrepared: PretextInternals | null = null; #unifiedPrepared: PreparedTextWithSegments | null = null;
#lastText = ''; #lastText = '';
#lastFontA = ''; #lastFontA = '';
@@ -192,8 +159,8 @@ export class DualFontLayout {
// 1. Prepare (or use cache) // 1. Prepare (or use cache)
if (isFontChange) { if (isFontChange) {
this.#preparedA = asPretextInternals(prepareWithSegments(text, fontA)); this.#preparedA = prepareWithSegments(text, fontA);
this.#preparedB = asPretextInternals(prepareWithSegments(text, fontB)); this.#preparedB = prepareWithSegments(text, fontB);
this.#unifiedPrepared = this.#createUnifiedPrepared(this.#preparedA, this.#preparedB, spacingPx); this.#unifiedPrepared = this.#createUnifiedPrepared(this.#preparedA, this.#preparedB, spacingPx);
this.#lastText = text; this.#lastText = text;
@@ -207,13 +174,7 @@ export class DualFontLayout {
return { lines: [], totalHeight: 0 }; return { lines: [], totalHeight: 0 };
} }
// pretext's `layoutWithLines` is typed against its public surface; pass the const { lines, height } = layoutWithLines(this.#unifiedPrepared, width, lineHeight);
// runtime-internal shape through with one boundary cast.
const { lines, height } = layoutWithLines(
this.#unifiedPrepared as unknown as PreparedTextWithSegments,
width,
lineHeight,
);
// 3. Map results back to both fonts // 3. Map results back to both fonts
const preparedA = this.#preparedA; const preparedA = this.#preparedA;
@@ -284,11 +245,11 @@ export class DualFontLayout {
* across both fonts, with `spacingPx` added to model letter-spacing. * across both fonts, with `spacingPx` added to model letter-spacing.
*/ */
#createUnifiedPrepared( #createUnifiedPrepared(
a: PretextInternals, a: PreparedTextWithSegments,
b: PretextInternals, b: PreparedTextWithSegments,
spacingPx: number = 0, spacingPx: number = 0,
): PretextInternals { ): PreparedTextWithSegments {
const unified: PretextInternals = { ...a }; const unified: PreparedTextWithSegments = { ...a };
unified.widths = a.widths.map((w, i) => Math.max(w, b.widths[i]) + spacingPx); unified.widths = a.widths.map((w, i) => Math.max(w, b.widths[i]) + spacingPx);
unified.lineEndFitAdvances = a.lineEndFitAdvances.map((w, i) => unified.lineEndFitAdvances = a.lineEndFitAdvances.map((w, i) =>