@vyaz/core 0.0.4 → 0.0.6
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.
- package/README.md +156 -0
- package/dist/compile/DocumentCompiler.d.ts +40 -0
- package/dist/index.browser.d.ts +28 -0
- package/dist/index.browser.js +18 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +11 -147930
- package/dist/layout/AutoFitEngine.d.ts +44 -0
- package/dist/layout/LineBoxValidator.d.ts +25 -0
- package/dist/layout/ParagraphLayoutEngine.d.ts +46 -0
- package/dist/layout/PositioningEngine.d.ts +68 -0
- package/dist/layout/TextFrameLayoutEngine.d.ts +50 -0
- package/dist/layout/estimateWidth.d.ts +36 -0
- package/dist/measure/FontEngine.d.ts +47 -0
- package/dist/measure/FontMetricsProvider.d.ts +49 -0
- package/dist/measure/FontNotFoundError.d.ts +6 -0
- package/dist/measure/SystemFontRegistry.d.ts +46 -0
- package/dist/measure/canvas-polyfill.d.ts +30 -0
- package/dist/types/Document.d.ts +593 -0
- package/dist/types/FontTypes.d.ts +61 -0
- package/dist/types/LayoutTypes.d.ts +128 -0
- package/dist/utils/env.d.ts +11 -0
- package/dist/utils/font.d.ts +16 -0
- package/dist/utils/groupLinesByParagraph.d.ts +44 -0
- package/dist/utils/list.d.ts +41 -0
- package/dist/utils/textTransform.d.ts +32 -0
- package/package.json +30 -13
- package/src/compile/DocumentCompiler.ts +0 -136
- package/src/index.ts +0 -80
- package/src/layout/AutoFitEngine.ts +0 -101
- package/src/layout/LineBoxValidator.ts +0 -162
- package/src/layout/ParagraphLayoutEngine.ts +0 -202
- package/src/layout/PositioningEngine.ts +0 -401
- package/src/layout/TextFrameLayoutEngine.ts +0 -91
- package/src/measure/FontMetricsProvider.ts +0 -226
- package/src/measure/FontNotFoundError.ts +0 -16
- package/src/measure/SystemFontRegistry.ts +0 -151
- package/src/measure/canvas-polyfill.d.ts +0 -6
- package/src/measure/canvas-polyfill.ts +0 -235
- package/src/measure/fontkit.d.ts +0 -44
- package/src/types/Document.ts +0 -540
- package/src/types/FontTypes.ts +0 -74
- package/src/types/LayoutTypes.ts +0 -141
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PositioningEngine.ts — pure X/Y positioning math.
|
|
3
|
-
*
|
|
4
|
-
* Takes pretext output (spans with fragments) + font metrics +
|
|
5
|
-
* paragraph style → returns Line[] with absolute coordinates.
|
|
6
|
-
*
|
|
7
|
-
* X: alignment (left/center/right/justify) + indent
|
|
8
|
-
* Y: baseline + lineHeight + spaceBefore/After
|
|
9
|
-
* Justify: fragmented approach (each space → separate Span)
|
|
10
|
-
*
|
|
11
|
-
* Specs:
|
|
12
|
-
* - CSS Text Module Level 3/4 (browser mode)
|
|
13
|
-
* - ISO/IEC 29500 (Office Open XML / DrawingML, office mode)
|
|
14
|
-
* - Parley alignment.rs (conceptually close, but here justify is simpler:
|
|
15
|
-
* slack is divided equally among stretchable space-spans,
|
|
16
|
-
* without mutating ClusterData.advance)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { ParagraphStyle } from '../types/Document.js';
|
|
20
|
-
import type { FontMetrics } from '../types/FontTypes.js';
|
|
21
|
-
import type { Line, Span, SpanFontMetrics } from '../types/LayoutTypes.js';
|
|
22
|
-
import type { PreparedRichInlineItem } from '../compile/DocumentCompiler.js';
|
|
23
|
-
|
|
24
|
-
// ── Helper types for pretext ───────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
interface PretextFragment {
|
|
27
|
-
itemIndex: number;
|
|
28
|
-
text: string;
|
|
29
|
-
gapBefore: number;
|
|
30
|
-
occupiedWidth: number;
|
|
31
|
-
start: { segmentIndex: number; graphemeIndex: number };
|
|
32
|
-
end: { segmentIndex: number; graphemeIndex: number };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface PretextLine {
|
|
36
|
-
fragments: PretextFragment[];
|
|
37
|
-
width: number; // natural width (without stretching)
|
|
38
|
-
end: { segmentIndex: number; graphemeIndex: number };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── PositioningEngine ─────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Build Line[] from pretext lines with alignment and metrics.
|
|
45
|
-
*
|
|
46
|
-
* @param pretextLines — pretext result (materializeRichInlineLineRange)
|
|
47
|
-
* @param items — original PreparedRichInlineItem[] (for metadata)
|
|
48
|
-
* @param fontMetricsFn — function to get font metrics for a span
|
|
49
|
-
* @param style — paragraph style
|
|
50
|
-
* @param maxWidth — available container width
|
|
51
|
-
* @param startY — initial Y position
|
|
52
|
-
* @param mode — metric mode ('browser' | 'office'), affects line height calculation
|
|
53
|
-
* @returns { lines: Line[], contentWidth: number }
|
|
54
|
-
*/
|
|
55
|
-
export function positionLines(
|
|
56
|
-
pretextLines: PretextLine[],
|
|
57
|
-
items: PreparedRichInlineItem[],
|
|
58
|
-
fontMetricsFn: (item: PreparedRichInlineItem) => FontMetrics,
|
|
59
|
-
style: ParagraphStyle,
|
|
60
|
-
maxWidth: number,
|
|
61
|
-
startY: number = 0,
|
|
62
|
-
mode: 'browser' | 'office' = 'browser',
|
|
63
|
-
paragraphId?: string,
|
|
64
|
-
): { lines: Line[]; contentWidth: number } {
|
|
65
|
-
const lines: Line[] = [];
|
|
66
|
-
let currentY = startY + style.spaceBefore;
|
|
67
|
-
let charIndex = 0;
|
|
68
|
-
let isFirstLine = true;
|
|
69
|
-
let contentWidth = 0;
|
|
70
|
-
|
|
71
|
-
for (let lineIdx = 0; lineIdx < pretextLines.length; lineIdx++) {
|
|
72
|
-
const ptLine = pretextLines[lineIdx];
|
|
73
|
-
|
|
74
|
-
// ── Build Span[] ────────────────────────────
|
|
75
|
-
let maxAscent = 0;
|
|
76
|
-
let maxDescent = 0;
|
|
77
|
-
let maxLineHeightBase = 0; // max(ascent + descent) — for Office mode
|
|
78
|
-
const spans: Span[] = [];
|
|
79
|
-
|
|
80
|
-
for (const frag of ptLine.fragments) {
|
|
81
|
-
const item = items[frag.itemIndex];
|
|
82
|
-
if (!item) continue;
|
|
83
|
-
|
|
84
|
-
const metrics = fontMetricsFn(item);
|
|
85
|
-
const effectiveAscent = metrics.ascent + (item.metadata.baselineOffset || 0);
|
|
86
|
-
const effectiveDescent = metrics.descent - (item.metadata.baselineOffset || 0);
|
|
87
|
-
|
|
88
|
-
maxAscent = Math.max(maxAscent, effectiveAscent);
|
|
89
|
-
maxDescent = Math.max(maxDescent, effectiveDescent);
|
|
90
|
-
// Office: line height base = ascent + descent (OS/2 usWinAscent + usWinDescent, scaled)
|
|
91
|
-
maxLineHeightBase = Math.max(maxLineHeightBase, metrics.ascent + metrics.descent);
|
|
92
|
-
|
|
93
|
-
// pretext: gapBefore — inter-word space BEFORE the word
|
|
94
|
-
// occupiedWidth = gapBefore + textWidth
|
|
95
|
-
// Split into two Span: space + word
|
|
96
|
-
const gapWidth = frag.gapBefore || 0;
|
|
97
|
-
const textWidth = frag.occupiedWidth;
|
|
98
|
-
|
|
99
|
-
const baseFontMetrics = {
|
|
100
|
-
ascent: metrics.ascent,
|
|
101
|
-
descent: metrics.descent,
|
|
102
|
-
fontSize: item.metadata.effectiveFontSize,
|
|
103
|
-
baselineOffset: item.metadata.baselineOffset || undefined,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
if (gapWidth > 0) {
|
|
107
|
-
spans.push({
|
|
108
|
-
x: 0,
|
|
109
|
-
width: gapWidth,
|
|
110
|
-
text: ' ',
|
|
111
|
-
itemIndex: frag.itemIndex,
|
|
112
|
-
paragraphId,
|
|
113
|
-
fontMetrics: baseFontMetrics,
|
|
114
|
-
style: item.metadata.style,
|
|
115
|
-
inlineWidget: item.metadata.inlineWidget,
|
|
116
|
-
type: 'space',
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Split leading/trailing spaces from frag.text into separate space spans
|
|
121
|
-
// This is needed for SVG rendering to avoid xml:space="preserve" dependency
|
|
122
|
-
const text = frag.text;
|
|
123
|
-
const leadingMatch = text.match(/^(\s+)/);
|
|
124
|
-
|
|
125
|
-
const totalChars = text.length;
|
|
126
|
-
let remainingText = text;
|
|
127
|
-
let leadingSpaceChars = 0;
|
|
128
|
-
let trailingSpaceChars = 0;
|
|
129
|
-
|
|
130
|
-
if (leadingMatch) {
|
|
131
|
-
leadingSpaceChars = leadingMatch[1].length;
|
|
132
|
-
remainingText = remainingText.slice(leadingSpaceChars);
|
|
133
|
-
}
|
|
134
|
-
if (remainingText.length > 0) {
|
|
135
|
-
const trailMatch = remainingText.match(/(\s+)$/);
|
|
136
|
-
if (trailMatch) {
|
|
137
|
-
trailingSpaceChars = trailMatch[1].length;
|
|
138
|
-
remainingText = remainingText.slice(0, -trailingSpaceChars);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const computePartialWidth = (charCount: number) => {
|
|
143
|
-
return totalChars > 0 ? (charCount / totalChars) * textWidth : 0;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Leading space span
|
|
147
|
-
if (leadingSpaceChars > 0) {
|
|
148
|
-
spans.push({
|
|
149
|
-
x: 0,
|
|
150
|
-
width: computePartialWidth(leadingSpaceChars),
|
|
151
|
-
text: text.slice(0, leadingSpaceChars),
|
|
152
|
-
itemIndex: frag.itemIndex,
|
|
153
|
-
paragraphId,
|
|
154
|
-
fontMetrics: baseFontMetrics,
|
|
155
|
-
style: item.metadata.style,
|
|
156
|
-
inlineWidget: item.metadata.inlineWidget,
|
|
157
|
-
type: 'space',
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Text span (trimmed)
|
|
162
|
-
if (remainingText.length > 0) {
|
|
163
|
-
const textWidth = computePartialWidth(remainingText.length);
|
|
164
|
-
// Compute per-glyph advances by distributing textWidth equally across glyphs.
|
|
165
|
-
// For monospace fonts this is exact; for proportional it's a linear approximation.
|
|
166
|
-
// The FontMetricsProvider can later supply real glyph advances when available.
|
|
167
|
-
const glyphAdvances: number[] = [];
|
|
168
|
-
if (remainingText.length > 1) {
|
|
169
|
-
const perGlyph = textWidth / remainingText.length;
|
|
170
|
-
for (let i = 0; i < remainingText.length; i++) {
|
|
171
|
-
glyphAdvances.push(perGlyph);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
spans.push({
|
|
175
|
-
x: 0,
|
|
176
|
-
width: textWidth,
|
|
177
|
-
text: remainingText,
|
|
178
|
-
itemIndex: frag.itemIndex,
|
|
179
|
-
paragraphId,
|
|
180
|
-
fontMetrics: baseFontMetrics,
|
|
181
|
-
style: item.metadata.style,
|
|
182
|
-
inlineWidget: item.metadata.inlineWidget,
|
|
183
|
-
type: 'text',
|
|
184
|
-
glyphAdvances: glyphAdvances.length > 0 ? glyphAdvances : undefined,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Trailing space span
|
|
189
|
-
if (trailingSpaceChars > 0) {
|
|
190
|
-
const trailingStart = leadingSpaceChars + remainingText.length;
|
|
191
|
-
spans.push({
|
|
192
|
-
x: 0,
|
|
193
|
-
width: computePartialWidth(trailingSpaceChars),
|
|
194
|
-
text: text.slice(trailingStart, trailingStart + trailingSpaceChars),
|
|
195
|
-
itemIndex: frag.itemIndex,
|
|
196
|
-
paragraphId,
|
|
197
|
-
fontMetrics: baseFontMetrics,
|
|
198
|
-
style: item.metadata.style,
|
|
199
|
-
inlineWidget: item.metadata.inlineWidget,
|
|
200
|
-
type: 'space',
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ── Inline-box width correction ─────────────────────────
|
|
206
|
-
// When a span has an inlineWidget, override its width
|
|
207
|
-
// to match the widget width (the \uFFFC advance is not included).
|
|
208
|
-
for (const s of spans) {
|
|
209
|
-
if (s.inlineWidget) {
|
|
210
|
-
s.width = s.inlineWidget.width;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ── Mark trailing whitespace spans ───────────────────
|
|
215
|
-
// CSS Text §4.1.3: end-of-line spaces have zero measure for line-advance calculations.
|
|
216
|
-
// Parley: LineItemData.has_trailing_whitespace → trailing whitespace is excluded from advance.
|
|
217
|
-
//
|
|
218
|
-
// Find the last space(s) at the end of the line and mark them trailing.
|
|
219
|
-
let trailingStartIndex = spans.length;
|
|
220
|
-
for (let i = spans.length - 1; i >= 0; i--) {
|
|
221
|
-
if (spans[i].type === 'space') {
|
|
222
|
-
trailingStartIndex = i;
|
|
223
|
-
} else {
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const trailingWidth = spans
|
|
229
|
-
.slice(trailingStartIndex)
|
|
230
|
-
.reduce((sum, f) => sum + f.width, 0);
|
|
231
|
-
|
|
232
|
-
for (let i = trailingStartIndex; i < spans.length; i++) {
|
|
233
|
-
spans[i].trailing = true;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ── Compute effective line width (excluding trailing whitespace) ─
|
|
237
|
-
// Use sum of actual span widths instead of ptLine.width, because
|
|
238
|
-
// ptLine.width may not include gapBefore spaces that were split into
|
|
239
|
-
// separate Span (e.g. "AA" + " A" → [AA][ ][A]).
|
|
240
|
-
const totalSpanWidth = spans.reduce((sum, f) => sum + f.width, 0);
|
|
241
|
-
const effectiveLineWidth = totalSpanWidth - trailingWidth;
|
|
242
|
-
|
|
243
|
-
// ── X positioning ───────────────────────────────
|
|
244
|
-
const indent = isFirstLine
|
|
245
|
-
? (style.leftIndent || 0) + (style.indent || 0)
|
|
246
|
-
: (style.leftIndent || 0);
|
|
247
|
-
const rightIndent = style.rightIndent || 0;
|
|
248
|
-
const availableWidth = maxWidth - indent - rightIndent;
|
|
249
|
-
|
|
250
|
-
const slack = Math.max(0, availableWidth - effectiveLineWidth);
|
|
251
|
-
|
|
252
|
-
let xOffset = indent;
|
|
253
|
-
|
|
254
|
-
if (style.alignment === 'center') {
|
|
255
|
-
xOffset = indent + slack / 2;
|
|
256
|
-
} else if (style.alignment === 'right') {
|
|
257
|
-
xOffset = indent + slack;
|
|
258
|
-
} else if (style.alignment === 'justify') {
|
|
259
|
-
// Justify: distribute slack evenly among whitespace spans
|
|
260
|
-
// (excluding trailing whitespace).
|
|
261
|
-
//
|
|
262
|
-
// CSS Text Module Level 3 §4.1.3: trailing spaces do not participate in justify.
|
|
263
|
-
// CSS text-align-last: last line is not justify, but start-align.
|
|
264
|
-
// OOXML (ISO 29500): last line of paragraph is not stretched.
|
|
265
|
-
// Parley alignment.rs: excludes last line (line.break_reason == None/Explicit)
|
|
266
|
-
// and lines with num_spaces == 0.
|
|
267
|
-
//
|
|
268
|
-
// Determine if this is the last line in the paragraph.
|
|
269
|
-
const isLastLine = lineIdx === pretextLines.length - 1;
|
|
270
|
-
|
|
271
|
-
// Count only "stretchable" spaces: type === 'space' and !trailing
|
|
272
|
-
const stretchableSpaces = spans.filter(
|
|
273
|
-
(f) => f.type === 'space' && !f.trailing,
|
|
274
|
-
);
|
|
275
|
-
const spaceCount = stretchableSpaces.length;
|
|
276
|
-
|
|
277
|
-
if (!isLastLine && spaceCount > 0 && slack > 0) {
|
|
278
|
-
const extraPerSpace = slack / spaceCount;
|
|
279
|
-
for (const sf of stretchableSpaces) {
|
|
280
|
-
sf.width += extraPerSpace;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// If this is the last line or no spaces — fallback to start-align
|
|
285
|
-
// (LTR → left, RTL → right — always left for now, Bidi in Phase 2)
|
|
286
|
-
xOffset = indent;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Assign X positions
|
|
290
|
-
let runX = xOffset;
|
|
291
|
-
for (const frag of spans) {
|
|
292
|
-
frag.x = Math.round(runX * 100) / 100;
|
|
293
|
-
runX += frag.width;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const lineWidth = runX - xOffset;
|
|
297
|
-
contentWidth = Math.max(contentWidth, lineWidth);
|
|
298
|
-
|
|
299
|
-
// ── Y positioning ───────────────────────────────
|
|
300
|
-
// Line height algorithm depends on mode:
|
|
301
|
-
//
|
|
302
|
-
// 'browser' (CSS-compatible, parley/Chrome matching):
|
|
303
|
-
// 1. lineHeightPx = maxFontSize * style.lineHeight
|
|
304
|
-
//
|
|
305
|
-
// 'office' (MS Office / DrawingML pixel-perfect):
|
|
306
|
-
// PowerPoint has no line-height multiplier for single lines.
|
|
307
|
-
// Line height strictly = ascent + descent (OS/2.usWinAscent + usWinDescent).
|
|
308
|
-
// Baseline = Top + ascent without any half-leading additions.
|
|
309
|
-
// See pixel-perfect-text-layout.md §1 and ECMA-376.
|
|
310
|
-
const maxFontSize = spans.reduce((max, f) => Math.max(max, f.fontMetrics.fontSize), 0);
|
|
311
|
-
|
|
312
|
-
const ascentRounded = Math.round(maxAscent);
|
|
313
|
-
const descentRounded = Math.round(maxDescent);
|
|
314
|
-
|
|
315
|
-
let lineBoxHeight: number;
|
|
316
|
-
let baseline: number;
|
|
317
|
-
|
|
318
|
-
if (mode === 'office') {
|
|
319
|
-
// DrawingML: lineHeight = ascent + descent, no lineHeight ×1.15 and no leading.
|
|
320
|
-
// DrawingML: base line height = OS/2 (usWinAscent + usWinDescent), without
|
|
321
|
-
// sum of rounded ascent/descent — that formula caused pixel-perfect
|
|
322
|
-
// mismatch with PowerPoint, so we use maxLineHeightBase.
|
|
323
|
-
lineBoxHeight = maxLineHeightBase;
|
|
324
|
-
baseline = ascentRounded;
|
|
325
|
-
} else {
|
|
326
|
-
// Browser: CSS-compatible with leading distribution.
|
|
327
|
-
const lineHeightPx = maxFontSize * style.lineHeight;
|
|
328
|
-
const ascentDescentRounded = ascentRounded + descentRounded;
|
|
329
|
-
|
|
330
|
-
const rawLineBoxHeight = Math.round(lineHeightPx);
|
|
331
|
-
lineBoxHeight = Math.max(rawLineBoxHeight, ascentDescentRounded);
|
|
332
|
-
const leading = lineBoxHeight - ascentDescentRounded; // always integer
|
|
333
|
-
|
|
334
|
-
if (leading <= 0) {
|
|
335
|
-
// Negative or zero leading: don't shrink the line
|
|
336
|
-
baseline = ascentRounded;
|
|
337
|
-
} else {
|
|
338
|
-
// Positive leading: distribute as integers with above_leading < below_leading
|
|
339
|
-
const ascentDescent = maxAscent + maxDescent;
|
|
340
|
-
const aboveLeadingFloat = ascentDescent > 0 ? leading * maxAscent / ascentDescent : leading / 2;
|
|
341
|
-
let aboveLeading = Math.round(aboveLeadingFloat);
|
|
342
|
-
let belowLeading = leading - aboveLeading;
|
|
343
|
-
|
|
344
|
-
// Ensure above_leading < below_leading (parley/Chrome heuristic)
|
|
345
|
-
if (aboveLeading >= belowLeading) {
|
|
346
|
-
aboveLeading = Math.floor((leading - 1) / 2);
|
|
347
|
-
belowLeading = leading - aboveLeading;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
baseline = ascentRounded + aboveLeading;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const startIdx = charIndex;
|
|
355
|
-
|
|
356
|
-
// Count characters in line (for INDEX_CONSIST)
|
|
357
|
-
let lineCharCount = 0;
|
|
358
|
-
for (const frag of spans) {
|
|
359
|
-
lineCharCount += frag.text.length;
|
|
360
|
-
}
|
|
361
|
-
const endIdx = startIdx + lineCharCount;
|
|
362
|
-
charIndex = endIdx;
|
|
363
|
-
|
|
364
|
-
// ── Mark break type on the last span ─────────────
|
|
365
|
-
// 'soft' — line wrap due to width constraint
|
|
366
|
-
// 'hard' — explicit break (\n)
|
|
367
|
-
// 'none'/undefined — not a line end (no break)
|
|
368
|
-
if (spans.length > 0) {
|
|
369
|
-
const lastSpan = spans[spans.length - 1];
|
|
370
|
-
const lastSpanItem = items[lastSpan.itemIndex];
|
|
371
|
-
// If last character is \n, it's a hard break
|
|
372
|
-
if (lastSpanItem && lastSpan.text.endsWith('\n')) {
|
|
373
|
-
lastSpan.breakType = 'hard';
|
|
374
|
-
} else if (lineIdx < pretextLines.length - 1) {
|
|
375
|
-
lastSpan.breakType = 'soft';
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
lines.push({
|
|
380
|
-
x: Math.round(xOffset * 100) / 100,
|
|
381
|
-
y: Math.round(currentY * 100) / 100,
|
|
382
|
-
width: Math.round(lineWidth * 100) / 100,
|
|
383
|
-
height: Math.round(lineBoxHeight * 100) / 100,
|
|
384
|
-
baseline: Math.round(baseline * 100) / 100,
|
|
385
|
-
ascent: Math.round(maxAscent * 100) / 100,
|
|
386
|
-
descent: Math.round(maxDescent * 100) / 100,
|
|
387
|
-
startIndex: startIdx,
|
|
388
|
-
endIndex: endIdx,
|
|
389
|
-
alignment: style.alignment,
|
|
390
|
-
spans,
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
currentY += lineBoxHeight;
|
|
394
|
-
isFirstLine = false;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Add spaceAfter to last line height
|
|
398
|
-
// (return as is — ParagraphLayoutEngine will add spaceAfter to total height)
|
|
399
|
-
|
|
400
|
-
return { lines, contentWidth };
|
|
401
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TextFrameLayoutEngine.ts — Layout a full TextFrame (multi-paragraph).
|
|
3
|
-
*
|
|
4
|
-
* Pipeline:
|
|
5
|
-
* TextFrame → Paragraph[] → paragraphLayoutEngine.layout() each → merge Line[]
|
|
6
|
-
*
|
|
7
|
-
* Handles:
|
|
8
|
-
* - Paragraph stacking with Y offset accumulation
|
|
9
|
-
* - Padding (left reduces available width, left shifts X)
|
|
10
|
-
* - frame.width/height optional → fitHorizontal/fitVertical flags
|
|
11
|
-
*/
|
|
12
|
-
import type { TextFrame } from '../types/Document.js';
|
|
13
|
-
import type { Line } from '../types/LayoutTypes.js';
|
|
14
|
-
import { paragraphLayoutEngine } from './ParagraphLayoutEngine.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Result of laying out a full TextFrame.
|
|
18
|
-
*
|
|
19
|
-
* `fitHorizontal` / `fitVertical` tell the renderer which dimension to use:
|
|
20
|
-
* - `'frame'` → use `frameWidth` / `frameHeight`
|
|
21
|
-
* - `'content'` → use `contentWidth` / `contentHeight`
|
|
22
|
-
*/
|
|
23
|
-
export interface TextFrameLayoutResult {
|
|
24
|
-
lines: Line[];
|
|
25
|
-
/** Frame width (set when TextFrame.width was provided). */
|
|
26
|
-
frameWidth?: number;
|
|
27
|
-
/** Frame height (set when TextFrame.height was provided). */
|
|
28
|
-
frameHeight?: number;
|
|
29
|
-
/** Actual content width (may exceed frameWidth when wrap=false). */
|
|
30
|
-
contentWidth: number;
|
|
31
|
-
/** Actual content height (may exceed frameHeight). */
|
|
32
|
-
contentHeight: number;
|
|
33
|
-
/** Whether horizontal dimension should use frame or content size. */
|
|
34
|
-
fitHorizontal: 'frame' | 'content';
|
|
35
|
-
/** Whether vertical dimension should use frame or content size. */
|
|
36
|
-
fitVertical: 'frame' | 'content';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Layout a full TextFrame by stacking paragraphs with Y offset accumulation.
|
|
41
|
-
*/
|
|
42
|
-
export function layoutTextFrame(frame: TextFrame): TextFrameLayoutResult {
|
|
43
|
-
const allLines: Line[] = [];
|
|
44
|
-
let yOffset = frame.padding?.top ?? 0;
|
|
45
|
-
let contentWidth = 0;
|
|
46
|
-
|
|
47
|
-
const leftPad = frame.padding?.left ?? 0;
|
|
48
|
-
const rightPad = frame.padding?.right ?? 0;
|
|
49
|
-
|
|
50
|
-
for (let i = 0; i < frame.paragraphs.length; i++) {
|
|
51
|
-
const p = frame.paragraphs[i];
|
|
52
|
-
|
|
53
|
-
// Available width: frame.width minus horizontal padding.
|
|
54
|
-
// If width is undefined → Infinite (no wrap constraint).
|
|
55
|
-
const maxWidth = frame.width !== undefined
|
|
56
|
-
? frame.width - leftPad - rightPad
|
|
57
|
-
: Infinity;
|
|
58
|
-
|
|
59
|
-
// If wrap is disabled, force no-wrap on the paragraph
|
|
60
|
-
if (frame.wrap === false) {
|
|
61
|
-
p.style = { ...p.style, whiteSpace: 'nowrap' };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const result = paragraphLayoutEngine.layout(p, maxWidth, yOffset);
|
|
65
|
-
|
|
66
|
-
for (const line of result.lines) {
|
|
67
|
-
// Shift lines by left padding
|
|
68
|
-
line.x += leftPad;
|
|
69
|
-
allLines.push(line);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
contentWidth = Math.max(contentWidth, result.contentWidth);
|
|
73
|
-
// result.height is absolute (includes yOffset passed in).
|
|
74
|
-
// Set, don't accumulate — otherwise yOffset compounds.
|
|
75
|
-
yOffset = result.height;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const contentHeight = allLines.length > 0
|
|
79
|
-
? allLines[allLines.length - 1].y + allLines[allLines.length - 1].height
|
|
80
|
-
: 0;
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
lines: allLines,
|
|
84
|
-
frameWidth: frame.width,
|
|
85
|
-
frameHeight: frame.height,
|
|
86
|
-
contentWidth,
|
|
87
|
-
contentHeight,
|
|
88
|
-
fitHorizontal: frame.width !== undefined ? 'frame' : 'content',
|
|
89
|
-
fitVertical: frame.height !== undefined ? 'frame' : 'content',
|
|
90
|
-
};
|
|
91
|
-
}
|