@embedpdf/core-stage 3.0.0-next.13 → 3.0.0-next.15
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/index.cjs +170 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -47
- package/dist/index.d.ts +47 -47
- package/dist/index.js +170 -171
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -9,21 +9,20 @@ let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
|
9
9
|
* (a focal point relative to a page — what survives layout/zoom changes & reloads).
|
|
10
10
|
*
|
|
11
11
|
* Navigation is pure camera math. Bounds/home/margin are three separate functions.
|
|
12
|
-
* No DOM, no framework; plain-data model plus derived spatial queries (the
|
|
13
|
-
* is serializable — `Scene` also carries query methods derived from it).
|
|
14
|
-
* is the v4 Rust core, verbatim.
|
|
12
|
+
* No DOM, no framework; plain-data model plus derived spatial queries (the data
|
|
13
|
+
* is serializable — `Scene` also carries query methods derived from it).
|
|
15
14
|
*/
|
|
16
15
|
/** The page's on-screen footprint: w↔h swapped for quarter-turns (via the shared
|
|
17
|
-
* geometry primitive), scaled by its `/UserUnit` (the page's
|
|
16
|
+
* geometry primitive), scaled by its `/UserUnit` (the page's physical size — a
|
|
18
17
|
* userUnit-5 page measures 5× its point size). Everything the layout packs uses
|
|
19
18
|
* these display dims; the content scale (isotropic) and the renderer's
|
|
20
19
|
* transform recover the content. */
|
|
21
20
|
function displayDims(pg) {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
return
|
|
25
|
-
width:
|
|
26
|
-
height:
|
|
21
|
+
const userUnit = pg.userUnit ?? 1;
|
|
22
|
+
const size = (0, _embedpdf_core_geometry.displaySize)(pg.size, pg.rotation ?? 0);
|
|
23
|
+
return userUnit === 1 ? size : {
|
|
24
|
+
width: size.width * userUnit,
|
|
25
|
+
height: size.height * userUnit
|
|
27
26
|
};
|
|
28
27
|
}
|
|
29
28
|
const ZOOM_MIN = .05;
|
|
@@ -32,37 +31,37 @@ const ZoomMode = {
|
|
|
32
31
|
Automatic: "automatic",
|
|
33
32
|
FitPage: "fit-page",
|
|
34
33
|
FitWidth: "fit-width",
|
|
35
|
-
/** Fit the
|
|
34
|
+
/** Fit the whole scene (every page) in view — the construction overview. */
|
|
36
35
|
FitAll: "fit-all"
|
|
37
36
|
};
|
|
38
|
-
const clamp = (
|
|
39
|
-
const toScreen = (
|
|
40
|
-
x: (
|
|
41
|
-
y: (
|
|
37
|
+
const clamp = (value, lo, hi) => Math.min(Math.max(value, lo), hi);
|
|
38
|
+
const toScreen = (camera, point) => ({
|
|
39
|
+
x: (point.x - camera.x) * camera.zoom,
|
|
40
|
+
y: (point.y - camera.y) * camera.zoom
|
|
42
41
|
});
|
|
43
|
-
const toWorld = (
|
|
44
|
-
x:
|
|
45
|
-
y:
|
|
42
|
+
const toWorld = (camera, point) => ({
|
|
43
|
+
x: camera.x + point.x / camera.zoom,
|
|
44
|
+
y: camera.y + point.y / camera.zoom
|
|
46
45
|
});
|
|
47
|
-
const cameraWorldRect = (
|
|
48
|
-
x:
|
|
49
|
-
y:
|
|
50
|
-
width: vp.width /
|
|
51
|
-
height: vp.height /
|
|
46
|
+
const cameraWorldRect = (camera, vp) => ({
|
|
47
|
+
x: camera.x,
|
|
48
|
+
y: camera.y,
|
|
49
|
+
width: vp.width / camera.zoom,
|
|
50
|
+
height: vp.height / camera.zoom
|
|
52
51
|
});
|
|
53
52
|
/** Focal zoom: keep the world point under `screenPt` fixed (no bounce). */
|
|
54
|
-
const zoomAround = (
|
|
55
|
-
const zoom = clamp(
|
|
53
|
+
const zoomAround = (camera, screenPt, factor) => {
|
|
54
|
+
const zoom = clamp(camera.zoom * factor, ZOOM_MIN, 64);
|
|
56
55
|
return {
|
|
57
|
-
x:
|
|
58
|
-
y:
|
|
56
|
+
x: camera.x + screenPt.x / camera.zoom - screenPt.x / zoom,
|
|
57
|
+
y: camera.y + screenPt.y / camera.zoom - screenPt.y / zoom,
|
|
59
58
|
zoom
|
|
60
59
|
};
|
|
61
60
|
};
|
|
62
|
-
const panByScreen = (
|
|
63
|
-
x:
|
|
64
|
-
y:
|
|
65
|
-
zoom:
|
|
61
|
+
const panByScreen = (camera, dxScreen, dyScreen) => ({
|
|
62
|
+
x: camera.x - dxScreen / camera.zoom,
|
|
63
|
+
y: camera.y - dyScreen / camera.zoom,
|
|
64
|
+
zoom: camera.zoom
|
|
66
65
|
});
|
|
67
66
|
const centerOnWorld = (worldPt, vp, zoom) => ({
|
|
68
67
|
zoom,
|
|
@@ -80,82 +79,82 @@ const travelRange = (origin, content, view, zoom, padding) => ({
|
|
|
80
79
|
* with a `padding` gutter the camera may reveal beyond each content edge.
|
|
81
80
|
*
|
|
82
81
|
* Per axis the travel interval is `travelRange`'s [near, far]. If the padded
|
|
83
|
-
* content
|
|
84
|
-
* the camera is
|
|
82
|
+
* content overflows the viewport, the camera travels freely in it. If it fits,
|
|
83
|
+
* the camera is locked to the `fitAlign` rest point — start = near, end = far,
|
|
85
84
|
* center = midpoint (x resolved logically against `direction`).
|
|
86
85
|
*/
|
|
87
|
-
const clampCamera = (
|
|
88
|
-
if (!
|
|
89
|
-
const
|
|
90
|
-
const fit =
|
|
86
|
+
const clampCamera = (camera, bounds, vp, constraint) => {
|
|
87
|
+
if (!constraint.bounded) return camera;
|
|
88
|
+
const padding = constraint.padding;
|
|
89
|
+
const fit = constraint.fitAlign ?? {
|
|
91
90
|
x: "center",
|
|
92
91
|
y: "center"
|
|
93
92
|
};
|
|
94
|
-
const axis = (pos, origin, content, view,
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
return clamp(pos,
|
|
93
|
+
const axis = (pos, origin, content, view, align) => {
|
|
94
|
+
const travel = travelRange(origin, content, view, camera.zoom, padding);
|
|
95
|
+
if (travel.fits) return align === "start" ? travel.near : align === "end" ? travel.far : (travel.near + travel.far) / 2;
|
|
96
|
+
return clamp(pos, travel.near, travel.far);
|
|
98
97
|
};
|
|
99
|
-
const ax =
|
|
98
|
+
const ax = constraint.direction === "rtl" && fit.x !== "center" ? fit.x === "start" ? "end" : "start" : fit.x;
|
|
100
99
|
return {
|
|
101
|
-
zoom:
|
|
102
|
-
x: axis(
|
|
103
|
-
y: axis(
|
|
100
|
+
zoom: camera.zoom,
|
|
101
|
+
x: axis(camera.x, bounds.x, bounds.width, vp.width, ax),
|
|
102
|
+
y: axis(camera.y, bounds.y, bounds.height, vp.height, fit.y)
|
|
104
103
|
};
|
|
105
104
|
};
|
|
106
|
-
const scrollMetrics = (
|
|
105
|
+
const scrollMetrics = (camera, bounds, vp, padding) => {
|
|
107
106
|
const axis = (pos, origin, content, view) => {
|
|
108
|
-
const lo = Math.min(origin - padding /
|
|
109
|
-
const total = (Math.max(origin + content + padding /
|
|
107
|
+
const lo = Math.min(origin - padding / camera.zoom, pos);
|
|
108
|
+
const total = (Math.max(origin + content + padding / camera.zoom, pos + view / camera.zoom) - lo) * camera.zoom;
|
|
110
109
|
return {
|
|
111
|
-
offset: (pos - lo) *
|
|
110
|
+
offset: (pos - lo) * camera.zoom,
|
|
112
111
|
total,
|
|
113
112
|
scrollable: total > view + .5
|
|
114
113
|
};
|
|
115
114
|
};
|
|
116
|
-
const
|
|
117
|
-
const
|
|
115
|
+
const horizontal = axis(camera.x, bounds.x, bounds.width, vp.width);
|
|
116
|
+
const vertical = axis(camera.y, bounds.y, bounds.height, vp.height);
|
|
118
117
|
return {
|
|
119
|
-
scrollLeft:
|
|
120
|
-
scrollTop:
|
|
121
|
-
scrollWidth:
|
|
122
|
-
scrollHeight:
|
|
118
|
+
scrollLeft: horizontal.offset,
|
|
119
|
+
scrollTop: vertical.offset,
|
|
120
|
+
scrollWidth: horizontal.total,
|
|
121
|
+
scrollHeight: vertical.total,
|
|
123
122
|
clientWidth: vp.width,
|
|
124
123
|
clientHeight: vp.height,
|
|
125
|
-
scrollableX:
|
|
126
|
-
scrollableY:
|
|
124
|
+
scrollableX: horizontal.scrollable,
|
|
125
|
+
scrollableY: vertical.scrollable
|
|
127
126
|
};
|
|
128
127
|
};
|
|
129
128
|
/**
|
|
130
129
|
* The inverse write — `Element.scrollTo` semantics: absolute offsets into the
|
|
131
|
-
*
|
|
130
|
+
* current range (the same union `scrollMetrics` reports), clamped into
|
|
132
131
|
* [0, total − view]; an omitted axis does not move. Zoom is untouched: scrolling
|
|
133
132
|
* is a pan in scroller clothing.
|
|
134
133
|
*/
|
|
135
|
-
const cameraFromScroll = (
|
|
134
|
+
const cameraFromScroll = (camera, bounds, vp, padding, target) => {
|
|
136
135
|
const axis = (want, pos, origin, content, view) => {
|
|
137
136
|
if (want === void 0) return pos;
|
|
138
|
-
const lo = Math.min(origin - padding /
|
|
139
|
-
const hi = Math.max(origin + content + padding /
|
|
140
|
-
const max = Math.max(0, (hi - lo) *
|
|
141
|
-
return lo + clamp(want, 0, max) /
|
|
137
|
+
const lo = Math.min(origin - padding / camera.zoom, pos);
|
|
138
|
+
const hi = Math.max(origin + content + padding / camera.zoom, pos + view / camera.zoom);
|
|
139
|
+
const max = Math.max(0, (hi - lo) * camera.zoom - view);
|
|
140
|
+
return lo + clamp(want, 0, max) / camera.zoom;
|
|
142
141
|
};
|
|
143
142
|
return {
|
|
144
|
-
zoom:
|
|
145
|
-
x: axis(target.left,
|
|
146
|
-
y: axis(target.top,
|
|
143
|
+
zoom: camera.zoom,
|
|
144
|
+
x: axis(target.left, camera.x, bounds.x, bounds.width, vp.width),
|
|
145
|
+
y: axis(target.top, camera.y, bounds.y, bounds.height, vp.height)
|
|
147
146
|
};
|
|
148
147
|
};
|
|
149
148
|
/**
|
|
150
|
-
*
|
|
149
|
+
* The placement algorithm — every arrival (goToPage, next/prev, reset) lands
|
|
151
150
|
* through it. Pure alignment: put the subject at `align` in the viewport —
|
|
152
151
|
* per axis 'start' (reading edge at the padded viewport edge), 'end' (far
|
|
153
152
|
* edge), 'center', or a fraction — with x resolved logically against the
|
|
154
|
-
* reading direction. The
|
|
153
|
+
* reading direction. The same rule at every zoom: whether the subject fits or
|
|
155
154
|
* overflows never changes where it lands. Landing is policy, not a side
|
|
156
155
|
* effect of magnification.
|
|
157
156
|
*
|
|
158
|
-
* Deliberately clamp-free: the caller clamps the result against the
|
|
157
|
+
* Deliberately clamp-free: the caller clamps the result against the true
|
|
159
158
|
* travel bounds (the scene in continuous flow, the item slice in paged). On
|
|
160
159
|
* an axis with no real freedom that clamp collapses the landing to the
|
|
161
160
|
* `fitAlign` rest point — "rests where it must" comes out of the clamp's
|
|
@@ -165,11 +164,11 @@ function placeCamera(subject, vp, zoom, padding = 0, align = {
|
|
|
165
164
|
x: "start",
|
|
166
165
|
y: "start"
|
|
167
166
|
}, direction = "ltr") {
|
|
168
|
-
const axis = (
|
|
169
|
-
if (
|
|
170
|
-
if (
|
|
171
|
-
const
|
|
172
|
-
return pos + extent / 2 - view *
|
|
167
|
+
const axis = (alignValue, pos, extent, view) => {
|
|
168
|
+
if (alignValue === "start") return pos - padding / zoom;
|
|
169
|
+
if (alignValue === "end") return pos + extent - (view - padding) / zoom;
|
|
170
|
+
const fraction = alignValue === "center" ? .5 : clamp(alignValue, 0, 1);
|
|
171
|
+
return pos + extent / 2 - view * fraction / zoom;
|
|
173
172
|
};
|
|
174
173
|
return {
|
|
175
174
|
zoom,
|
|
@@ -178,13 +177,13 @@ function placeCamera(subject, vp, zoom, padding = 0, align = {
|
|
|
178
177
|
};
|
|
179
178
|
}
|
|
180
179
|
/**
|
|
181
|
-
* The
|
|
180
|
+
* The minimal camera move that makes `rect` fully visible (with a `padding` gutter)
|
|
182
181
|
* — DOM scrollIntoView({ block: 'nearest' }) as camera math. Per axis, the cameras
|
|
183
|
-
* that show the rect form an interval; the answer is the current camera
|
|
182
|
+
* that show the rect form an interval; the answer is the current camera clamped
|
|
184
183
|
* into it: already inside → unchanged (the no-op case, by construction, not by
|
|
185
184
|
* condition). An oversized rect (interval inverted) aligns to its start.
|
|
186
185
|
*
|
|
187
|
-
* This is deliberately
|
|
186
|
+
* This is deliberately not placement: `placeCamera` answers "I am navigating here"
|
|
188
187
|
* (canonical); `revealCamera` answers "make sure this is seeable" (minimal).
|
|
189
188
|
*/
|
|
190
189
|
function revealCamera(cam, rect, vp, padding = 0) {
|
|
@@ -219,11 +218,11 @@ function anchorAtPoint(scene, worldPt) {
|
|
|
219
218
|
const item = scene.nearestItem(worldPt);
|
|
220
219
|
let page = item.pages[0];
|
|
221
220
|
let best = Infinity;
|
|
222
|
-
for (const
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
225
|
-
best =
|
|
226
|
-
page =
|
|
221
|
+
for (const candidate of item.pages) {
|
|
222
|
+
const distance = Math.abs(candidate.x + candidate.width / 2 - worldPt.x);
|
|
223
|
+
if (distance < best) {
|
|
224
|
+
best = distance;
|
|
225
|
+
page = candidate;
|
|
227
226
|
}
|
|
228
227
|
}
|
|
229
228
|
return {
|
|
@@ -232,11 +231,11 @@ function anchorAtPoint(scene, worldPt) {
|
|
|
232
231
|
fy: (worldPt.y - page.y) / page.height
|
|
233
232
|
};
|
|
234
233
|
}
|
|
235
|
-
/** The camera that puts the anchor's world point at a given
|
|
234
|
+
/** The camera that puts the anchor's world point at a given screen point. */
|
|
236
235
|
function cameraForAnchorAtScreen(anchor, scene, screenPt, zoom) {
|
|
237
236
|
const item = scene.items[scene.itemOfPage(anchor.pageIndex)];
|
|
238
237
|
let page = item.pages[0];
|
|
239
|
-
for (const
|
|
238
|
+
for (const candidate of item.pages) if (candidate.pageIndex === anchor.pageIndex) page = candidate;
|
|
240
239
|
const world = {
|
|
241
240
|
x: page.x + anchor.fx * page.width,
|
|
242
241
|
y: page.y + anchor.fy * page.height
|
|
@@ -256,53 +255,53 @@ function anchorFromCamera(cam, scene, vp, at) {
|
|
|
256
255
|
}));
|
|
257
256
|
}
|
|
258
257
|
/** The camera that restores an anchor to the same viewport point it was
|
|
259
|
-
* captured at — capture and restore
|
|
258
|
+
* captured at — capture and restore must agree on `at` for exact round-trips. */
|
|
260
259
|
function cameraFromAnchor(anchor, scene, vp, zoom, at) {
|
|
261
260
|
return cameraForAnchorAtScreen(anchor, scene, at ?? {
|
|
262
261
|
x: vp.width / 2,
|
|
263
262
|
y: vp.height / 2
|
|
264
263
|
}, zoom);
|
|
265
264
|
}
|
|
266
|
-
function lowerBound(arr,
|
|
265
|
+
function lowerBound(arr, value) {
|
|
267
266
|
let lo = 0;
|
|
268
267
|
let hi = arr.length;
|
|
269
268
|
while (lo < hi) {
|
|
270
269
|
const mid = lo + hi >> 1;
|
|
271
|
-
if (arr[mid] <
|
|
270
|
+
if (arr[mid] < value) lo = mid + 1;
|
|
272
271
|
else hi = mid;
|
|
273
272
|
}
|
|
274
273
|
return lo;
|
|
275
274
|
}
|
|
276
|
-
function upperBound(arr,
|
|
275
|
+
function upperBound(arr, value) {
|
|
277
276
|
let lo = 0;
|
|
278
277
|
let hi = arr.length;
|
|
279
278
|
while (lo < hi) {
|
|
280
279
|
const mid = lo + hi >> 1;
|
|
281
|
-
if (arr[mid] <=
|
|
280
|
+
if (arr[mid] <= value) lo = mid + 1;
|
|
282
281
|
else hi = mid;
|
|
283
282
|
}
|
|
284
283
|
return lo;
|
|
285
284
|
}
|
|
286
285
|
const OVERSCAN = 2;
|
|
287
286
|
/** Intrinsic item dimensions (pages only, no margins) — the `uniform` reference.
|
|
288
|
-
* Uses
|
|
287
|
+
* Uses display dims, so `uniform` equalizes the rotated footprint and a rotated
|
|
289
288
|
* page sizes to match its neighbours as it actually appears. */
|
|
290
289
|
function measureItem(pages, group, gap) {
|
|
291
|
-
let
|
|
292
|
-
let
|
|
290
|
+
let width = 0;
|
|
291
|
+
let height = 0;
|
|
293
292
|
for (let j = 0; j < group.length; j++) {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
if (
|
|
293
|
+
const size = displayDims(pages[group[j]]);
|
|
294
|
+
width += size.width + (j > 0 ? gap : 0);
|
|
295
|
+
if (size.height > height) height = size.height;
|
|
297
296
|
}
|
|
298
297
|
return {
|
|
299
|
-
width
|
|
300
|
-
height
|
|
298
|
+
width,
|
|
299
|
+
height
|
|
301
300
|
};
|
|
302
301
|
}
|
|
303
302
|
/**
|
|
304
|
-
* Pack one item from
|
|
305
|
-
* (and the spread's inner gap) scale with the sizing factor; the margins do
|
|
303
|
+
* Pack one item from scaled pages plus constant per-page margins. The page boxes
|
|
304
|
+
* (and the spread's inner gap) scale with the sizing factor; the margins do not —
|
|
306
305
|
* they are screen-px-derived chrome bands and stay constant in world space.
|
|
307
306
|
*/
|
|
308
307
|
function packScaledItem(pages, group, scale, gap, frame, direction) {
|
|
@@ -311,23 +310,23 @@ function packScaledItem(pages, group, scale, gap, frame, direction) {
|
|
|
311
310
|
const local = [];
|
|
312
311
|
let lx = 0;
|
|
313
312
|
for (let j = 0; j < group.length; j++) {
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const
|
|
313
|
+
const size = displayDims(pages[group[j]]);
|
|
314
|
+
const scaledWidth = size.width * scale;
|
|
315
|
+
const scaledHeight = size.height * scale;
|
|
317
316
|
local.push({
|
|
318
317
|
pageIndex: group[j],
|
|
319
318
|
lx: lx + frame.left,
|
|
320
|
-
ly: frame.top + (maxH -
|
|
321
|
-
w,
|
|
322
|
-
h,
|
|
319
|
+
ly: frame.top + (maxH - scaledHeight) / 2,
|
|
320
|
+
w: scaledWidth,
|
|
321
|
+
h: scaledHeight,
|
|
323
322
|
rotation: pages[group[j]].rotation ?? 0,
|
|
324
323
|
userUnit: pages[group[j]].userUnit ?? 1
|
|
325
324
|
});
|
|
326
|
-
lx += frame.left +
|
|
325
|
+
lx += frame.left + scaledWidth + frame.right + gap * scale;
|
|
327
326
|
}
|
|
328
327
|
const width = Math.max(0, lx - gap * scale);
|
|
329
328
|
const height = frame.top + maxH + frame.bottom;
|
|
330
|
-
if (direction === "rtl") for (const
|
|
329
|
+
if (direction === "rtl") for (const box of local) box.lx = width - box.lx - box.w + (frame.left - frame.right);
|
|
331
330
|
return {
|
|
332
331
|
local,
|
|
333
332
|
width,
|
|
@@ -335,14 +334,14 @@ function packScaledItem(pages, group, scale, gap, frame, direction) {
|
|
|
335
334
|
};
|
|
336
335
|
}
|
|
337
336
|
function placePages(item, local, contentScale) {
|
|
338
|
-
return local.map((
|
|
339
|
-
pageIndex:
|
|
340
|
-
x: item.x +
|
|
341
|
-
y: item.y +
|
|
342
|
-
width:
|
|
343
|
-
height:
|
|
344
|
-
rotation:
|
|
345
|
-
contentScale: contentScale *
|
|
337
|
+
return local.map((box) => ({
|
|
338
|
+
pageIndex: box.pageIndex,
|
|
339
|
+
x: item.x + box.lx,
|
|
340
|
+
y: item.y + box.ly,
|
|
341
|
+
width: box.w,
|
|
342
|
+
height: box.h,
|
|
343
|
+
rotation: box.rotation,
|
|
344
|
+
contentScale: contentScale * box.userUnit
|
|
346
345
|
}));
|
|
347
346
|
}
|
|
348
347
|
/** Per-item world scale = sizing factor (1 intrinsic, else cross-equalize for
|
|
@@ -350,17 +349,17 @@ function placePages(item, local, contentScale) {
|
|
|
350
349
|
* result becomes the page's `contentScale`, so world units are view px and 100%
|
|
351
350
|
* is physically accurate. `viewUnitsPerPoint` defaults to 1 (neutral). */
|
|
352
351
|
const itemScale = (crossIntrinsic, refCross, sizing, viewUnitsPerPoint = 1) => (sizing === "uniform" && crossIntrinsic > 0 ? refCross / crossIntrinsic : 1) * viewUnitsPerPoint;
|
|
353
|
-
function linearLayout(pages, grouping,
|
|
354
|
-
const gap =
|
|
355
|
-
const vertical = (
|
|
356
|
-
const align =
|
|
357
|
-
const sizing =
|
|
358
|
-
const direction =
|
|
352
|
+
function linearLayout(pages, grouping, options = {}) {
|
|
353
|
+
const gap = options.gap ?? 16;
|
|
354
|
+
const vertical = (options.axis ?? "y") === "y";
|
|
355
|
+
const align = options.align ?? "center";
|
|
356
|
+
const sizing = options.sizing ?? "intrinsic";
|
|
357
|
+
const direction = options.direction ?? "ltr";
|
|
359
358
|
const mirrored = !vertical && direction === "rtl";
|
|
360
|
-
const frame =
|
|
359
|
+
const frame = options.pageFrame ?? _embedpdf_core_geometry.NO_FRAME;
|
|
361
360
|
const measures = grouping.map((group) => measureItem(pages, group, gap));
|
|
362
|
-
const crossOf = (
|
|
363
|
-
const refCross = measures.reduce((
|
|
361
|
+
const crossOf = (measure) => vertical ? measure.width : measure.height;
|
|
362
|
+
const refCross = measures.reduce((largest, measure) => Math.max(largest, crossOf(measure)), 0);
|
|
364
363
|
const items = new Array(grouping.length);
|
|
365
364
|
const locals = new Array(grouping.length);
|
|
366
365
|
const scales = new Array(grouping.length);
|
|
@@ -369,10 +368,10 @@ function linearLayout(pages, grouping, opts = {}) {
|
|
|
369
368
|
let maxW = 0;
|
|
370
369
|
let maxH = 0;
|
|
371
370
|
for (let i = 0; i < grouping.length; i++) {
|
|
372
|
-
const
|
|
373
|
-
const packed = packScaledItem(pages, grouping[i],
|
|
371
|
+
const scale = itemScale(crossOf(measures[i]), refCross, sizing, options.viewUnitsPerPoint);
|
|
372
|
+
const packed = packScaledItem(pages, grouping[i], scale, gap, frame, direction);
|
|
374
373
|
const { width, height } = packed;
|
|
375
|
-
scales[i] =
|
|
374
|
+
scales[i] = scale;
|
|
376
375
|
locals[i] = packed.local;
|
|
377
376
|
const it = {
|
|
378
377
|
index: i,
|
|
@@ -410,13 +409,13 @@ function linearLayout(pages, grouping, opts = {}) {
|
|
|
410
409
|
else it.y = align === "center" ? (size.height - it.height) / 2 : 0;
|
|
411
410
|
it.pages = placePages(it, locals[i], scales[i]);
|
|
412
411
|
}
|
|
413
|
-
const posIdx = (
|
|
414
|
-
const starts = items.map((_,
|
|
415
|
-
const it = items[posIdx(
|
|
412
|
+
const posIdx = (position) => mirrored ? items.length - 1 - position : position;
|
|
413
|
+
const starts = items.map((_, position) => {
|
|
414
|
+
const it = items[posIdx(position)];
|
|
416
415
|
return vertical ? it.y : it.x;
|
|
417
416
|
});
|
|
418
|
-
const ends = items.map((_,
|
|
419
|
-
const it = items[posIdx(
|
|
417
|
+
const ends = items.map((_, position) => {
|
|
418
|
+
const it = items[posIdx(position)];
|
|
420
419
|
return vertical ? it.y + it.height : it.x + it.width;
|
|
421
420
|
});
|
|
422
421
|
const firstPage = items.map((it) => it.pageIndexes[0]);
|
|
@@ -429,41 +428,41 @@ function linearLayout(pages, grouping, opts = {}) {
|
|
|
429
428
|
width: maxW,
|
|
430
429
|
height: maxH
|
|
431
430
|
},
|
|
432
|
-
query(
|
|
433
|
-
const a0 = vertical ?
|
|
434
|
-
const a1 = vertical ?
|
|
431
|
+
query(rect) {
|
|
432
|
+
const a0 = vertical ? rect.y : rect.x;
|
|
433
|
+
const a1 = vertical ? rect.y + rect.height : rect.x + rect.width;
|
|
435
434
|
const lo = Math.max(0, lowerBound(ends, a0) - OVERSCAN);
|
|
436
435
|
const hi = Math.min(items.length, upperBound(starts, a1) + OVERSCAN);
|
|
437
436
|
const out = [];
|
|
438
|
-
for (let
|
|
437
|
+
for (let position = lo; position < hi; position++) out.push(items[posIdx(position)]);
|
|
439
438
|
return out;
|
|
440
439
|
},
|
|
441
440
|
nearestItem(pt) {
|
|
442
|
-
const
|
|
443
|
-
return items[posIdx(
|
|
441
|
+
const position = clamp(upperBound(starts, vertical ? pt.y : pt.x) - 1, 0, items.length - 1);
|
|
442
|
+
return items[posIdx(position)];
|
|
444
443
|
},
|
|
445
444
|
itemOfPage(pi) {
|
|
446
445
|
return clamp(upperBound(firstPage, pi) - 1, 0, items.length - 1);
|
|
447
446
|
}
|
|
448
447
|
};
|
|
449
448
|
}
|
|
450
|
-
function gridLayout(pages, grouping,
|
|
451
|
-
const gap =
|
|
452
|
-
const
|
|
453
|
-
const sizing =
|
|
454
|
-
const direction =
|
|
455
|
-
const frame =
|
|
449
|
+
function gridLayout(pages, grouping, options = {}) {
|
|
450
|
+
const gap = options.gap ?? 48;
|
|
451
|
+
const itemCount = grouping.length;
|
|
452
|
+
const sizing = options.sizing ?? "intrinsic";
|
|
453
|
+
const direction = options.direction ?? "ltr";
|
|
454
|
+
const frame = options.pageFrame ?? _embedpdf_core_geometry.NO_FRAME;
|
|
456
455
|
const measures = grouping.map((group) => measureItem(pages, group, gap));
|
|
457
|
-
const refW = measures.reduce((
|
|
458
|
-
const locals = new Array(
|
|
459
|
-
const sizes = new Array(
|
|
460
|
-
const scales = new Array(
|
|
456
|
+
const refW = measures.reduce((widest, measure) => Math.max(widest, measure.width), 0);
|
|
457
|
+
const locals = new Array(itemCount);
|
|
458
|
+
const sizes = new Array(itemCount);
|
|
459
|
+
const scales = new Array(itemCount);
|
|
461
460
|
let cellW = 1;
|
|
462
461
|
let cellH = 1;
|
|
463
|
-
for (let i = 0; i <
|
|
464
|
-
const
|
|
465
|
-
const packed = packScaledItem(pages, grouping[i],
|
|
466
|
-
scales[i] =
|
|
462
|
+
for (let i = 0; i < itemCount; i++) {
|
|
463
|
+
const scale = itemScale(measures[i].width, refW, sizing, options.viewUnitsPerPoint);
|
|
464
|
+
const packed = packScaledItem(pages, grouping[i], scale, gap, frame, direction);
|
|
465
|
+
scales[i] = scale;
|
|
467
466
|
locals[i] = packed.local;
|
|
468
467
|
sizes[i] = {
|
|
469
468
|
width: packed.width,
|
|
@@ -472,25 +471,25 @@ function gridLayout(pages, grouping, opts = {}) {
|
|
|
472
471
|
cellW = Math.max(cellW, sizes[i].width);
|
|
473
472
|
cellH = Math.max(cellH, sizes[i].height);
|
|
474
473
|
}
|
|
475
|
-
const wanted =
|
|
476
|
-
const columns = Math.max(1, Math.min(wanted, Math.max(1,
|
|
474
|
+
const wanted = options.lineWidth !== void 0 ? Math.floor((options.lineWidth + gap) / (cellW + gap)) : options.columns ?? Math.ceil(Math.sqrt(itemCount));
|
|
475
|
+
const columns = Math.max(1, Math.min(wanted, Math.max(1, itemCount)));
|
|
477
476
|
const colAt = (col) => direction === "rtl" ? columns - 1 - col : col;
|
|
478
477
|
const stepX = cellW + gap;
|
|
479
|
-
const rows = Math.max(1, Math.ceil(
|
|
478
|
+
const rows = Math.max(1, Math.ceil(itemCount / columns));
|
|
480
479
|
const rowHeight = new Array(rows).fill(1);
|
|
481
|
-
for (let i = 0; i <
|
|
480
|
+
for (let i = 0; i < itemCount; i++) {
|
|
482
481
|
const row = Math.floor(i / columns);
|
|
483
482
|
rowHeight[row] = Math.max(rowHeight[row], sizes[i].height);
|
|
484
483
|
}
|
|
485
484
|
const rowTop = new Array(rows);
|
|
486
485
|
let yCursor = 0;
|
|
487
|
-
for (let
|
|
488
|
-
rowTop[
|
|
489
|
-
yCursor += rowHeight[
|
|
486
|
+
for (let row = 0; row < rows; row++) {
|
|
487
|
+
rowTop[row] = yCursor;
|
|
488
|
+
yCursor += rowHeight[row] + gap;
|
|
490
489
|
}
|
|
491
|
-
const rowEnd = rowTop.map((top,
|
|
492
|
-
const items = new Array(
|
|
493
|
-
for (let i = 0; i <
|
|
490
|
+
const rowEnd = rowTop.map((top, row) => top + rowHeight[row]);
|
|
491
|
+
const items = new Array(itemCount);
|
|
492
|
+
for (let i = 0; i < itemCount; i++) {
|
|
494
493
|
const col = colAt(i % columns);
|
|
495
494
|
const row = Math.floor(i / columns);
|
|
496
495
|
const it = {
|
|
@@ -513,31 +512,31 @@ function gridLayout(pages, grouping, opts = {}) {
|
|
|
513
512
|
return {
|
|
514
513
|
size,
|
|
515
514
|
items,
|
|
516
|
-
itemCount
|
|
515
|
+
itemCount,
|
|
517
516
|
axis: "grid",
|
|
518
517
|
maxItemSize: {
|
|
519
518
|
width: cellW,
|
|
520
519
|
height: cellH
|
|
521
520
|
},
|
|
522
|
-
query(
|
|
523
|
-
const c0 = Math.max(0, Math.floor(
|
|
524
|
-
const c1 = Math.min(columns - 1, Math.floor((
|
|
525
|
-
const r0 = Math.max(0, lowerBound(rowEnd,
|
|
526
|
-
const r1 = Math.min(rows - 1, upperBound(rowTop,
|
|
521
|
+
query(rect) {
|
|
522
|
+
const c0 = Math.max(0, Math.floor(rect.x / stepX));
|
|
523
|
+
const c1 = Math.min(columns - 1, Math.floor((rect.x + rect.width) / stepX));
|
|
524
|
+
const r0 = Math.max(0, lowerBound(rowEnd, rect.y));
|
|
525
|
+
const r1 = Math.min(rows - 1, upperBound(rowTop, rect.y + rect.height) - 1);
|
|
527
526
|
const out = [];
|
|
528
527
|
for (let row = r0; row <= r1; row++) for (let col = c0; col <= c1; col++) {
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
528
|
+
const index = row * columns + colAt(col);
|
|
529
|
+
if (index < itemCount) out.push(items[index]);
|
|
531
530
|
}
|
|
532
531
|
return out;
|
|
533
532
|
},
|
|
534
533
|
nearestItem(pt) {
|
|
535
534
|
const col = clamp(Math.floor(pt.x / stepX), 0, columns - 1);
|
|
536
535
|
const row = clamp(upperBound(rowTop, pt.y) - 1, 0, rows - 1);
|
|
537
|
-
return items[clamp(row * columns + colAt(col), 0,
|
|
536
|
+
return items[clamp(row * columns + colAt(col), 0, itemCount - 1)];
|
|
538
537
|
},
|
|
539
538
|
itemOfPage(pi) {
|
|
540
|
-
return clamp(upperBound(firstPage, pi) - 1, 0,
|
|
539
|
+
return clamp(upperBound(firstPage, pi) - 1, 0, itemCount - 1);
|
|
541
540
|
}
|
|
542
541
|
};
|
|
543
542
|
}
|