@vyaz/core 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/compile/DocumentCompiler.d.ts +83 -0
  2. package/dist/index.browser.d.ts +28 -0
  3. package/dist/index.browser.js +18 -0
  4. package/dist/index.d.ts +30 -0
  5. package/dist/index.js +13 -147930
  6. package/dist/layout/AutoFitEngine.d.ts +44 -0
  7. package/dist/layout/LineBoxValidator.d.ts +25 -0
  8. package/dist/layout/ParagraphLayoutEngine.d.ts +46 -0
  9. package/dist/layout/PositioningEngine.d.ts +68 -0
  10. package/dist/layout/TextFrameLayoutEngine.d.ts +50 -0
  11. package/{src/layout/estimateWidth.ts → dist/layout/estimateWidth.d.ts} +2 -40
  12. package/dist/measure/FontEngine.d.ts +47 -0
  13. package/dist/measure/FontMetricsProvider.d.ts +49 -0
  14. package/dist/measure/FontNotFoundError.d.ts +6 -0
  15. package/dist/measure/SystemFontRegistry.d.ts +46 -0
  16. package/dist/measure/canvas-polyfill.d.ts +30 -0
  17. package/dist/types/Document.d.ts +599 -0
  18. package/dist/types/FontTypes.d.ts +61 -0
  19. package/dist/types/LayoutTypes.d.ts +137 -0
  20. package/{src/utils/env.ts → dist/utils/env.d.ts} +1 -8
  21. package/{src/utils/font.ts → dist/utils/font.d.ts} +1 -13
  22. package/{src/utils/groupLinesByParagraph.ts → dist/utils/groupLinesByParagraph.d.ts} +7 -37
  23. package/dist/utils/list.d.ts +41 -0
  24. package/dist/utils/textTransform.d.ts +32 -0
  25. package/package.json +13 -13
  26. package/src/compile/DocumentCompiler.ts +0 -144
  27. package/src/index.browser.ts +0 -90
  28. package/src/index.ts +0 -97
  29. package/src/layout/AutoFitEngine.ts +0 -101
  30. package/src/layout/LineBoxValidator.ts +0 -162
  31. package/src/layout/ParagraphLayoutEngine.ts +0 -264
  32. package/src/layout/PositioningEngine.ts +0 -615
  33. package/src/layout/TextFrameLayoutEngine.ts +0 -363
  34. package/src/measure/FontEngine.ts +0 -144
  35. package/src/measure/FontMetricsProvider.ts +0 -224
  36. package/src/measure/FontNotFoundError.ts +0 -16
  37. package/src/measure/SystemFontRegistry.ts +0 -152
  38. package/src/measure/canvas-polyfill.d.ts +0 -6
  39. package/src/measure/canvas-polyfill.ts +0 -243
  40. package/src/measure/fontkit.d.ts +0 -44
  41. package/src/types/Document.ts +0 -666
  42. package/src/types/FontTypes.ts +0 -74
  43. package/src/types/LayoutTypes.ts +0 -158
  44. package/src/utils/list.ts +0 -107
  45. package/src/utils/textTransform.ts +0 -96
package/src/index.ts DELETED
@@ -1,97 +0,0 @@
1
- /**
2
- * @vyaz/core — Public API.
3
- *
4
- * Exports input types (Logical level), output types (Physical Box Model),
5
- * layout engines, font metric providers, renderers, and utilities.
6
- */
7
-
8
- // ── Input types (Logical level) ─────────────────────────────────────────
9
- export type {
10
- TextFrame,
11
- Paragraph,
12
- ParagraphStyle,
13
- TextRun,
14
- InlineWidget,
15
- AutofitConfig,
16
- TextAlignment,
17
- WritingMode,
18
- TextOrientation,
19
- VerticalAlignment,
20
- ScriptType,
21
- WhiteSpace,
22
- MultiColumnConfig,
23
- DominantBaseline,
24
- LineFitEdge,
25
- TextAlignLast,
26
- WordBreak,
27
- LineBreak,
28
- OverflowWrap,
29
- TextDecorationStyle,
30
- TextTransform,
31
- ListType,
32
- NumberFormat,
33
- ListStylePosition,
34
- ListStyle,
35
- } from './types/Document.js';
36
- export {
37
- DEFAULT_PARAGRAPH_STYLE,
38
- DEFAULT_TEXT_STYLE,
39
- } from './types/Document.js';
40
-
41
- // ── Output types (Physical Box Model) ───────────────────────────────────
42
- export type {
43
- ParagraphLayoutResult,
44
- Line,
45
- Span,
46
- SpanFontMetrics,
47
- SemanticParagraph,
48
- SemanticLine,
49
- SemanticFragment,
50
- } from './types/LayoutTypes.js';
51
-
52
- // ── Font types ──────────────────────────────────────────────────────────
53
- export type {
54
- FontMetrics,
55
- IFontMetricsProvider,
56
- GlyphData,
57
- } from './types/FontTypes.js';
58
-
59
- // ── Layout Engine ───────────────────────────────────────────────────────
60
- export { ParagraphLayoutEngine, paragraphLayoutEngine } from './layout/ParagraphLayoutEngine.js';
61
- export { positionLines } from './layout/PositioningEngine.js';
62
- export { assertLineInvariants, linesToYAML } from './layout/LineBoxValidator.js';
63
- export type { InvariantError } from './layout/LineBoxValidator.js';
64
-
65
- // ── TextFrame Layout Engine ──────────────────────────────────────────────
66
- export { layoutTextFrame } from './layout/TextFrameLayoutEngine.js';
67
- export type { TextFrameLayoutResult } from './layout/TextFrameLayoutEngine.js';
68
-
69
- // ── Autofit ─────────────────────────────────────────────────────────────
70
- export { applyScale, findScale } from './layout/AutoFitEngine.js';
71
- export type { AutoFitOptions, AutoFitResult } from './layout/AutoFitEngine.js';
72
-
73
- // ── Utils ────────────────────────────────────────────────────────────────
74
- export { groupLinesByParagraph } from './utils/groupLinesByParagraph.js';
75
- export type { ParagraphGroup } from './utils/groupLinesByParagraph.js';
76
- export { transformText } from './utils/textTransform.js';
77
- export { formatListNumber, defaultBulletChar, BULLET_CHARACTERS } from './utils/list.js';
78
-
79
- // ── Compiler ────────────────────────────────────────────────────────────
80
- export { compileParagraph, getParagraphText, makeFontToken } from './compile/DocumentCompiler.js';
81
- export type { PreparedRichInlineItem } from './compile/DocumentCompiler.js';
82
-
83
- // ── Font Engine ──────────────────────────────────────────────────────────
84
- export type { FontFace } from './measure/FontEngine.js';
85
- export { createFontFace, getGlyphAdvance, computePixelMetrics, isFontEngineAvailable } from './measure/FontEngine.js';
86
-
87
- // ── Font metrics ────────────────────────────────────────────────────────
88
- export { FontMetricsProvider, fontMetricsProvider } from './measure/FontMetricsProvider.js';
89
-
90
- // ── System font registry ────────────────────────────────────────────────
91
- export { SystemFontRegistry, systemFontRegistry } from './measure/SystemFontRegistry.js';
92
-
93
- // ── Font utilities ───────────────────────────────────────────────────────
94
- export { getFontBuffer } from './utils/font.js';
95
-
96
- // ── Errors ──────────────────────────────────────────────────────────────
97
- export { FontNotFoundError } from './measure/FontNotFoundError.js';
@@ -1,101 +0,0 @@
1
- /**
2
- * AutoFitEngine.ts — proportional font-size scaling (autofit).
3
- *
4
- * Algorithm: binary search for scale in [0.0, 1.0].
5
- * For each candidate scale: temporarily scale fontSize,
6
- * run full layout, check whether the result fits in
7
- * maxWidth × maxHeight.
8
- *
9
- * Proportions are preserved: every TextRun's fontSize is multiplied
10
- * by the same scale, the visual hierarchy is unchanged.
11
- * inlineWidget dimensions are NOT scaled (images keep their size).
12
- */
13
-
14
- import type { TextFrame } from '../types/Document.js';
15
-
16
- /** Autofit options */
17
- export interface AutoFitOptions {
18
- minScale?: number; // minimum scale (default 0.1)
19
- tolerance?: number; // binary search tolerance (default 0.01)
20
- maxIterations?: number; // max iterations (default 50)
21
- }
22
-
23
- /** Autofit result */
24
- export interface AutoFitResult {
25
- scaleFactor: number; // 0.0 … 1.0
26
- }
27
-
28
- /**
29
- * Apply a scale factor to all fontSize values in the document.
30
- * inlineWidget dimensions are NOT scaled.
31
- * Returns a NEW document (does not mutate the original).
32
- */
33
- export function applyScale(
34
- doc: TextFrame,
35
- scale: number,
36
- ): TextFrame {
37
- const clone = JSON.parse(JSON.stringify(doc)) as TextFrame;
38
-
39
- for (const paragraph of clone.paragraphs) {
40
- for (const run of paragraph.children) {
41
- run.fontSize = Math.round(run.fontSize * scale * 100) / 100;
42
- }
43
- }
44
-
45
- if (clone.defaultStyle?.fontSize) {
46
- clone.defaultStyle.fontSize = Math.round(clone.defaultStyle.fontSize * scale * 100) / 100;
47
- }
48
-
49
- return clone;
50
- }
51
-
52
- /**
53
- * Find the optimal scale factor for a document.
54
- *
55
- * @param doc — source document
56
- * @param layoutFn — layout(doc) → { height: number; width: number }
57
- * @param config — autofit maxWidth/maxHeight
58
- * @param options — search precision
59
- */
60
- export function findScale(
61
- doc: TextFrame,
62
- layoutFn: (scaledDoc: TextFrame) => { height: number; width: number },
63
- config: { maxWidth: number; maxHeight: number },
64
- options?: AutoFitOptions,
65
- ): AutoFitResult {
66
- const minScale = options?.minScale ?? 0.1;
67
- const tolerance = options?.tolerance ?? 0.01;
68
- const maxIterations = options?.maxIterations ?? 50;
69
- const maxHeight = config.maxHeight;
70
- const maxWidth = config.maxWidth;
71
-
72
- // Check original size
73
- const origResult = layoutFn(doc);
74
- if (origResult.height <= maxHeight && origResult.width <= maxWidth) {
75
- return { scaleFactor: 1 };
76
- }
77
-
78
- // Binary search
79
- let lo = minScale;
80
- let hi = 1.0;
81
- let best = minScale;
82
-
83
- for (let iter = 0; iter < maxIterations; iter++) {
84
- const mid = (lo + hi) / 2;
85
- const scaledDoc = applyScale(doc, mid);
86
- const result = layoutFn(scaledDoc);
87
-
88
- if (result.height <= maxHeight && result.width <= maxWidth) {
89
- // scale is valid — try larger
90
- best = mid;
91
- lo = mid + tolerance / 2;
92
- } else {
93
- // scale is invalid — try smaller
94
- hi = mid - tolerance / 2;
95
- }
96
-
97
- if (hi - lo < tolerance) break;
98
- }
99
-
100
- return { scaleFactor: Math.round(best * 100) / 100 };
101
- }
@@ -1,162 +0,0 @@
1
- /**
2
- * LineBoxValidator.ts — invariant checks and YAML serialization.
3
- *
4
- * Parley-inspired invariant checks:
5
- * NO_OVERLAP, MONOTONIC_Y, INDEX_CONSIST, WIDTH_FIT, BASELINE_EQ
6
- *
7
- * YAML snapshots: semantic data only (no metric noise).
8
- */
9
-
10
- import { dump } from 'js-yaml';
11
- import type { Line, Span, SemanticParagraph } from '../types/LayoutTypes.js';
12
-
13
- const EPSILON = 0.5; // subpixel tolerance
14
-
15
- // ── Invariant guard ────────────────────────────────────────────────────
16
-
17
- export interface InvariantError {
18
- invariant: string;
19
- message: string;
20
- details?: any;
21
- }
22
-
23
- /**
24
- * Check all 5 invariants for a Line array.
25
- * Throws on first violation.
26
- */
27
- export function assertLineInvariants(
28
- lines: Line[],
29
- originalText: string,
30
- maxWidth: number,
31
- ): void {
32
- if (lines.length === 0) return;
33
-
34
- const errors: InvariantError[] = [];
35
-
36
- for (let i = 0; i < lines.length; i++) {
37
- const line = lines[i];
38
-
39
- // 1. NO_OVERLAP: lines[i+1].y >= lines[i].y + lines[i].height
40
- if (i > 0) {
41
- const prev = lines[i - 1];
42
- if (line.y < prev.y + prev.height - EPSILON) {
43
- errors.push({
44
- invariant: 'NO_OVERLAP',
45
- message: `Line ${i} overlaps with line ${i - 1}`,
46
- details: {
47
- prevY: prev.y,
48
- prevHeight: prev.height,
49
- prevBottom: prev.y + prev.height,
50
- currentY: line.y,
51
- },
52
- });
53
- }
54
- }
55
-
56
- // 2. MONOTONIC_Y: lines[i+1].y > lines[i].y
57
- if (i > 0 && line.y <= lines[i - 1].y) {
58
- errors.push({
59
- invariant: 'MONOTONIC_Y',
60
- message: `Line ${i} has Y=${line.y} not > prev Y=${lines[i - 1].y}`,
61
- });
62
- }
63
-
64
- // 3. INDEX_CONSIST: checked below (sum of all lengths)
65
- if (line.endIndex <= line.startIndex) {
66
- errors.push({
67
- invariant: 'INDEX_CONSIST',
68
- message: `Line ${i}: endIndex=${line.endIndex} <= startIndex=${line.startIndex}`,
69
- });
70
- }
71
-
72
- // 4. WIDTH_FIT: line.width <= maxWidth + epsilon
73
- // When maxWidth=0 (zero-width container) allow any width
74
- if (maxWidth > 0 && line.width > maxWidth + EPSILON) {
75
- errors.push({
76
- invariant: 'WIDTH_FIT',
77
- message: `Line ${i}: width=${line.width} > maxWidth=${maxWidth}`,
78
- });
79
- }
80
-
81
- // 5. BASELINE_EQ: all spans in a line have the same baseline
82
- if (line.spans.length > 1) {
83
- const firstBaseline = line.baseline;
84
- for (let j = 0; j < line.spans.length; j++) {
85
- const span = line.spans[j];
86
- const spanBaseline = span.fontMetrics.ascent;
87
- if (Math.abs(spanBaseline - firstBaseline) > EPSILON) {
88
- // NOTE: baseline may differ for super/sub — that's normal
89
- // So we only check that baseline is set
90
- if (spanBaseline <= 0) {
91
- errors.push({
92
- invariant: 'BASELINE_EQ',
93
- message: `Line ${i}, span ${j}: baseline=${spanBaseline} is invalid`,
94
- });
95
- }
96
- }
97
- }
98
- }
99
- }
100
-
101
- // 3. INDEX_CONSIST: total text length
102
- // Difference allowed due to trailing whitespace (pretext may drop it)
103
- // const totalChars = lines.reduce((sum, l) => sum + (l.endIndex - l.startIndex), 0);
104
- // if (totalChars > originalText.length + EPSILON) {
105
- // errors.push({
106
- // invariant: 'INDEX_CONSIST',
107
- // message: `Total chars in lines (${totalChars}) > original text length (${originalText.length})`,
108
- // details: { totalChars, originalLength: originalText.length },
109
- // });
110
- // }
111
-
112
- // if (errors.length > 0) {
113
- // const msg = errors.map(e => `[${e.invariant}] ${e.message}`).join('\n');
114
- // throw new Error(`LineBox invariants violated:\n${msg}`);
115
- // }
116
- }
117
-
118
- // ── YAML serialization ─────────────────────────────────────────────────
119
-
120
- /** Span style label for snapshot */
121
- function spanStyleLabel(span: Span): 'bold' | 'italic' | 'normal' {
122
- if (span.style.fontStyle === 'italic') return 'italic';
123
- const w = span.style.fontWeight;
124
- if (w === 'bold' || w === 700) return 'bold';
125
- return 'normal';
126
- }
127
-
128
- /**
129
- * Convert Line[] to YAML string for snapshots.
130
- * Only semantic data: text, x, width, style.
131
- * No glyphAdvances, fontMetrics (noise), inlineWidget.
132
- */
133
- export function linesToYAML(
134
- lines: Line[],
135
- paragraphWidth: number,
136
- paragraphHeight: number,
137
- ): string {
138
- const obj: SemanticParagraph = {
139
- width: paragraphWidth,
140
- height: paragraphHeight,
141
- lines: lines.map(line => ({
142
- y: Math.round(line.y * 100) / 100,
143
- width: Math.round(line.width * 100) / 100,
144
- height: Math.round(line.height * 100) / 100,
145
- baseline: Math.round(line.baseline * 100) / 100,
146
- fragments: line.spans.map(span => ({
147
- text: span.text,
148
- x: Math.round(span.x * 100) / 100,
149
- width: Math.round(span.width * 100) / 100,
150
- ...(spanStyleLabel(span) !== 'normal' ? { style: spanStyleLabel(span) } : {}),
151
- })),
152
- })),
153
- };
154
-
155
- return dump(obj, {
156
- indent: 2,
157
- lineWidth: 120,
158
- noRefs: true,
159
- sortKeys: false,
160
- });
161
- }
162
-
@@ -1,264 +0,0 @@
1
- /**
2
- * ParagraphLayoutEngine.ts — main orchestrator.
3
- *
4
- * Pipeline:
5
- * Paragraph
6
- * → compile (DocumentCompiler)
7
- * → prepareRichInline (pretext)
8
- * → walkRichInlineLineRanges + materializeRichInlineLineRange (pretext)
9
- * → positionLines (PositioningEngine)
10
- * → assertLineInvariants (LineInvariants)
11
- *
12
- * Supports autofit via AutoFitEngine.findScale.
13
- * Caches PreparedRichInline per paragraph key (Parley LayoutContext pattern).
14
- */
15
-
16
- // Polyfill OffscreenCanvas for Node.js (node-canvas)
17
- import '../measure/canvas-polyfill.js';
18
-
19
- import type { Paragraph, ListStyle } from '../types/Document.js';
20
- import type { FontMetrics } from '../types/FontTypes.js';
21
- import type { IFontMetricsProvider } from '../types/FontTypes.js';
22
- import type { ParagraphLayoutResult } from '../types/LayoutTypes.js';
23
- import { compileParagraph, getParagraphText } from '../compile/DocumentCompiler.js';
24
- import type { PreparedRichInlineItem } from '../compile/DocumentCompiler.js';
25
- import { fontMetricsProvider, MISSING_GLYPH_FACTOR } from '../measure/FontMetricsProvider.js';
26
- import { FontNotFoundError } from '../measure/FontNotFoundError.js';
27
- import { positionLines } from './PositioningEngine.js';
28
- import { assertLineInvariants } from './LineBoxValidator.js';
29
-
30
- // @ts-ignore
31
- import { prepareRichInline, materializeRichInlineLineRange, walkRichInlineLineRanges, type PreparedRichInline } from '@chenglou/pretext/rich-inline';
32
-
33
- // ── Cache key builder ─────────────────────────────────────────────────────
34
-
35
- function glyphCacheKey(
36
- text: string,
37
- fontSize: number,
38
- fontFamily?: string,
39
- fontWeight?: string,
40
- fontStyle?: string,
41
- ): string {
42
- return `${fontSize}_${fontFamily || ''}_${fontWeight || ''}_${fontStyle || ''}_${text}`;
43
- }
44
-
45
- // ── Helpers ─────────────────────────────────────────────────────────────
46
-
47
- /** Get FontMetrics for a PreparedRichInlineItem */
48
- function getFontMetricsForItem(item: PreparedRichInlineItem): FontMetrics {
49
- return fontMetricsProvider.getMetrics(
50
- item.metadata.style.fontFamily,
51
- item.metadata.effectiveFontSize,
52
- String(item.metadata.style.fontWeight || 400),
53
- item.metadata.style.fontStyle || 'normal',
54
- );
55
- }
56
-
57
- // ── ParagraphLayoutEngine ─────────────────────────────────────────────
58
-
59
- export class ParagraphLayoutEngine {
60
- private preparedCache = new Map<string, PreparedRichInline>();
61
-
62
- /**
63
- * Layout a single paragraph — basic variant.
64
- *
65
- * @param paragraph — input paragraph
66
- * @param maxWidth — available container width (px)
67
- * @param fontProvider — optional metrics provider (default: fontMetricsProvider)
68
- * @returns ParagraphLayoutResult with Line[]
69
- */
70
- layout(
71
- paragraph: Paragraph,
72
- maxWidth: number,
73
- yOffset: number = 0,
74
- fontProvider?: IFontMetricsProvider,
75
- listStyle?: ListStyle,
76
- listIndex?: number,
77
- listMarkerWidth?: number,
78
- ): ParagraphLayoutResult {
79
- const provider = fontProvider || fontMetricsProvider;
80
-
81
- // Phase 1: Compile
82
- const items = compileParagraph(paragraph);
83
-
84
- // Phase 2: Prepare (cached)
85
- const cacheKey = JSON.stringify(paragraph);
86
- let prepared = this.preparedCache.get(cacheKey);
87
- if (!prepared) {
88
- prepared = prepareRichInline(items);
89
- this.preparedCache.set(cacheKey, prepared);
90
- }
91
-
92
- // Phase 3: Layout — walk lines
93
- const effectiveMaxWidth = paragraph.style.whiteSpace === 'nowrap' ? Infinity : maxWidth;
94
- const pretextLines: any[] = [];
95
- walkRichInlineLineRanges(prepared, effectiveMaxWidth, (range: any) => {
96
- pretextLines.push(range);
97
- });
98
-
99
- // Materialize each line
100
- const materializedLines: any[] = [];
101
- for (const range of pretextLines) {
102
- materializedLines.push(materializeRichInlineLineRange(prepared, range));
103
- }
104
-
105
- // Phase 4: Position
106
- const renderMode = provider.getMode();
107
-
108
- // Per-layout glyph cache: map<text+font+size, Float32Array>
109
- // Lives only for the duration of one layout() call.
110
- const glyphCache = new Map<string, Float32Array>();
111
-
112
- // Build measureText callback: single fontkit pass, caches advances.
113
- const measureTextFn = (
114
- text: string,
115
- fontSize: number,
116
- fontFamily?: string,
117
- fontWeight?: string,
118
- fontStyle?: string,
119
- ): number => {
120
- if (!text) return 0;
121
- const key = glyphCacheKey(text, fontSize, fontFamily, fontWeight, fontStyle);
122
-
123
- // Check cache first — same text+font may appear across multiple fragments
124
- let advances = glyphCache.get(key);
125
- if (!advances) {
126
- advances = this.computeGlyphAdvances(text, fontSize, fontFamily, fontWeight, fontStyle);
127
- glyphCache.set(key, advances);
128
- }
129
-
130
- let total = 0;
131
- for (let i = 0; i < advances.length; i++) total += advances[i];
132
- return Math.round(total * 100) / 100;
133
- };
134
-
135
- const { lines, contentWidth } = positionLines(
136
- materializedLines,
137
- items,
138
- (item) => {
139
- if (fontProvider) {
140
- return fontProvider.getMetrics(
141
- item.metadata.style.fontFamily,
142
- item.metadata.effectiveFontSize,
143
- String(item.metadata.style.fontWeight || 400),
144
- item.metadata.style.fontStyle || 'normal',
145
- );
146
- }
147
- return getFontMetricsForItem(item);
148
- },
149
- paragraph.style,
150
- maxWidth,
151
- yOffset,
152
- renderMode,
153
- measureTextFn,
154
- paragraph.id,
155
- paragraph.style.listStyle,
156
- paragraph.style.listStyle ? (listIndex ?? 1) : undefined,
157
- listMarkerWidth,
158
- );
159
-
160
- // Phase 4b: Fill per-glyph advances — pull from cache or compute if miss
161
- for (const line of lines) {
162
- for (const span of line.spans) {
163
- if (span.type === 'text' && span.text.length > 0 && !span.inlineWidget && !span.glyphAdvances) {
164
- const key = glyphCacheKey(
165
- span.text,
166
- span.fontMetrics.fontSize,
167
- span.style.fontFamily,
168
- String(span.style.fontWeight || 400),
169
- span.style.fontStyle || 'normal',
170
- );
171
- const cached = glyphCache.get(key);
172
- if (cached) {
173
- span.glyphAdvances = Array.from(cached);
174
- } else {
175
- // Cache miss (single-fragment spans skip resolveFragmentWidths),
176
- // compute directly.
177
- span.glyphAdvances = Array.from(this.computeGlyphAdvances(
178
- span.text,
179
- span.fontMetrics.fontSize,
180
- span.style.fontFamily,
181
- String(span.style.fontWeight || 400),
182
- span.style.fontStyle || 'normal',
183
- ));
184
- }
185
- }
186
- }
187
- }
188
-
189
- // Phase 5: Validate
190
- assertLineInvariants(lines, getParagraphText(paragraph), maxWidth);
191
-
192
- // Phase 6: Compute height
193
- const totalHeight = lines.length > 0
194
- ? lines[lines.length - 1].y + lines[lines.length - 1].height + (paragraph.style.spaceAfter || 0)
195
- : 0;
196
-
197
- // Phase 7: Content BBox
198
- const contentHeight = lines.length > 0
199
- ? lines[lines.length - 1].y + lines[lines.length - 1].height
200
- : 0;
201
-
202
- return { width: maxWidth, height: totalHeight, lines, contentWidth, contentHeight };
203
- }
204
-
205
- /**
206
- * Layout with per-glyph advance widths (for SVG glyph mode).
207
- *
208
- * glyphAdvances are now filled by layout() automatically, so
209
- * this method is equivalent to layout(). Kept for API compatibility.
210
- */
211
- layoutGlyph(
212
- paragraph: Paragraph,
213
- maxWidth: number,
214
- yOffset: number = 0,
215
- ): ParagraphLayoutResult {
216
- return this.layout(paragraph, maxWidth, yOffset);
217
- }
218
-
219
- /**
220
- * Compute per-character advance widths via FontEngine (fontkit).
221
- * Returns Float32Array for memory efficiency and faster iteration.
222
- *
223
- * Throws FontNotFoundError if the font is not registered.
224
- */
225
- private computeGlyphAdvances(
226
- text: string,
227
- fontSize: number,
228
- fontFamily?: string,
229
- fontWeight?: string,
230
- fontStyle?: string,
231
- ): Float32Array {
232
- const font = fontMetricsProvider.getFont(
233
- fontFamily || 'Arial',
234
- fontWeight || '400',
235
- fontStyle || 'normal',
236
- );
237
- if (!font) {
238
- throw new FontNotFoundError(
239
- fontFamily || 'Arial',
240
- fontWeight || '400',
241
- fontStyle || 'normal',
242
- );
243
- }
244
-
245
- const scale = fontSize / font.unitsPerEm;
246
- const advances = new Float32Array(text.length);
247
-
248
- for (let i = 0; i < text.length; i++) {
249
- const codePoint = text.codePointAt(i)!;
250
- const advance = font._raw.glyphForCodePoint(codePoint)?.advanceWidth;
251
- if (advance != null) {
252
- advances[i] = advance * scale;
253
- } else {
254
- advances[i] = fontSize * MISSING_GLYPH_FACTOR;
255
- }
256
- if (codePoint > 0xffff) i++;
257
- }
258
-
259
- return advances;
260
- }
261
- }
262
-
263
- /** Singleton */
264
- export const paragraphLayoutEngine = new ParagraphLayoutEngine();