postext 1.35.0 → 1.36.0
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/dist/__bench__/composedPage.bench.test.d.ts +2 -0
- package/dist/__bench__/composedPage.bench.test.d.ts.map +1 -0
- package/dist/__bench__/composedPage.bench.test.js +95 -0
- package/dist/__bench__/composedPage.bench.test.js.map +1 -0
- package/dist/__tests__/composed/fit.test.d.ts +2 -0
- package/dist/__tests__/composed/fit.test.d.ts.map +1 -0
- package/dist/__tests__/composed/fit.test.js +405 -0
- package/dist/__tests__/composed/fit.test.js.map +1 -0
- package/dist/__tests__/composed/layout.test.js +2 -1
- package/dist/__tests__/composed/layout.test.js.map +1 -1
- package/dist/__tests__/composed/pageMemo.test.d.ts +2 -0
- package/dist/__tests__/composed/pageMemo.test.d.ts.map +1 -0
- package/dist/__tests__/composed/pageMemo.test.js +128 -0
- package/dist/__tests__/composed/pageMemo.test.js.map +1 -0
- package/dist/__tests__/composed/sourceMap.test.d.ts +2 -0
- package/dist/__tests__/composed/sourceMap.test.d.ts.map +1 -0
- package/dist/__tests__/composed/sourceMap.test.js +369 -0
- package/dist/__tests__/composed/sourceMap.test.js.map +1 -0
- package/dist/__tests__/composed/templateFrameRects.test.d.ts +2 -0
- package/dist/__tests__/composed/templateFrameRects.test.d.ts.map +1 -0
- package/dist/__tests__/composed/templateFrameRects.test.js +37 -0
- package/dist/__tests__/composed/templateFrameRects.test.js.map +1 -0
- package/dist/__tests__/design/imageAltText.test.js +2 -1
- package/dist/__tests__/design/imageAltText.test.js.map +1 -1
- package/dist/__tests__/exports.test.js +22 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/pipeline/contourWrap.test.d.ts +2 -0
- package/dist/__tests__/pipeline/contourWrap.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/contourWrap.test.js +412 -0
- package/dist/__tests__/pipeline/contourWrap.test.js.map +1 -0
- package/dist/__tests__/toc.test.js +3 -2
- package/dist/__tests__/toc.test.js.map +1 -1
- package/dist/bundle/codec.d.ts +16 -0
- package/dist/bundle/codec.d.ts.map +1 -1
- package/dist/bundle/codec.js +44 -2
- package/dist/bundle/codec.js.map +1 -1
- package/dist/bundle/index.d.ts +1 -1
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/index.js.map +1 -1
- package/dist/canvas-backend/composed.d.ts.map +1 -1
- package/dist/canvas-backend/composed.js +5 -2
- package/dist/canvas-backend/composed.js.map +1 -1
- package/dist/composed/contour.d.ts +21 -0
- package/dist/composed/contour.d.ts.map +1 -0
- package/dist/composed/contour.js +34 -0
- package/dist/composed/contour.js.map +1 -0
- package/dist/composed/geometry.d.ts +8 -0
- package/dist/composed/geometry.d.ts.map +1 -1
- package/dist/composed/geometry.js +26 -0
- package/dist/composed/geometry.js.map +1 -1
- package/dist/composed/ids.d.ts +22 -0
- package/dist/composed/ids.d.ts.map +1 -0
- package/dist/composed/ids.js +32 -0
- package/dist/composed/ids.js.map +1 -0
- package/dist/composed/pageMemo.d.ts +72 -0
- package/dist/composed/pageMemo.d.ts.map +1 -0
- package/dist/composed/pageMemo.js +287 -0
- package/dist/composed/pageMemo.js.map +1 -0
- package/dist/composed/paint.d.ts +21 -7
- package/dist/composed/paint.d.ts.map +1 -1
- package/dist/composed/paint.js +33 -5
- package/dist/composed/paint.js.map +1 -1
- package/dist/composed/reflowLog.d.ts +11 -1
- package/dist/composed/reflowLog.d.ts.map +1 -1
- package/dist/composed/reflowLog.js +45 -2
- package/dist/composed/reflowLog.js.map +1 -1
- package/dist/composed/slotChecks.d.ts.map +1 -1
- package/dist/composed/slotChecks.js +3 -1
- package/dist/composed/slotChecks.js.map +1 -1
- package/dist/composed/sourceMap.d.ts +118 -0
- package/dist/composed/sourceMap.d.ts.map +1 -0
- package/dist/composed/sourceMap.js +668 -0
- package/dist/composed/sourceMap.js.map +1 -0
- package/dist/composed/spread.d.ts.map +1 -1
- package/dist/composed/spread.js +10 -11
- package/dist/composed/spread.js.map +1 -1
- package/dist/composed/templates.d.ts +1 -0
- package/dist/composed/templates.d.ts.map +1 -1
- package/dist/composed/templates.js +4 -0
- package/dist/composed/templates.js.map +1 -1
- package/dist/composed/types.d.ts +15 -32
- package/dist/composed/types.d.ts.map +1 -1
- package/dist/composed/validate.d.ts.map +1 -1
- package/dist/composed/validate.js +3 -2
- package/dist/composed/validate.js.map +1 -1
- package/dist/composed/wrap.d.ts.map +1 -1
- package/dist/composed/wrap.js +7 -1
- package/dist/composed/wrap.js.map +1 -1
- package/dist/contour/index.d.ts +134 -0
- package/dist/contour/index.d.ts.map +1 -0
- package/dist/contour/index.js +354 -0
- package/dist/contour/index.js.map +1 -0
- package/dist/contour/png.d.ts +16 -0
- package/dist/contour/png.d.ts.map +1 -0
- package/dist/contour/png.js +173 -0
- package/dist/contour/png.js.map +1 -0
- package/dist/html-backend.js +2 -2
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +2 -0
- package/dist/measure/rich.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +44 -3
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/types.d.ts +20 -0
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +108 -8
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildMeasurement.d.ts +7 -2
- package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
- package/dist/pipeline/buildMeasurement.js +10 -5
- package/dist/pipeline/buildMeasurement.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +20 -14
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +20 -2
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/composedLayout.d.ts +36 -5
- package/dist/pipeline/composedLayout.d.ts.map +1 -1
- package/dist/pipeline/composedLayout.js +553 -68
- package/dist/pipeline/composedLayout.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +13 -4
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +2 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js +1 -0
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/frameLayout.d.ts +3 -3
- package/dist/pipeline/frameLayout.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.d.ts +9 -0
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +56 -4
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +1 -0
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/outline.d.ts.map +1 -1
- package/dist/pipeline/outline.js +2 -0
- package/dist/pipeline/outline.js.map +1 -1
- package/dist/pipeline/textWrap.d.ts +4 -1
- package/dist/pipeline/textWrap.d.ts.map +1 -1
- package/dist/pipeline/textWrap.js +17 -3
- package/dist/pipeline/textWrap.js.map +1 -1
- package/dist/pipeline/toc.d.ts.map +1 -1
- package/dist/pipeline/toc.js +1 -0
- package/dist/pipeline/toc.js.map +1 -1
- package/dist/types.d.ts +64 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +155 -11
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,18 +1,412 @@
|
|
|
1
|
+
import { pageIsMirrored } from '../vdt';
|
|
2
|
+
import { contourBounds, scaleContourAbout, shiftContour } from '../contour';
|
|
3
|
+
import { frameContour } from '../composed/contour';
|
|
1
4
|
import { dimensionToPx } from '../units';
|
|
2
5
|
import { frameBounds } from '../composed/paint';
|
|
3
6
|
import { FRAME_ATTR } from '../composed/wrap';
|
|
4
7
|
import { framePlaceable, nextFrameOf, groupPlaceable, placeBox, unionRect } from '../composed/geometry';
|
|
5
8
|
import { composedOrder, groupOfFrames, layoutGroup } from '../composed/groups';
|
|
6
9
|
import { reflowEvents } from '../composed/reflowLog';
|
|
7
|
-
import { anchorTarget } from '../composed/templates';
|
|
10
|
+
import { anchorTarget, continueInTemplate } from '../composed/templates';
|
|
8
11
|
import { resolvePageTemplate } from '../composed/templates';
|
|
12
|
+
import { frameElementId } from '../composed/ids';
|
|
9
13
|
import { offsetCalloutToAbsolute } from './calloutLayout';
|
|
14
|
+
import { isMarkerBlock } from './buildHelpers';
|
|
10
15
|
import { CUT_START, fragmentRange, makeCalloutLayouter, splitCalloutFragment } from './frameLayout';
|
|
11
16
|
const MM_PER_IN = 25.4;
|
|
17
|
+
const NO_FIT = { patches: new Map(), text: new Map(), pictures: new Map(), yieldAll: new Set() };
|
|
18
|
+
/** Step of the text and picture scales the ladder tries (DESIGN §5.4). */
|
|
19
|
+
const FIT_STEP = 0.025;
|
|
20
|
+
/** Lowest text scale an `allowShrink` proposal goes to. */
|
|
21
|
+
const PROPOSAL_MIN_SCALE = 0.5;
|
|
22
|
+
/** Most compositions of one page the ladder and its proposals may run. */
|
|
23
|
+
const COMPOSE_BUDGET = 96;
|
|
24
|
+
/** The scales from just under 1 (or under `from`) down to `min`, in
|
|
25
|
+
* {@link FIT_STEP} steps, `min` last. */
|
|
26
|
+
function scaleSteps(min, from = 1) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (let k = 1;; k++) {
|
|
29
|
+
const s = Math.round((from - k * FIT_STEP) * 1e4) / 1e4;
|
|
30
|
+
if (s <= min + 1e-6)
|
|
31
|
+
break;
|
|
32
|
+
out.push(s);
|
|
33
|
+
}
|
|
34
|
+
if (min < from - 1e-6)
|
|
35
|
+
out.push(Math.round(min * 1e4) / 1e4);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** The template with the ladder's patches on its frames. */
|
|
39
|
+
function patchTemplate(template, patches) {
|
|
40
|
+
if (patches.size === 0)
|
|
41
|
+
return template;
|
|
42
|
+
return { ...template, frames: template.frames.map((f) => (patches.has(f.name) ? { ...f, ...patches.get(f.name) } : f)) };
|
|
43
|
+
}
|
|
44
|
+
/** A fixed height (a length, not a percentage). */
|
|
45
|
+
const fixedHeight = (f) => {
|
|
46
|
+
const h = f.height;
|
|
47
|
+
return h !== null && typeof h === 'object' && 'value' in h && 'unit' in h ? f.height : undefined;
|
|
48
|
+
};
|
|
12
49
|
export function layoutComposedPage(input) {
|
|
13
|
-
const {
|
|
50
|
+
const { template, fence, dpi } = input;
|
|
14
51
|
const fi = fence.fenceIndex;
|
|
15
|
-
const
|
|
52
|
+
const mmPx = dpi / MM_PER_IN;
|
|
53
|
+
const byName = new Map(template.frames.map((f) => [f.name, f]));
|
|
54
|
+
const groupOf = groupOfFrames(template);
|
|
55
|
+
const prev = new Map();
|
|
56
|
+
for (const f of template.frames) {
|
|
57
|
+
const n = nextFrameOf(f);
|
|
58
|
+
if (n && byName.has(n) && !prev.has(n))
|
|
59
|
+
prev.set(n, f.name);
|
|
60
|
+
}
|
|
61
|
+
const headOf = (name) => {
|
|
62
|
+
let at = name;
|
|
63
|
+
const seen = new Set([at]);
|
|
64
|
+
for (let p = prev.get(at); p !== undefined && !seen.has(p); p = prev.get(at)) {
|
|
65
|
+
seen.add(p);
|
|
66
|
+
at = p;
|
|
67
|
+
}
|
|
68
|
+
return at;
|
|
69
|
+
};
|
|
70
|
+
// Compositions by fit state: the ladder and its proposals ask for some
|
|
71
|
+
// twice.
|
|
72
|
+
const memo = new Map();
|
|
73
|
+
let runs = 0;
|
|
74
|
+
const keyOf = (tpl, fit) => JSON.stringify([tpl.id, [...fit.patches], [...fit.text], [...fit.pictures], [...fit.yieldAll]]);
|
|
75
|
+
const compose = (fit, tpl = template) => {
|
|
76
|
+
const key = keyOf(tpl, fit);
|
|
77
|
+
const known = memo.get(key);
|
|
78
|
+
if (known)
|
|
79
|
+
return known;
|
|
80
|
+
runs++;
|
|
81
|
+
const c = composePage(input, patchTemplate(tpl, fit.patches), fit);
|
|
82
|
+
memo.set(key, c);
|
|
83
|
+
return c;
|
|
84
|
+
};
|
|
85
|
+
const frameIn = (c, name) => c.frames.find((f) => f.name === name);
|
|
86
|
+
const overflows = (c, name) => (frameIn(c, name)?.overflowPx ?? 0) > 0.5;
|
|
87
|
+
// The ladder, frame by frame in template order (ties break there).
|
|
88
|
+
let fit = NO_FIT;
|
|
89
|
+
let cur = compose(fit);
|
|
90
|
+
const tried = new Map();
|
|
91
|
+
const patched = (f, name, patch) => ({ ...f, patches: new Map([...f.patches, [name, { ...f.patches.get(name), ...patch }]]) });
|
|
92
|
+
const withText = (f, head, s) => ({ ...f, text: new Map([...f.text, [head, s]]) });
|
|
93
|
+
const withPictures = (f, heads, s, by) => {
|
|
94
|
+
const pictures = new Map(f.pictures);
|
|
95
|
+
for (const h of heads)
|
|
96
|
+
pictures.set(h, { scale: s, by });
|
|
97
|
+
return { ...f, pictures };
|
|
98
|
+
};
|
|
99
|
+
/** Take `next` when it makes `name` fit; else keep it anyway (each step
|
|
100
|
+
* builds on the ones before it at their limit) and say so. */
|
|
101
|
+
const attempt = (next, name) => {
|
|
102
|
+
fit = next;
|
|
103
|
+
cur = compose(fit);
|
|
104
|
+
return !overflows(cur, name);
|
|
105
|
+
};
|
|
106
|
+
/** The largest scale among `steps` (descending) at which `name` fits,
|
|
107
|
+
* checked at the lowest first, then by bisection; adopted. */
|
|
108
|
+
const searchScale = (steps, make, name) => {
|
|
109
|
+
if (steps.length === 0)
|
|
110
|
+
return false;
|
|
111
|
+
if (!attempt(make(steps[steps.length - 1]), name))
|
|
112
|
+
return false;
|
|
113
|
+
let lo = 0;
|
|
114
|
+
let hi = steps.length - 1; // fits at hi
|
|
115
|
+
while (lo < hi && runs < COMPOSE_BUDGET) {
|
|
116
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
117
|
+
if (!overflows(compose(make(steps[mid])), name))
|
|
118
|
+
hi = mid;
|
|
119
|
+
else
|
|
120
|
+
lo = mid + 1;
|
|
121
|
+
}
|
|
122
|
+
fit = make(steps[hi]);
|
|
123
|
+
cur = compose(fit);
|
|
124
|
+
return true;
|
|
125
|
+
};
|
|
126
|
+
/** Story heads of the frame and of its group's frames whose content holds
|
|
127
|
+
* a picture. */
|
|
128
|
+
const pictureHeads = (frame) => {
|
|
129
|
+
const g = groupOf.get(frame.name);
|
|
130
|
+
const names = g ? [frame.name, ...g.frames.filter((m) => m !== frame.name)] : [frame.name];
|
|
131
|
+
const heads = [];
|
|
132
|
+
for (const n of names) {
|
|
133
|
+
const head = headOf(n);
|
|
134
|
+
const story = input.stories.get(head);
|
|
135
|
+
if (!story || heads.includes(head))
|
|
136
|
+
continue;
|
|
137
|
+
for (let i = story.startIdx + 1; i < story.plan.endIdx; i++) {
|
|
138
|
+
if (input.pass.contentBlocks[i]?.type === 'resourceBlock') {
|
|
139
|
+
heads.push(head);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return heads;
|
|
145
|
+
};
|
|
146
|
+
/** The frame's story sets text (not only pictures): its text can
|
|
147
|
+
* shrink. */
|
|
148
|
+
const holdsText = (frame) => {
|
|
149
|
+
const story = input.stories.get(headOf(frame.name));
|
|
150
|
+
if (!story)
|
|
151
|
+
return false;
|
|
152
|
+
for (let i = story.startIdx + 1; i < story.plan.endIdx; i++) {
|
|
153
|
+
const b = input.pass.contentBlocks[i];
|
|
154
|
+
if (b && b.type !== 'resourceBlock' && b.type !== 'directive' && !isMarkerBlock(b))
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
};
|
|
159
|
+
const movesOthers = (frame) => {
|
|
160
|
+
const g = groupOf.get(frame.name);
|
|
161
|
+
if (g && g.frames.filter((m) => byName.has(m)).length > 1)
|
|
162
|
+
return true;
|
|
163
|
+
return template.frames.some((f) => f.name !== frame.name && !f.grid && anchorTarget(f.placement) === frame.name);
|
|
164
|
+
};
|
|
165
|
+
/** `free` applies: a fixed height with room up to a higher `maxHeight`. */
|
|
166
|
+
const growPatch = (frame) => {
|
|
167
|
+
const h = fixedHeight(frame);
|
|
168
|
+
if (!h || !frame.maxHeight)
|
|
169
|
+
return undefined;
|
|
170
|
+
if (dimensionToPx(frame.maxHeight, dpi, input.emPx) <= dimensionToPx(h, dpi, input.emPx) + 0.5)
|
|
171
|
+
return undefined;
|
|
172
|
+
const min = frame.minHeight && dimensionToPx(frame.minHeight, dpi, input.emPx) > dimensionToPx(h, dpi, input.emPx) ? frame.minHeight : h;
|
|
173
|
+
return { height: 'auto', minHeight: min };
|
|
174
|
+
};
|
|
175
|
+
for (const frame of template.frames) {
|
|
176
|
+
const name = frame.name;
|
|
177
|
+
if (!overflows(cur, name) || runs >= COMPOSE_BUDGET)
|
|
178
|
+
continue;
|
|
179
|
+
const steps = [];
|
|
180
|
+
tried.set(name, steps);
|
|
181
|
+
// 1. Free space: a fixed height grows to its maximum ('auto' heights
|
|
182
|
+
// already take the room their area and group leave).
|
|
183
|
+
steps.push('free');
|
|
184
|
+
const grow = growPatch(frame);
|
|
185
|
+
if (grow && attempt(patched(fit, name, grow), name))
|
|
186
|
+
continue;
|
|
187
|
+
// 2. Push siblings: the group moved the frames after it and the frames
|
|
188
|
+
// that shrink gave space back when it was laid.
|
|
189
|
+
if (movesOthers(frame))
|
|
190
|
+
steps.push('push');
|
|
191
|
+
// 3. Pictures of the frame and its group, down to imageMinScale.
|
|
192
|
+
const heads = frame.fit.scaleImages ? pictureHeads(frame) : [];
|
|
193
|
+
if (heads.length > 0 && frame.fit.imageMinScale < 1) {
|
|
194
|
+
steps.push('scaleImages');
|
|
195
|
+
if (searchScale(scaleSteps(frame.fit.imageMinScale), (s) => withPictures(fit, heads, s, name), name))
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// 4. Redistribute: every other frame of the group gives space (or
|
|
199
|
+
// width) back, down to its minimum or its content.
|
|
200
|
+
const g = groupOf.get(name);
|
|
201
|
+
if (g && g.frames.filter((m) => byName.has(m)).length > 1) {
|
|
202
|
+
steps.push('redistribute');
|
|
203
|
+
if (attempt({ ...fit, yieldAll: new Set([...fit.yieldAll, g.name]) }, name))
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
// 5. The frame's text (its whole story), down to fit.minScale.
|
|
207
|
+
if (frame.fit.shrinkText && frame.fit.minScale < 1 && holdsText(frame)) {
|
|
208
|
+
steps.push('shrinkText');
|
|
209
|
+
const head = headOf(name);
|
|
210
|
+
if (searchScale(scaleSteps(frame.fit.minScale), (s) => withText(fit, head, s), name))
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// 6. Continuation on another page (`overflow.continueIn: 'next-page'`
|
|
214
|
+
// or `'template:<id>'`) is #859's: the step is reported, the content
|
|
215
|
+
// still stops here.
|
|
216
|
+
if (continueOnOtherPage(frame))
|
|
217
|
+
steps.push('continue');
|
|
218
|
+
}
|
|
219
|
+
// 7. Warn, with what would make each frame that still overflows fit.
|
|
220
|
+
const warnings = [...(cur.notes ?? [])];
|
|
221
|
+
const templateAttr = fence.attrSources.template;
|
|
222
|
+
for (const frame of template.frames) {
|
|
223
|
+
const vdt = frameIn(cur, frame.name);
|
|
224
|
+
if (!vdt || vdt.overflowPx <= 0.5)
|
|
225
|
+
continue;
|
|
226
|
+
const overflowMm = Math.round((vdt.overflowPx / mmPx) * 10) / 10;
|
|
227
|
+
const proposals = overflowProposals({
|
|
228
|
+
frame,
|
|
229
|
+
vdt,
|
|
230
|
+
cur,
|
|
231
|
+
fit,
|
|
232
|
+
compose,
|
|
233
|
+
overflows,
|
|
234
|
+
headOf,
|
|
235
|
+
withText,
|
|
236
|
+
patched,
|
|
237
|
+
groupOf,
|
|
238
|
+
byName,
|
|
239
|
+
mmPx,
|
|
240
|
+
dpi,
|
|
241
|
+
emPx: input.emPx,
|
|
242
|
+
alternatives: input.alternatives,
|
|
243
|
+
templateSource: templateAttr ? { sourceStart: templateAttr.sourceStart + input.bodyOffset, sourceEnd: templateAttr.sourceEnd + input.bodyOffset } : undefined,
|
|
244
|
+
budgetLeft: () => runs < COMPOSE_BUDGET,
|
|
245
|
+
holdsText: holdsText(frame),
|
|
246
|
+
});
|
|
247
|
+
warnings.push({
|
|
248
|
+
kind: 'frameOverflow',
|
|
249
|
+
template: template.id,
|
|
250
|
+
frame: frame.name,
|
|
251
|
+
fenceIndex: fi,
|
|
252
|
+
overflowMm,
|
|
253
|
+
configPath: frame.configPath,
|
|
254
|
+
...(input.locale ? { locale: input.locale } : {}),
|
|
255
|
+
stepsTried: tried.get(frame.name) ?? ['free'],
|
|
256
|
+
scale: vdt.scale,
|
|
257
|
+
...(vdt.imageScale !== undefined ? { imageScale: vdt.imageScale } : {}),
|
|
258
|
+
proposals,
|
|
259
|
+
...(vdt.sourceStart !== undefined ? { sourceStart: vdt.sourceStart, sourceEnd: vdt.sourceEnd } : {}),
|
|
260
|
+
pageIndex: input.page.index,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const { frames, blocks, log, laid, fitted } = cur;
|
|
264
|
+
// Template order on the page record.
|
|
265
|
+
const order = new Map(template.frames.map((f, i) => [f.name, i]));
|
|
266
|
+
frames.sort((a, b) => order.get(a.name) - order.get(b.name));
|
|
267
|
+
const contentBlocks = input.pass.contentBlocks;
|
|
268
|
+
const composed = {
|
|
269
|
+
template: template.id,
|
|
270
|
+
...(template.variant ? { variant: template.variant } : {}),
|
|
271
|
+
fenceIndex: fi,
|
|
272
|
+
role: template.role,
|
|
273
|
+
attrs: { ...fence.attrs },
|
|
274
|
+
attrSources: Object.fromEntries(Object.entries(fence.attrSources).map(([k, v]) => [k, { sourceStart: v.sourceStart + input.bodyOffset, sourceEnd: v.sourceEnd + input.bodyOffset }])),
|
|
275
|
+
frames,
|
|
276
|
+
log,
|
|
277
|
+
configPath: template.configPath,
|
|
278
|
+
sourceStart: contentBlocks[input.startIdx].sourceStart + input.bodyOffset,
|
|
279
|
+
sourceEnd: contentBlocks[input.endIdx].sourceEnd + input.bodyOffset,
|
|
280
|
+
};
|
|
281
|
+
return { composed, blocks, warnings, laid, ...(fitted.size > 0 ? { fitted } : {}) };
|
|
282
|
+
}
|
|
283
|
+
/** The frame sends what does not fit to another page (#859). */
|
|
284
|
+
function continueOnOtherPage(frame) {
|
|
285
|
+
const to = frame.overflow.continueIn;
|
|
286
|
+
return to === 'next-page' || continueInTemplate(to) !== undefined;
|
|
287
|
+
}
|
|
288
|
+
const mmDim = (value) => ({ value, unit: 'mm' });
|
|
289
|
+
/** What would make `frame`'s content fit, each value worked out from the
|
|
290
|
+
* shortfall and, where the page can tell, tried on it (DESIGN §5.7). */
|
|
291
|
+
function overflowProposals(c) {
|
|
292
|
+
const { frame, vdt, cur, fit, mmPx } = c;
|
|
293
|
+
const name = frame.name;
|
|
294
|
+
const out = [];
|
|
295
|
+
const path = (key) => frame.fieldPaths[key] ?? `${frame.configPath}/${key}`;
|
|
296
|
+
const fits = (next, tpl) => c.budgetLeft() && !c.overflows(c.compose(next, tpl), name);
|
|
297
|
+
// More shrink: the largest text scale, in 2.5 % steps, at which it fits.
|
|
298
|
+
const head = c.headOf(name);
|
|
299
|
+
const from = frame.fit.shrinkText ? Math.min(frame.fit.minScale, vdt.scale) : 1;
|
|
300
|
+
const steps = scaleSteps(PROPOSAL_MIN_SCALE, from);
|
|
301
|
+
if (c.holdsText && steps.length > 0 && fits(c.withText(fit, head, steps[steps.length - 1]))) {
|
|
302
|
+
let lo = 0;
|
|
303
|
+
let hi = steps.length - 1;
|
|
304
|
+
while (lo < hi && c.budgetLeft()) {
|
|
305
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
306
|
+
if (fits(c.withText(fit, head, steps[mid])))
|
|
307
|
+
hi = mid;
|
|
308
|
+
else
|
|
309
|
+
lo = mid + 1;
|
|
310
|
+
}
|
|
311
|
+
out.push({
|
|
312
|
+
kind: 'allowShrink',
|
|
313
|
+
path: path('fit/minScale'),
|
|
314
|
+
value: steps[hi],
|
|
315
|
+
...(frame.fit.shrinkText ? {} : { also: [{ path: path('fit/shrinkText'), value: true }] }),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
// A higher maximum: the frame's height plus the shortfall, in whole mm.
|
|
319
|
+
const fixed = fixedHeight(frame);
|
|
320
|
+
if (frame.maxHeight || fixed) {
|
|
321
|
+
const want = Math.ceil((vdt.rect.height + vdt.overflowPx) / mmPx - 1e-6);
|
|
322
|
+
const patch = { maxHeight: mmDim(want), ...(fixed ? { height: 'auto', minHeight: fixed } : {}) };
|
|
323
|
+
if (fits(c.patched(fit, name, patch)))
|
|
324
|
+
out.push({ kind: 'raiseMax', path: path('maxHeight'), value: mmDim(want) });
|
|
325
|
+
}
|
|
326
|
+
// A lower minimum on a frame of its group that stands at its minimum
|
|
327
|
+
// with less content than that: lowered by the shortfall, not below its
|
|
328
|
+
// content.
|
|
329
|
+
const g = c.groupOf.get(name);
|
|
330
|
+
if (g) {
|
|
331
|
+
for (const other of g.frames) {
|
|
332
|
+
const f = c.byName.get(other);
|
|
333
|
+
if (!f || other === name || f.pin || !f.minHeight)
|
|
334
|
+
continue;
|
|
335
|
+
const box = cur.frames.find((x) => x.name === other);
|
|
336
|
+
if (!box)
|
|
337
|
+
continue;
|
|
338
|
+
const minPx = dimensionToPx(f.minHeight, c.dpi, c.emPx);
|
|
339
|
+
const need = cur.needs.get(other)?.() ?? 0;
|
|
340
|
+
if (Math.abs(box.rect.height - minPx) > 0.5 || need >= minPx - mmPx)
|
|
341
|
+
continue;
|
|
342
|
+
const lowered = Math.max(Math.ceil(need / mmPx - 1e-6), Math.floor((minPx - vdt.overflowPx) / mmPx + 1e-6));
|
|
343
|
+
if (lowered * mmPx >= minPx - 0.5)
|
|
344
|
+
continue;
|
|
345
|
+
if (fits(c.patched(fit, other, { minHeight: mmDim(lowered) }))) {
|
|
346
|
+
out.push({ kind: 'lowerMin', path: f.fieldPaths.minHeight ?? `${f.configPath}/minHeight`, value: mmDim(lowered), frame: other });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Continue on the next page (#859 lays it out).
|
|
351
|
+
if (!frame.overflow.continueIn)
|
|
352
|
+
out.push({ kind: 'continueNextPage', path: path('overflow/continueIn'), value: 'next-page' });
|
|
353
|
+
// Another template of the book with a frame of this name, where the
|
|
354
|
+
// content fits as it is.
|
|
355
|
+
for (const alt of c.alternatives?.() ?? []) {
|
|
356
|
+
if (!c.budgetLeft())
|
|
357
|
+
break;
|
|
358
|
+
if (!alt.frames.some((f) => f.name === name))
|
|
359
|
+
continue;
|
|
360
|
+
const trial = c.compose(NO_FIT, alt);
|
|
361
|
+
const laidThere = trial.frames.find((f) => f.name === name);
|
|
362
|
+
if (laidThere && laidThere.blockIds.length > 0 && laidThere.overflowPx <= 0.5) {
|
|
363
|
+
out.push({ kind: 'useTemplate', value: alt.id, ...(c.templateSource ?? {}) });
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Shorter text: the lines that do not fit, at the frame's characters a
|
|
368
|
+
// line.
|
|
369
|
+
const lines = cur.blocks.filter((b) => vdt.blockIds.includes(b.id) && b.type !== 'resource').flatMap((b) => b.lines.filter((l) => l.text.length > 0));
|
|
370
|
+
if (lines.length > 0) {
|
|
371
|
+
// Characters of a full line: the lines that do not end their paragraph.
|
|
372
|
+
const full = lines.filter((l) => !l.isLastLine);
|
|
373
|
+
const sample = full.length > 0 ? full : lines;
|
|
374
|
+
const perLine = sample.reduce((n, l) => n + l.text.length, 0) / sample.length;
|
|
375
|
+
const lineH = (lines.reduce((h, l) => h + l.bbox.height, 0) / lines.length) * vdt.scale;
|
|
376
|
+
if (lineH > 0) {
|
|
377
|
+
const over = Math.ceil(vdt.overflowPx / lineH - 1e-6);
|
|
378
|
+
out.push({ kind: 'shortenText', value: Math.ceil(over * perLine), ...(vdt.sourceStart !== undefined ? { sourceStart: vdt.sourceStart, sourceEnd: vdt.sourceEnd } : {}) });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
/** A frame style whose box keeps its drawn size when the frame's text is
|
|
384
|
+
* set at `scale` (#858): the frame is laid out `1 / scale` as large and
|
|
385
|
+
* painted at `scale`, so its padding, border, corner radius, stripe and
|
|
386
|
+
* tab are laid out `1 / scale` as large to come out as drawn. Only the
|
|
387
|
+
* text and what it holds shrink: padding that keeps room for the photos
|
|
388
|
+
* set over a box stays that room. */
|
|
389
|
+
export function keepChromeAt(style, scale) {
|
|
390
|
+
if (scale === 1)
|
|
391
|
+
return style;
|
|
392
|
+
const d = (v) => ({ ...v, value: v.value / scale });
|
|
393
|
+
const p = style.padding;
|
|
394
|
+
return {
|
|
395
|
+
...style,
|
|
396
|
+
padding: { top: d(p.top), right: d(p.right), bottom: d(p.bottom), left: d(p.left) },
|
|
397
|
+
border: { ...style.border, width: d(style.border.width) },
|
|
398
|
+
borderRadius: d(style.borderRadius),
|
|
399
|
+
stripe: { ...style.stripe, width: d(style.stripe.width) },
|
|
400
|
+
...(style.label
|
|
401
|
+
? { label: { ...style.label, height: d(style.label.height), paddingX: d(style.label.paddingX), offset: d(style.label.offset), inset: d(style.label.inset), rule: { ...style.label.rule, width: d(style.label.rule.width) } } }
|
|
402
|
+
: {}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** Lay out every frame of the page once, with the reflow ladder's `fit`. */
|
|
406
|
+
function composePage(input, template, fit) {
|
|
407
|
+
const { pass, fence, dpi } = input;
|
|
408
|
+
const fi = fence.fenceIndex;
|
|
409
|
+
const frameIdOf = (name) => frameElementId(fi, name);
|
|
16
410
|
const byName = new Map(template.frames.map((f) => [f.name, f]));
|
|
17
411
|
const prev = new Map();
|
|
18
412
|
for (const f of template.frames) {
|
|
@@ -30,11 +424,26 @@ export function layoutComposedPage(input) {
|
|
|
30
424
|
}
|
|
31
425
|
return at;
|
|
32
426
|
};
|
|
427
|
+
/** The text scale of a frame's story (#858). */
|
|
428
|
+
const textScale = (frame) => fit.text.get(headOf(frame.name)) ?? 1;
|
|
429
|
+
/** The style a frame is laid out in: its box as drawn at any text scale. */
|
|
430
|
+
const styleOf = (frame) => keepChromeAt(input.styleOf(frame), textScale(frame));
|
|
431
|
+
/** The scale of a frame's pictures (#858), below 1, or undefined. */
|
|
432
|
+
const pictureScale = (frame) => fit.pictures.get(headOf(frame.name))?.scale;
|
|
433
|
+
/** Rectangles in a frame's box coordinates, in its laid-out (scaled)
|
|
434
|
+
* coordinates. */
|
|
435
|
+
const unscale = (rs, s) => rs && s !== 1
|
|
436
|
+
? rs.map((r) => ({ x: r.x / s, y: r.y / s, width: r.width / s, height: r.height / s, ...(r.contour ? { contour: scaleContourAbout(r.contour, 0, 0, 1 / s) } : {}) }))
|
|
437
|
+
: rs;
|
|
33
438
|
/** Place and fill every frame. `seed`: the boxes of the frames that push
|
|
34
439
|
* text, from a first run, so a frame placed before one that pushes it
|
|
35
440
|
* (the photo anchored to its box) still runs round it. */
|
|
36
|
-
const run = (seed) => {
|
|
441
|
+
const run = (seed, seedShapes) => {
|
|
37
442
|
const pushRects = new Map();
|
|
443
|
+
/** The shapes of the frames that push with their pictures' contour (#883). */
|
|
444
|
+
const pushShapes = new Map();
|
|
445
|
+
/** Pictures of a contour push whose shape is not known (#883). */
|
|
446
|
+
const notes = [];
|
|
38
447
|
const states = new Map();
|
|
39
448
|
const stateOf = (frame) => {
|
|
40
449
|
const head = headOf(frame.name);
|
|
@@ -47,7 +456,7 @@ export function layoutComposedPage(input) {
|
|
|
47
456
|
return undefined;
|
|
48
457
|
const st = {
|
|
49
458
|
story,
|
|
50
|
-
L: makeCalloutLayouter(pass, story.startIdx, story.plan,
|
|
459
|
+
L: makeCalloutLayouter(pass, story.startIdx, story.plan, styleOf(frame)),
|
|
51
460
|
cut: CUT_START,
|
|
52
461
|
pieces: 0,
|
|
53
462
|
headBlockId: frameIdOf(head),
|
|
@@ -82,10 +491,15 @@ export function layoutComposedPage(input) {
|
|
|
82
491
|
const frames = [];
|
|
83
492
|
const vdtByName = new Map();
|
|
84
493
|
const blocks = [];
|
|
85
|
-
const warnings = [];
|
|
86
494
|
const overflowLog = [];
|
|
495
|
+
const fitLog = [];
|
|
496
|
+
const needs = new Map();
|
|
497
|
+
/** What `fitBox` did to each frame (#861), last placement wins. */
|
|
498
|
+
const fitted = new Map();
|
|
87
499
|
const laid = new Set();
|
|
88
|
-
const pushed = seed
|
|
500
|
+
const pushed = seed
|
|
501
|
+
? [...seed].map(([name, box]) => ({ name, box, ...(seedShapes?.get(name) ? { contour: seedShapes.get(name) } : {}) }))
|
|
502
|
+
: [];
|
|
89
503
|
/** The rectangles of the pushing frames placed so far that `rect`
|
|
90
504
|
* meets, in the frame block's coordinates. */
|
|
91
505
|
const exclusionsFor = (frame, rect, open) => {
|
|
@@ -94,11 +508,17 @@ export function layoutComposedPage(input) {
|
|
|
94
508
|
for (const p of pushed) {
|
|
95
509
|
if (p.name === frame.name)
|
|
96
510
|
continue;
|
|
97
|
-
|
|
511
|
+
// A contour (#883) pushes with its shape, its margin round it.
|
|
512
|
+
const cb = p.contour ? contourBounds(p.contour) : undefined;
|
|
513
|
+
const b = cb ?? p.box;
|
|
514
|
+
const e = cb ? 0 : g;
|
|
98
515
|
const below = open ? true : b.y < rect.y + rect.height;
|
|
99
|
-
if (b.x -
|
|
516
|
+
if (b.x - e >= rect.x + rect.width || b.x + b.width + e <= rect.x || !below || b.y + b.height + e <= rect.y)
|
|
100
517
|
continue;
|
|
101
|
-
out.push({
|
|
518
|
+
out.push({
|
|
519
|
+
x: b.x - e - rect.x, y: b.y - e - rect.y, width: b.width + 2 * e, height: b.height + 2 * e,
|
|
520
|
+
...(cb && p.contour ? { contour: shiftContour(p.contour, -rect.x, -rect.y) } : {}),
|
|
521
|
+
});
|
|
102
522
|
}
|
|
103
523
|
return out.length > 0 ? out : undefined;
|
|
104
524
|
};
|
|
@@ -107,7 +527,7 @@ export function layoutComposedPage(input) {
|
|
|
107
527
|
// Each frame of a chain sets the story in its own style: the cuts are
|
|
108
528
|
// positions in the story's blocks, the same in any.
|
|
109
529
|
if (st)
|
|
110
|
-
st.L = makeCalloutLayouter(pass, st.story.startIdx, st.story.plan,
|
|
530
|
+
st.L = makeCalloutLayouter(pass, st.story.startIdx, st.story.plan, styleOf(frame));
|
|
111
531
|
// A frame continues its story only once the frame before it in the
|
|
112
532
|
// chain was filled (a frame placed before it, through an anchor, starts
|
|
113
533
|
// empty).
|
|
@@ -119,9 +539,13 @@ export function layoutComposedPage(input) {
|
|
|
119
539
|
return { frame, st, before, holds, continuation, blockId };
|
|
120
540
|
};
|
|
121
541
|
/** The content's height at `width`, running round the pushing frames
|
|
122
|
-
* below the top of `at` (the frame's box as first placed).
|
|
542
|
+
* below the top of `at` (the frame's box as first placed). A frame
|
|
543
|
+
* whose text the ladder scales (#858) is laid out `1 / scale` as
|
|
544
|
+
* wide and is `scale` as tall as that. */
|
|
123
545
|
const naturalOf = (p, at) => {
|
|
124
546
|
const memo = new Map();
|
|
547
|
+
const s = textScale(p.frame);
|
|
548
|
+
const pic = pictureScale(p.frame);
|
|
125
549
|
return (width) => {
|
|
126
550
|
if (!p.holds)
|
|
127
551
|
return 0;
|
|
@@ -129,10 +553,12 @@ export function layoutComposedPage(input) {
|
|
|
129
553
|
const known = memo.get(key);
|
|
130
554
|
if (known !== undefined)
|
|
131
555
|
return known;
|
|
132
|
-
const exclusions = at ? exclusionsFor(p.frame, { ...at, width }, true) : undefined;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
556
|
+
const exclusions = at ? unscale(exclusionsFor(p.frame, { ...at, width }, true), s) : undefined;
|
|
557
|
+
const opts = { ...(exclusions ? { exclusions } : {}), ...(pic !== undefined ? { pictureScale: pic } : {}) };
|
|
558
|
+
const r = p.st.L.layoutRange(p.st.cut, p.st.L.end, width / s, p.blockId, p.continuation, input.mirrored, opts.exclusions || opts.pictureScale !== undefined ? opts : undefined);
|
|
559
|
+
const h = r.totalHeight * s;
|
|
560
|
+
memo.set(key, h);
|
|
561
|
+
return h;
|
|
136
562
|
};
|
|
137
563
|
};
|
|
138
564
|
/** `placeBox`, then the box the page lets the frame take (`fitBox`:
|
|
@@ -143,10 +569,18 @@ export function layoutComposedPage(input) {
|
|
|
143
569
|
const r = placeBox(placeable, template, ctx, refs, nat);
|
|
144
570
|
if (!input.fitBox)
|
|
145
571
|
return r;
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
572
|
+
const keep = (placed) => {
|
|
573
|
+
const kept = input.fitBox(frame, placed);
|
|
574
|
+
if (sameRect(kept, placed))
|
|
575
|
+
fitted.delete(frame.name);
|
|
576
|
+
else
|
|
577
|
+
fitted.set(frame.name, { placed, kept });
|
|
578
|
+
return kept;
|
|
579
|
+
};
|
|
580
|
+
const first = keep(r);
|
|
581
|
+
if (Math.abs(first.width - r.width) < 0.01)
|
|
582
|
+
return first;
|
|
583
|
+
return keep(placeBox(placeable, template, ctx, refs, () => nat(first.width)));
|
|
150
584
|
};
|
|
151
585
|
/** A frame's height as its template declares it: its `minHeight` where
|
|
152
586
|
* the height follows the content, else the content's (`natural`). */
|
|
@@ -177,10 +611,13 @@ export function layoutComposedPage(input) {
|
|
|
177
611
|
return rect;
|
|
178
612
|
};
|
|
179
613
|
/** Lay a group: its frames' boxes as the template declares them and as
|
|
180
|
-
* their content needs (`composed/groups.ts`).
|
|
614
|
+
* their content needs (`composed/groups.ts`). With the ladder's
|
|
615
|
+
* `redistribute` (#858) every frame gives space back, down to its
|
|
616
|
+
* minimum or its content, whether it `shrink`s or not. */
|
|
181
617
|
const placeGroup = (group, members) => {
|
|
182
618
|
const own = groupPlaceable(group);
|
|
183
619
|
const mmPx = dpi / MM_PER_IN;
|
|
620
|
+
const yieldAll = fit.yieldAll.has(group.name);
|
|
184
621
|
const preps = members.map(prepare);
|
|
185
622
|
const solve = (mode, naturals) => {
|
|
186
623
|
const refs = mode === 'final' ? refOf : declaredRefOf;
|
|
@@ -200,12 +637,13 @@ export function layoutComposedPage(input) {
|
|
|
200
637
|
const fill = f.height === 'fill';
|
|
201
638
|
const minH = f.minHeight ? dimensionToPx(f.minHeight, dpi, input.emPx) : 0;
|
|
202
639
|
const want = fill ? minH : auto ? (mode === 'final' ? natural(box.width) : declaredNatural(f, natural)(box.width)) : box.height;
|
|
203
|
-
|
|
640
|
+
const max = f.maxHeight ? dimensionToPx(f.maxHeight, dpi, input.emPx) : Infinity;
|
|
641
|
+
const item = {
|
|
204
642
|
name: f.name,
|
|
205
643
|
own: box,
|
|
206
644
|
want,
|
|
207
645
|
min: minH,
|
|
208
|
-
max
|
|
646
|
+
max,
|
|
209
647
|
minWidth: f.minWidth ? dimensionToPx(f.minWidth, dpi, input.emPx) : 0,
|
|
210
648
|
maxWidth: f.maxWidth ? dimensionToPx(f.maxWidth, dpi, input.emPx) : Infinity,
|
|
211
649
|
grow: fill ? Math.max(1, f.grow) : f.grow,
|
|
@@ -215,6 +653,17 @@ export function layoutComposedPage(input) {
|
|
|
215
653
|
fixedHeight: !auto && !fill,
|
|
216
654
|
need: natural,
|
|
217
655
|
};
|
|
656
|
+
if (yieldAll && mode === 'final' && !f.pin) {
|
|
657
|
+
// Down to its minimum, never under what its content needs, never
|
|
658
|
+
// over the height it has.
|
|
659
|
+
const height = Math.min(Math.max(want, minH), max);
|
|
660
|
+
item.shrink = f.shrink > 0 ? f.shrink : 1;
|
|
661
|
+
// (A row's frames give width, each down to the narrowest width
|
|
662
|
+
// its content still fits: their heights stay the row's.)
|
|
663
|
+
if (group.kind === 'stack')
|
|
664
|
+
item.min = Math.min(height, Math.max(minH, natural(box.width)));
|
|
665
|
+
}
|
|
666
|
+
return item;
|
|
218
667
|
});
|
|
219
668
|
const firstFree = items.find((it) => !it.pin) ?? items[0];
|
|
220
669
|
const start = firstFree?.own ?? input.areas.area;
|
|
@@ -257,41 +706,51 @@ export function layoutComposedPage(input) {
|
|
|
257
706
|
groupDeclared.set(group.name, decl.box);
|
|
258
707
|
return fin.rects;
|
|
259
708
|
};
|
|
260
|
-
/** Pour a frame's content into its box and record the frame.
|
|
709
|
+
/** Pour a frame's content into its box and record the frame. A frame
|
|
710
|
+
* whose text the ladder scales (#858) is laid out in a box `1 / scale`
|
|
711
|
+
* as large, painted at `scale` (`frameMatrix`). */
|
|
261
712
|
const fill = (p, rect) => {
|
|
262
713
|
const { frame, st, holds, continuation, blockId, before } = p;
|
|
263
|
-
const style =
|
|
714
|
+
const style = styleOf(frame);
|
|
715
|
+
const s = textScale(frame);
|
|
716
|
+
const pic = pictureScale(frame);
|
|
717
|
+
const laidW = rect.width / s;
|
|
718
|
+
const laidH = rect.height / s;
|
|
264
719
|
let result;
|
|
265
720
|
let overflowPx = 0;
|
|
266
721
|
const from = st?.cut ?? CUT_START;
|
|
267
722
|
if (holds) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
723
|
+
// What its content needs at its width, measured only when a
|
|
724
|
+
// proposal asks (`lowerMin`), from where the frame starts.
|
|
725
|
+
const width = rect.width;
|
|
726
|
+
const startAt = { ...p, st: { ...st, cut: from } };
|
|
727
|
+
let known;
|
|
728
|
+
needs.set(frame.name, () => (known ??= naturalOf(startAt)(width)));
|
|
729
|
+
const exclusions = unscale(exclusionsFor(frame, rect, false), s);
|
|
730
|
+
const extra = { ...(exclusions ? { exclusions } : {}), ...(pic !== undefined ? { pictureScale: pic } : {}) };
|
|
731
|
+
const opts = { boxHeight: laidH, verticalAlign: frame.verticalAlign, ...extra };
|
|
732
|
+
const whole = st.L.layoutRange(from, st.L.end, laidW, blockId, continuation, input.mirrored, opts);
|
|
274
733
|
const nextName = nextFrameOf(frame);
|
|
275
734
|
const nextFrame = nextName !== undefined ? byName.get(nextName) : undefined;
|
|
276
735
|
if ((whole.overflowPx ?? 0) > 0.5 && nextFrame && !vdtByName.has(nextFrame.name)) {
|
|
277
736
|
// The rest goes on in the next frame of the chain.
|
|
278
737
|
const minLines = Math.max(1, style.splitMinLines);
|
|
279
|
-
const frag = splitCalloutFragment(pass, st.L, from,
|
|
738
|
+
const frag = splitCalloutFragment(pass, st.L, from, laidW, laidH, blockId, continuation, minLines, input.mirrored, extra.exclusions || extra.pictureScale !== undefined ? extra : undefined);
|
|
280
739
|
const to = frag?.to ?? from;
|
|
281
740
|
result = frag
|
|
282
|
-
? st.L.layoutRange(from, to,
|
|
741
|
+
? st.L.layoutRange(from, to, laidW, blockId, continuation, input.mirrored, opts)
|
|
283
742
|
: undefined;
|
|
284
743
|
st.cut = to;
|
|
285
744
|
}
|
|
286
745
|
else {
|
|
287
746
|
result = whole;
|
|
288
|
-
overflowPx = whole.overflowPx ?? 0;
|
|
747
|
+
overflowPx = (whole.overflowPx ?? 0) * s;
|
|
289
748
|
st.cut = st.L.end;
|
|
290
749
|
}
|
|
291
750
|
}
|
|
292
751
|
else if (st && frame.style) {
|
|
293
752
|
// A styled frame with nothing (left) to hold still draws its box.
|
|
294
|
-
result = st.L.layoutRange(st.L.end, st.L.end,
|
|
753
|
+
result = st.L.layoutRange(st.L.end, st.L.end, laidW, blockId, true, input.mirrored, { boxHeight: laidH });
|
|
295
754
|
}
|
|
296
755
|
const vdt = {
|
|
297
756
|
id: frameIdOf(frame.name),
|
|
@@ -300,7 +759,8 @@ export function layoutComposedPage(input) {
|
|
|
300
759
|
rotate: frame.rotate,
|
|
301
760
|
layer: frame.layer,
|
|
302
761
|
...(frame.style ? { styleId: frame.style } : {}),
|
|
303
|
-
scale:
|
|
762
|
+
scale: s,
|
|
763
|
+
...(pic !== undefined && holds ? { imageScale: pic } : {}),
|
|
304
764
|
overflowPx,
|
|
305
765
|
blockIds: [],
|
|
306
766
|
configPath: frame.configPath,
|
|
@@ -321,26 +781,36 @@ export function layoutComposedPage(input) {
|
|
|
321
781
|
st.pieces++;
|
|
322
782
|
st.lastFrameId = vdt.id;
|
|
323
783
|
laid.add(headOf(frame.name));
|
|
784
|
+
// What the ladder changed in the frame's content (#858).
|
|
785
|
+
const pictures = fit.pictures.get(headOf(frame.name));
|
|
786
|
+
if (pictures && result.children.some((b) => b.type === 'resource')) {
|
|
787
|
+
fitLog.push({ frame: frame.name, kind: 'imageScaled', scale: pictures.scale, ...(pictures.by !== frame.name ? { cause: pictures.by } : {}) });
|
|
788
|
+
}
|
|
789
|
+
if (s !== 1 && holds)
|
|
790
|
+
fitLog.push({ frame: frame.name, kind: 'textScaled', scale: s });
|
|
324
791
|
}
|
|
325
792
|
if (overflowPx > 0.5) {
|
|
326
793
|
const overflowMm = Math.round((overflowPx / dpi) * MM_PER_IN * 10) / 10;
|
|
327
794
|
overflowLog.push({ frame: frame.name, kind: 'overflow', toMm: overflowMm });
|
|
328
|
-
warnings.push({
|
|
329
|
-
kind: 'frameOverflow',
|
|
330
|
-
template: template.id,
|
|
331
|
-
frame: frame.name,
|
|
332
|
-
fenceIndex: fi,
|
|
333
|
-
overflowMm,
|
|
334
|
-
configPath: frame.configPath,
|
|
335
|
-
...(vdt.sourceStart !== undefined ? { sourceStart: vdt.sourceStart, sourceEnd: vdt.sourceEnd } : {}),
|
|
336
|
-
pageIndex: page.index,
|
|
337
|
-
});
|
|
338
795
|
}
|
|
339
796
|
if (frame.exclude === 'push') {
|
|
340
797
|
const box = frameBounds({ rect, rotate: frame.rotate });
|
|
341
798
|
pushRects.set(frame.name, box);
|
|
799
|
+
// `wrap: 'contour'` (#883): the shape of the pictures it holds; a
|
|
800
|
+
// turned frame pushes with its box.
|
|
801
|
+
const shape = frame.wrap === 'contour' && frame.rotate === 0 && result
|
|
802
|
+
? frameContour(result.children, { gapPx: input.pushGapPx, dpi, emPx: input.emPx, mirrored: pageIsMirrored(input.page), pageIndex: input.page.index })
|
|
803
|
+
: undefined;
|
|
804
|
+
if (shape)
|
|
805
|
+
notes.push(...shape.notes);
|
|
806
|
+
// A frame whose text the ladder scaled (#858) holds its pictures in
|
|
807
|
+
// its laid-out coordinates: on the page they are `s` as large, from
|
|
808
|
+
// its box's corner (`frameMatrix`).
|
|
809
|
+
const contour = shape?.contour && s !== 1 ? scaleContourAbout(shape.contour, rect.x, rect.y, s, false) : shape?.contour;
|
|
810
|
+
if (contour)
|
|
811
|
+
pushShapes.set(frame.name, contour);
|
|
342
812
|
if (!seed)
|
|
343
|
-
pushed.push({ name: frame.name, box });
|
|
813
|
+
pushed.push({ name: frame.name, box, ...(contour ? { contour } : {}) });
|
|
344
814
|
}
|
|
345
815
|
frames.push(vdt);
|
|
346
816
|
vdtByName.set(frame.name, vdt);
|
|
@@ -357,29 +827,12 @@ export function layoutComposedPage(input) {
|
|
|
357
827
|
for (const f of step.frames)
|
|
358
828
|
fill(prepare(f), solved.get(f.name));
|
|
359
829
|
}
|
|
360
|
-
const log = mergeLog(reflowEvents(motionsOf(template, rects, declared), dpi / MM_PER_IN), overflowLog, template);
|
|
361
|
-
return { frames, blocks,
|
|
830
|
+
const log = mergeLog(reflowEvents(motionsOf(template, rects, declared), dpi / MM_PER_IN), [...fitLog, ...overflowLog], template);
|
|
831
|
+
return { frames, blocks, log, laid, needs, pushRects, pushShapes, fitted, notes };
|
|
362
832
|
};
|
|
363
833
|
const first = run();
|
|
364
|
-
const { frames, blocks,
|
|
365
|
-
|
|
366
|
-
const order = new Map(template.frames.map((f, i) => [f.name, i]));
|
|
367
|
-
frames.sort((a, b) => order.get(a.name) - order.get(b.name));
|
|
368
|
-
const contentBlocks = pass.contentBlocks;
|
|
369
|
-
const composed = {
|
|
370
|
-
template: template.id,
|
|
371
|
-
...(template.variant ? { variant: template.variant } : {}),
|
|
372
|
-
fenceIndex: fi,
|
|
373
|
-
role: template.role,
|
|
374
|
-
attrs: { ...fence.attrs },
|
|
375
|
-
attrSources: Object.fromEntries(Object.entries(fence.attrSources).map(([k, v]) => [k, { sourceStart: v.sourceStart + input.bodyOffset, sourceEnd: v.sourceEnd + input.bodyOffset }])),
|
|
376
|
-
frames,
|
|
377
|
-
log,
|
|
378
|
-
configPath: template.configPath,
|
|
379
|
-
sourceStart: contentBlocks[input.startIdx].sourceStart + input.bodyOffset,
|
|
380
|
-
sourceEnd: contentBlocks[input.endIdx].sourceEnd + input.bodyOffset,
|
|
381
|
-
};
|
|
382
|
-
return { composed, blocks, warnings, laid };
|
|
834
|
+
const { frames, blocks, log, laid, needs, fitted, notes } = first.pushRects.size > 0 ? run(first.pushRects, first.pushShapes) : first;
|
|
835
|
+
return { frames, blocks, log, laid, needs, template, fitted, ...(notes.length > 0 ? { notes } : {}) };
|
|
383
836
|
}
|
|
384
837
|
const sameRect = (a, b) => Math.abs(a.x - b.x) < 0.01 && Math.abs(a.y - b.y) < 0.01 && Math.abs(a.width - b.width) < 0.01 && Math.abs(a.height - b.height) < 0.01;
|
|
385
838
|
/** Each laid frame's declared and final boxes, in template order, with
|
|
@@ -439,6 +892,7 @@ function commitFrame(result, rect, story, range, input, vdt, out) {
|
|
|
439
892
|
}
|
|
440
893
|
box.pageIndex = page.index;
|
|
441
894
|
box.columnIndex = 0;
|
|
895
|
+
stampFrameAttrText(box, blocks[story.startIdx]?.containerAttrSources, story.plan.attrs, bodyOffset);
|
|
442
896
|
offsetCalloutToAbsolute(result, rect.x, rect.y);
|
|
443
897
|
out.push(box);
|
|
444
898
|
vdt.blockIds.push(box.id);
|
|
@@ -449,6 +903,37 @@ function commitFrame(result, rect, story, range, input, vdt, out) {
|
|
|
449
903
|
vdt.blockIds.push(child.id);
|
|
450
904
|
}
|
|
451
905
|
}
|
|
906
|
+
/** The frame's tab (`label`) and title (`title`) as the box draws them,
|
|
907
|
+
* stamped with where the `:::frame` fence writes them (#857): the
|
|
908
|
+
* attribute's value range, and a per-character map when the text is
|
|
909
|
+
* printed as written. */
|
|
910
|
+
function stampFrameAttrText(box, sources, attrs, bodyOffset) {
|
|
911
|
+
if (!sources || !box.designOverlay)
|
|
912
|
+
return;
|
|
913
|
+
for (const name of ['label', 'title']) {
|
|
914
|
+
const src = sources[name];
|
|
915
|
+
const value = attrs[name];
|
|
916
|
+
if (!src || value === undefined || value.trim() === '')
|
|
917
|
+
continue;
|
|
918
|
+
const start = src.sourceStart + bodyOffset;
|
|
919
|
+
const end = src.sourceEnd + bodyOffset;
|
|
920
|
+
for (const b of box.designOverlay.blocks) {
|
|
921
|
+
if (b.kind !== 'text' || b.attr !== undefined)
|
|
922
|
+
continue;
|
|
923
|
+
const shown = b.lines.map((l) => l.text).join(' ');
|
|
924
|
+
if (shown.trim() !== value.trim() && shown.trim().toLocaleLowerCase() !== value.trim().toLocaleLowerCase())
|
|
925
|
+
continue;
|
|
926
|
+
b.sourceStart = start;
|
|
927
|
+
b.sourceEnd = end;
|
|
928
|
+
b.attr = name;
|
|
929
|
+
if (shown === value && end - start === value.length) {
|
|
930
|
+
b.sourceText = value;
|
|
931
|
+
b.sourceMap = Array.from({ length: value.length }, (_, i) => start + i);
|
|
932
|
+
}
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
452
937
|
/** The callout style a frame is drawn with: `base` (its `style`, or the
|
|
453
938
|
* frameless frame style) with what a frame sets of its own: its padding
|
|
454
939
|
* (side by side) and a measure that is the frame's. A style's placement,
|