@embedpdf/plugin-stage 3.0.0-next.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/LICENSE +192 -0
- package/README.md +205 -0
- package/dist/index.cjs +1300 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +603 -0
- package/dist/index.d.ts +603 -0
- package/dist/index.js +1266 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let _embedpdf_core = require("@embedpdf/core");
|
|
25
|
+
let _embedpdf_plugin_interaction = require("@embedpdf/plugin-interaction");
|
|
26
|
+
let _embedpdf_core_stage = require("@embedpdf/core-stage");
|
|
27
|
+
_embedpdf_core_stage = __toESM(_embedpdf_core_stage, 1);
|
|
28
|
+
let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
29
|
+
//#region src/settings.ts
|
|
30
|
+
/**
|
|
31
|
+
* Out-of-the-box defaults — a sensible document-reading feel. They are JUST
|
|
32
|
+
* defaults: every field is overridable in `stagePlugin(config)` and at runtime via
|
|
33
|
+
* the setters / `update()`. The plugin ships NO named presets ("document",
|
|
34
|
+
* "canvas", …) — a preset is simply an object the app keeps and passes to
|
|
35
|
+
* `update()`, so that taxonomy stays a customer concern.
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_SETTINGS = {
|
|
38
|
+
flow: "continuous",
|
|
39
|
+
layout: "vertical",
|
|
40
|
+
spread: "none",
|
|
41
|
+
sizing: "intrinsic",
|
|
42
|
+
columns: "square",
|
|
43
|
+
bounded: true,
|
|
44
|
+
padding: 24,
|
|
45
|
+
gap: 16,
|
|
46
|
+
pageFrame: {
|
|
47
|
+
top: 0,
|
|
48
|
+
right: 0,
|
|
49
|
+
bottom: 0,
|
|
50
|
+
left: 0
|
|
51
|
+
},
|
|
52
|
+
direction: "ltr",
|
|
53
|
+
fitAlign: {
|
|
54
|
+
x: "center",
|
|
55
|
+
y: "center"
|
|
56
|
+
},
|
|
57
|
+
arrivalAlign: {
|
|
58
|
+
x: "start",
|
|
59
|
+
y: "start"
|
|
60
|
+
},
|
|
61
|
+
zoomAlign: {
|
|
62
|
+
x: "center",
|
|
63
|
+
y: "center"
|
|
64
|
+
},
|
|
65
|
+
anchorAlign: {
|
|
66
|
+
x: "start",
|
|
67
|
+
y: "start"
|
|
68
|
+
},
|
|
69
|
+
viewRotation: 0,
|
|
70
|
+
zoom: { mode: _embedpdf_core_stage.ZoomMode.Automatic },
|
|
71
|
+
scrollBehavior: "smooth",
|
|
72
|
+
viewUnitsPerPoint: 96 / 72
|
|
73
|
+
};
|
|
74
|
+
const SETTINGS_EFFECT = {
|
|
75
|
+
flow: "reflow",
|
|
76
|
+
layout: "scene",
|
|
77
|
+
spread: "scene",
|
|
78
|
+
sizing: "scene",
|
|
79
|
+
columns: "scene",
|
|
80
|
+
bounded: "reclamp",
|
|
81
|
+
padding: "reclamp",
|
|
82
|
+
gap: "scene",
|
|
83
|
+
pageFrame: "scene",
|
|
84
|
+
direction: "scene",
|
|
85
|
+
fitAlign: "reclamp",
|
|
86
|
+
arrivalAlign: "none",
|
|
87
|
+
zoomAlign: "none",
|
|
88
|
+
anchorAlign: "none",
|
|
89
|
+
viewRotation: "scene",
|
|
90
|
+
zoom: "refit",
|
|
91
|
+
scrollBehavior: "none",
|
|
92
|
+
viewUnitsPerPoint: "scene"
|
|
93
|
+
};
|
|
94
|
+
const SETTING_KEYS = Object.keys(SETTINGS_EFFECT);
|
|
95
|
+
const valueEq = (a, b) => {
|
|
96
|
+
if (a === b) return true;
|
|
97
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
98
|
+
const ka = Object.keys(a);
|
|
99
|
+
return ka.length === Object.keys(b).length && ka.every((k) => a[k] === b[k]);
|
|
100
|
+
};
|
|
101
|
+
/** Field-by-field settings equality, derived from the registry — a new setting is
|
|
102
|
+
* covered automatically. (The React `useStageSettings` selector equality.) */
|
|
103
|
+
const settingsEqual = (a, b) => SETTING_KEYS.every((k) => valueEq(a[k], b[k]));
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/capability.ts
|
|
106
|
+
/**
|
|
107
|
+
* The Stage capability — selectors (pure reads) + intents (the only writers).
|
|
108
|
+
*
|
|
109
|
+
* Layering, stated honestly:
|
|
110
|
+
* • stage-core — PURE spatial math (no DOM, no time). Ports to Rust later.
|
|
111
|
+
* • this capability — the IMPURE platform shell. It dispatches, caches, and owns
|
|
112
|
+
* the camera tween. It is NOT pure; the one host dependency it has (frame
|
|
113
|
+
* timing) enters through an injected Scheduler, never a hidden global.
|
|
114
|
+
*
|
|
115
|
+
* The model: EVERY camera move is defined by what it holds fixed.
|
|
116
|
+
* • gestures (pan/pinch/wheel) — the content under the pointer. Physics, no setting.
|
|
117
|
+
* • focal zoom (zoomIn/zoomTo/fit-mode switches) — the zoomAlign viewport point.
|
|
118
|
+
* • reframes (resize/rotate/re-layout) — the page-point at the anchorAlign
|
|
119
|
+
* viewport point: captured there before the change, restored there after.
|
|
120
|
+
* • arrivals (goToPage/next/prev/reset) — nothing prior: a fresh landing at
|
|
121
|
+
* arrivalAlign, THE SAME at every zoom (landing is policy, never a zoom side effect).
|
|
122
|
+
* • explicit (reveal/destination/viewpoint) — whatever the call specifies.
|
|
123
|
+
* fitAlign stands apart: the clamp's standing rest constraint wherever an axis has no
|
|
124
|
+
* freedom against the TRUE bounds — it shapes every move above, and is why a fitting
|
|
125
|
+
* axis settles identically whatever arrivalAlign says.
|
|
126
|
+
*
|
|
127
|
+
* "Does it fit the viewport?" still decides SIZE and SUBJECT — never alignment:
|
|
128
|
+
* • step size: next/prev step by ITEM (spread) when the item fits, by PAGE when
|
|
129
|
+
* zoomed in past it.
|
|
130
|
+
* • subject: you arrive AT the unit that fits (item, page — or the whole scene
|
|
131
|
+
* under fit-all).
|
|
132
|
+
*
|
|
133
|
+
* The `cursor` is THE current page in both flows: navigation sets it; continuous
|
|
134
|
+
* scrolling syncs it from the camera; paged panning never moves it.
|
|
135
|
+
*/
|
|
136
|
+
function createStageCapability(ctx, config = {}) {
|
|
137
|
+
const host = globalThis;
|
|
138
|
+
const canAnimate = !!config.scheduler || typeof host.requestAnimationFrame === "function";
|
|
139
|
+
const scheduler = config.scheduler ?? (typeof host.requestAnimationFrame === "function" ? {
|
|
140
|
+
raf: (cb) => host.requestAnimationFrame(cb),
|
|
141
|
+
caf: (h) => host.cancelAnimationFrame(h)
|
|
142
|
+
} : {
|
|
143
|
+
raf: () => 0,
|
|
144
|
+
caf: () => {}
|
|
145
|
+
});
|
|
146
|
+
const cam = () => ctx.getState().camera;
|
|
147
|
+
const vp = () => ctx.getState().vp;
|
|
148
|
+
const dpr = () => ctx.getState().dpr;
|
|
149
|
+
const pad = () => ctx.getState().padding;
|
|
150
|
+
const paged = () => ctx.getState().flow === "paged";
|
|
151
|
+
const isFitAll = () => {
|
|
152
|
+
const z = ctx.getState().zoom;
|
|
153
|
+
return "mode" in z && z.mode === _embedpdf_core_stage.ZoomMode.FitAll;
|
|
154
|
+
};
|
|
155
|
+
let groupingCache = null;
|
|
156
|
+
const grouping = () => {
|
|
157
|
+
const doc = ctx.document();
|
|
158
|
+
const st = ctx.getState();
|
|
159
|
+
const key = `${doc ? doc.pageCount : 0}|${st.spread}`;
|
|
160
|
+
if (groupingCache && groupingCache.key === key) return groupingCache;
|
|
161
|
+
const g = _embedpdf_core_stage.groupPages(doc ? doc.pageCount : 0, st.spread);
|
|
162
|
+
groupingCache = {
|
|
163
|
+
key,
|
|
164
|
+
grouping: g,
|
|
165
|
+
firstPages: g.map((item) => item[0])
|
|
166
|
+
};
|
|
167
|
+
return groupingCache;
|
|
168
|
+
};
|
|
169
|
+
const itemIndexOfPage = (pageIndex) => {
|
|
170
|
+
const fp = grouping().firstPages;
|
|
171
|
+
let lo = 0;
|
|
172
|
+
let hi = fp.length;
|
|
173
|
+
while (lo < hi) {
|
|
174
|
+
const mid = lo + hi >> 1;
|
|
175
|
+
if (fp[mid] <= pageIndex) lo = mid + 1;
|
|
176
|
+
else hi = mid;
|
|
177
|
+
}
|
|
178
|
+
return Math.max(0, lo - 1);
|
|
179
|
+
};
|
|
180
|
+
const effectiveZoom = () => {
|
|
181
|
+
const z = ctx.getState().zoom;
|
|
182
|
+
return "level" in z ? z.level : Math.max(ctx.getState().camera.zoom, 1e-4);
|
|
183
|
+
};
|
|
184
|
+
const wrapLineWidth = () => Math.max(1, (vp().width - 2 * pad()) / effectiveZoom());
|
|
185
|
+
const worldPageFrame = () => {
|
|
186
|
+
const m = ctx.getState().pageFrame;
|
|
187
|
+
if (!m.top && !m.right && !m.bottom && !m.left) return m;
|
|
188
|
+
const ez = effectiveZoom();
|
|
189
|
+
return {
|
|
190
|
+
top: m.top / ez,
|
|
191
|
+
right: m.right / ez,
|
|
192
|
+
bottom: m.bottom / ez,
|
|
193
|
+
left: m.left / ez
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const frameKey = () => {
|
|
197
|
+
const m = ctx.getState().pageFrame;
|
|
198
|
+
if (!m.top && !m.right && !m.bottom && !m.left) return "-";
|
|
199
|
+
const w = worldPageFrame();
|
|
200
|
+
return `${Math.round(w.top)},${Math.round(w.right)},${Math.round(w.bottom)},${Math.round(w.left)}`;
|
|
201
|
+
};
|
|
202
|
+
const worldGap = () => {
|
|
203
|
+
const g = ctx.getState().gap;
|
|
204
|
+
return typeof g === "number" ? g : g.px ? g.px / effectiveZoom() : 0;
|
|
205
|
+
};
|
|
206
|
+
const gapKey = () => {
|
|
207
|
+
const g = ctx.getState().gap;
|
|
208
|
+
return typeof g === "number" ? String(g) : `px:${Math.round(worldGap())}`;
|
|
209
|
+
};
|
|
210
|
+
const layoutFor = (groups) => {
|
|
211
|
+
const st = ctx.getState();
|
|
212
|
+
const raw = ctx.document()?.pages ?? [];
|
|
213
|
+
const vr = st.viewRotation;
|
|
214
|
+
const pages = vr === 0 ? raw : raw.map((p) => ({
|
|
215
|
+
...p,
|
|
216
|
+
rotation: (0, _embedpdf_core_geometry.addRotations)(p.rotation, vr)
|
|
217
|
+
}));
|
|
218
|
+
const pageFrame = worldPageFrame();
|
|
219
|
+
const gap = worldGap();
|
|
220
|
+
const vupp = st.viewUnitsPerPoint;
|
|
221
|
+
if (st.layout === "grid") return _embedpdf_core_stage.gridLayout(pages, groups, {
|
|
222
|
+
gap,
|
|
223
|
+
sizing: st.sizing,
|
|
224
|
+
direction: st.direction,
|
|
225
|
+
pageFrame,
|
|
226
|
+
viewUnitsPerPoint: vupp,
|
|
227
|
+
columns: typeof st.columns === "number" ? st.columns : void 0,
|
|
228
|
+
lineWidth: st.columns === "auto" ? wrapLineWidth() : void 0
|
|
229
|
+
});
|
|
230
|
+
return st.layout === "horizontal" ? _embedpdf_core_stage.linearLayout(pages, groups, {
|
|
231
|
+
axis: "x",
|
|
232
|
+
gap,
|
|
233
|
+
sizing: st.sizing,
|
|
234
|
+
direction: st.direction,
|
|
235
|
+
pageFrame,
|
|
236
|
+
viewUnitsPerPoint: vupp
|
|
237
|
+
}) : _embedpdf_core_stage.linearLayout(pages, groups, {
|
|
238
|
+
axis: "y",
|
|
239
|
+
gap,
|
|
240
|
+
sizing: st.sizing,
|
|
241
|
+
direction: st.direction,
|
|
242
|
+
pageFrame,
|
|
243
|
+
viewUnitsPerPoint: vupp
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
const columnsKey = () => {
|
|
247
|
+
const st = ctx.getState();
|
|
248
|
+
if (st.layout !== "grid") return "-";
|
|
249
|
+
return st.columns === "auto" ? `auto:${Math.round(wrapLineWidth())}` : String(st.columns);
|
|
250
|
+
};
|
|
251
|
+
const SCENE_KEY_FNS = {
|
|
252
|
+
columns: columnsKey,
|
|
253
|
+
gap: gapKey,
|
|
254
|
+
pageFrame: frameKey
|
|
255
|
+
};
|
|
256
|
+
const SCENE_KEYS = SETTING_KEYS.filter((k) => SETTINGS_EFFECT[k] === "scene");
|
|
257
|
+
const settingsKey = () => SCENE_KEYS.map((k) => {
|
|
258
|
+
const fn = SCENE_KEY_FNS[k];
|
|
259
|
+
if (fn) return fn();
|
|
260
|
+
const v = ctx.getState()[k];
|
|
261
|
+
return typeof v === "object" && v !== null ? JSON.stringify(v) : String(v);
|
|
262
|
+
}).join("|");
|
|
263
|
+
let sceneCache = null;
|
|
264
|
+
const docKey = () => {
|
|
265
|
+
const doc = ctx.document();
|
|
266
|
+
return doc ? `${doc.pageCount}.${doc.revision}` : "0.0";
|
|
267
|
+
};
|
|
268
|
+
const buildScene = () => {
|
|
269
|
+
const st = ctx.getState();
|
|
270
|
+
const { grouping: g } = grouping();
|
|
271
|
+
if (st.flow === "paged") {
|
|
272
|
+
const idx = g.length ? Math.min(itemIndexOfPage(st.cursor), g.length - 1) : 0;
|
|
273
|
+
const key = `paged|${docKey()}|${settingsKey()}|${idx}`;
|
|
274
|
+
if (sceneCache && sceneCache.key === key) return sceneCache.scene;
|
|
275
|
+
const scene = layoutFor(g.length ? [g[idx]] : []);
|
|
276
|
+
sceneCache = {
|
|
277
|
+
key,
|
|
278
|
+
scene
|
|
279
|
+
};
|
|
280
|
+
return scene;
|
|
281
|
+
}
|
|
282
|
+
const key = `cont|${docKey()}|${settingsKey()}`;
|
|
283
|
+
if (sceneCache && sceneCache.key === key) return sceneCache.scene;
|
|
284
|
+
const scene = layoutFor(g);
|
|
285
|
+
sceneCache = {
|
|
286
|
+
key,
|
|
287
|
+
scene
|
|
288
|
+
};
|
|
289
|
+
return scene;
|
|
290
|
+
};
|
|
291
|
+
const sceneRect = () => {
|
|
292
|
+
const { width, height } = buildScene().size;
|
|
293
|
+
return {
|
|
294
|
+
x: 0,
|
|
295
|
+
y: 0,
|
|
296
|
+
width,
|
|
297
|
+
height
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
const itemRect = (it) => ({
|
|
301
|
+
x: it.x,
|
|
302
|
+
y: it.y,
|
|
303
|
+
width: it.width,
|
|
304
|
+
height: it.height
|
|
305
|
+
});
|
|
306
|
+
const pageRectOf = (it, pageIndex) => {
|
|
307
|
+
const box = it.pages.find((p) => p.pageIndex === pageIndex) ?? it.pages[0];
|
|
308
|
+
return {
|
|
309
|
+
x: box.x,
|
|
310
|
+
y: box.y,
|
|
311
|
+
width: box.width,
|
|
312
|
+
height: box.height
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
/** The item shown for the cursor: the slice's only item (paged) / the full-scene item. */
|
|
316
|
+
const cursorItem = () => {
|
|
317
|
+
const sc = buildScene();
|
|
318
|
+
return paged() ? sc.items[0] : sc.items[itemIndexOfPage(ctx.getState().cursor)];
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* CONTENT-space rect on a page → WORLD rect: the same quarter-turn matrix
|
|
322
|
+
* `pageRectToScreen` uses, minus the camera — so a positioned reveal and
|
|
323
|
+
* the rendered overlay can never disagree about where a rect is.
|
|
324
|
+
*/
|
|
325
|
+
const worldRectForContent = (it, pageIndex, rect) => {
|
|
326
|
+
const box = it.pages.find((p) => p.pageIndex === pageIndex) ?? it.pages[0];
|
|
327
|
+
const content = (0, _embedpdf_core_geometry.displaySize)({
|
|
328
|
+
width: box.width,
|
|
329
|
+
height: box.height
|
|
330
|
+
}, box.rotation);
|
|
331
|
+
const wr = (0, _embedpdf_core_geometry.applyRect)((0, _embedpdf_core_geometry.rotateScaleMatrix)(box.contentScale, content.width, content.height, box.rotation), rect);
|
|
332
|
+
return {
|
|
333
|
+
x: box.x + wr.x,
|
|
334
|
+
y: box.y + wr.y,
|
|
335
|
+
width: wr.width,
|
|
336
|
+
height: wr.height
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* One axis of a positioned-reveal camera. `undefined` = 'nearest' (only
|
|
341
|
+
* move if the target is outside the padded view) — unless the zoom just
|
|
342
|
+
* changed, where "don't move" is meaningless and the spec's slack-axis
|
|
343
|
+
* rule (center) applies. 'keep' never moves the axis (PDF /XYZ null).
|
|
344
|
+
*/
|
|
345
|
+
const revealAxis = (a, camPos, rectPos, rectExtent, vpExtent, zoom, zoomChanged) => {
|
|
346
|
+
if (a === "keep") return camPos;
|
|
347
|
+
const p = pad();
|
|
348
|
+
if (a === void 0) {
|
|
349
|
+
if (!zoomChanged) {
|
|
350
|
+
const lo = camPos + p / zoom;
|
|
351
|
+
const hi = camPos + (vpExtent - p) / zoom;
|
|
352
|
+
if (rectPos >= lo && rectPos + rectExtent <= hi) return camPos;
|
|
353
|
+
if (rectExtent > hi - lo || rectPos < lo) return rectPos - p / zoom;
|
|
354
|
+
return rectPos + rectExtent - (vpExtent - p) / zoom;
|
|
355
|
+
}
|
|
356
|
+
a = "center";
|
|
357
|
+
}
|
|
358
|
+
if (a === "start") return rectPos - p / zoom;
|
|
359
|
+
if (a === "end") return rectPos + rectExtent - (vpExtent - p) / zoom;
|
|
360
|
+
const f = a === "center" ? .5 : Math.min(1, Math.max(0, a));
|
|
361
|
+
return rectPos + rectExtent / 2 - vpExtent * f / zoom;
|
|
362
|
+
};
|
|
363
|
+
const fits = (rect, zoom) => {
|
|
364
|
+
const v = vp();
|
|
365
|
+
const p = pad();
|
|
366
|
+
const eps = .5;
|
|
367
|
+
return rect.width * zoom <= v.width - 2 * p + eps && rect.height * zoom <= v.height - 2 * p + eps;
|
|
368
|
+
};
|
|
369
|
+
const alignFraction = (a) => a === "start" ? 0 : a === "center" ? .5 : a === "end" ? 1 : Math.min(1, Math.max(0, a));
|
|
370
|
+
const alignPoint = (al, v) => {
|
|
371
|
+
const rtl = ctx.getState().direction === "rtl";
|
|
372
|
+
const ax = rtl && al.x === "start" ? "end" : rtl && al.x === "end" ? "start" : al.x;
|
|
373
|
+
const p = pad();
|
|
374
|
+
return {
|
|
375
|
+
x: p + (v.width - 2 * p) * alignFraction(ax),
|
|
376
|
+
y: p + (v.height - 2 * p) * alignFraction(al.y)
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
const anchorPoint = () => alignPoint(ctx.getState().anchorAlign, vp());
|
|
380
|
+
/** Fit-box for resolving the zoom intent: whole scene (fit-all), the current item
|
|
381
|
+
* (paged — per-page fit), or the document max (continuous — doc-stable zoom). */
|
|
382
|
+
const fitBox = (item) => {
|
|
383
|
+
if (isFitAll()) return buildScene().size;
|
|
384
|
+
return paged() ? {
|
|
385
|
+
width: item.width,
|
|
386
|
+
height: item.height
|
|
387
|
+
} : buildScene().maxItemSize;
|
|
388
|
+
};
|
|
389
|
+
/** Clamp rect for a camera write targeting this item (scene-wide in continuous). */
|
|
390
|
+
const boundsFor = (it) => paged() ? itemRect(it) : sceneRect();
|
|
391
|
+
/** Clamp rect for a "stay" write (pan/zoom): the slice item (paged) / the scene. */
|
|
392
|
+
const stayBounds = () => paged() ? itemRect(buildScene().items[0]) : sceneRect();
|
|
393
|
+
const constraint = () => ({
|
|
394
|
+
bounded: ctx.getState().bounded,
|
|
395
|
+
padding: pad(),
|
|
396
|
+
fitAlign: ctx.getState().fitAlign,
|
|
397
|
+
direction: ctx.getState().direction
|
|
398
|
+
});
|
|
399
|
+
const setCam = (next, bounds = stayBounds()) => {
|
|
400
|
+
ctx.dispatch({
|
|
401
|
+
type: "CAMERA",
|
|
402
|
+
camera: _embedpdf_core_stage.clampCamera(next, bounds, vp(), constraint())
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Cursor reconciliation, one direction per interaction:
|
|
407
|
+
* navigation → cursor is INTENT, set explicitly; the camera honors it as far
|
|
408
|
+
* as the clamp allows — and a clamped camera never revokes it.
|
|
409
|
+
* manipulation → (pan / drag / pinch) the camera moves freely; the cursor is
|
|
410
|
+
* DERIVED from it. Only those verbs call this. Paged never syncs.
|
|
411
|
+
*/
|
|
412
|
+
const syncCursorFromCamera = () => {
|
|
413
|
+
if (paged()) return;
|
|
414
|
+
const sc = buildScene();
|
|
415
|
+
if (!sc.itemCount) return;
|
|
416
|
+
const page = _embedpdf_core_stage.anchorFromCamera(cam(), sc, vp()).pageIndex;
|
|
417
|
+
if (page !== ctx.getState().cursor) ctx.dispatch({
|
|
418
|
+
type: "CURSOR",
|
|
419
|
+
cursor: page
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
let raf = 0;
|
|
423
|
+
const cancelAnim = () => {
|
|
424
|
+
if (raf) {
|
|
425
|
+
scheduler.caf(raf);
|
|
426
|
+
raf = 0;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const lerp = (a, b, k) => a + (b - a) * k;
|
|
430
|
+
const animateTo = (target, bounds, ms = 240, then) => {
|
|
431
|
+
if (!canAnimate) {
|
|
432
|
+
setCam(target, bounds);
|
|
433
|
+
then?.();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
cancelAnim();
|
|
437
|
+
const from = cam();
|
|
438
|
+
const ease = (t) => 1 - Math.pow(1 - t, 3);
|
|
439
|
+
let started = false;
|
|
440
|
+
let t0 = 0;
|
|
441
|
+
const tick = (now) => {
|
|
442
|
+
if (!started) {
|
|
443
|
+
started = true;
|
|
444
|
+
t0 = now;
|
|
445
|
+
}
|
|
446
|
+
const k = ease(Math.min(1, (now - t0) / ms));
|
|
447
|
+
setCam({
|
|
448
|
+
x: lerp(from.x, target.x, k),
|
|
449
|
+
y: lerp(from.y, target.y, k),
|
|
450
|
+
zoom: lerp(from.zoom, target.zoom, k)
|
|
451
|
+
}, bounds);
|
|
452
|
+
if (k < 1) raf = scheduler.raf(tick);
|
|
453
|
+
else {
|
|
454
|
+
raf = 0;
|
|
455
|
+
then?.();
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
raf = scheduler.raf(tick);
|
|
459
|
+
};
|
|
460
|
+
const anchorAt = (at) => {
|
|
461
|
+
const sc = buildScene();
|
|
462
|
+
return sc.itemCount ? _embedpdf_core_stage.anchorFromCamera(cam(), sc, vp(), at) : {
|
|
463
|
+
pageIndex: ctx.getState().cursor,
|
|
464
|
+
fx: .5,
|
|
465
|
+
fy: 0
|
|
466
|
+
};
|
|
467
|
+
};
|
|
468
|
+
const currentAnchor = () => anchorAt(anchorPoint());
|
|
469
|
+
/**
|
|
470
|
+
* Run a placement whose RESOLVED zoom may change the scene (wrapped mode: zoom is
|
|
471
|
+
* a layout input). Re-run it until the scene it placed against is the scene that
|
|
472
|
+
* results — every non-wrapped mode is stable after the first pass by construction,
|
|
473
|
+
* and wrapped fit-modes converge on the second (the fit-box is wrap-independent).
|
|
474
|
+
* Capped at 3 passes for the one genuinely circular case (fit-all + wrapped).
|
|
475
|
+
*/
|
|
476
|
+
const stabilized = (place) => {
|
|
477
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
478
|
+
const scene = buildScene();
|
|
479
|
+
place();
|
|
480
|
+
if (buildScene() === scene) return;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const applyAnchor = (anchor, align) => {
|
|
484
|
+
stabilized(() => {
|
|
485
|
+
const scene = buildScene();
|
|
486
|
+
if (!scene.itemCount) return;
|
|
487
|
+
const item = scene.items[scene.itemOfPage(anchor.pageIndex)];
|
|
488
|
+
const zoom = _embedpdf_core_stage.resolveZoom(ctx.getState().zoom, fitBox(item), vp(), pad());
|
|
489
|
+
const at = alignPoint(align ?? ctx.getState().anchorAlign, vp());
|
|
490
|
+
setCam(_embedpdf_core_stage.cameraFromAnchor(anchor, scene, vp(), zoom, at), boundsFor(item));
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* Re-apply the view after a structural/zoom/viewport change. Normally anchor-
|
|
495
|
+
* preserving (keep looking at the same spot, held at the invariant point).
|
|
496
|
+
* Under fit-all the subject is the WHOLE scene, so "keep my anchor" is
|
|
497
|
+
* meaningless — re-place instead (this is what centers the scene even when
|
|
498
|
+
* unbounded).
|
|
499
|
+
*/
|
|
500
|
+
const reapply = (anchor, align) => {
|
|
501
|
+
if (isFitAll()) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
502
|
+
else applyAnchor(anchor, align);
|
|
503
|
+
};
|
|
504
|
+
const goToTarget = (pageIndex, opts) => {
|
|
505
|
+
cancelAnim();
|
|
506
|
+
const doc = ctx.document();
|
|
507
|
+
if (!doc || doc.pageCount === 0) return;
|
|
508
|
+
const target = Math.max(0, Math.min(pageIndex, doc.pageCount - 1));
|
|
509
|
+
if (target !== ctx.getState().cursor) ctx.dispatch({
|
|
510
|
+
type: "CURSOR",
|
|
511
|
+
cursor: target
|
|
512
|
+
});
|
|
513
|
+
if (opts?.viewpoint) {
|
|
514
|
+
ctx.dispatch({
|
|
515
|
+
type: "PATCH",
|
|
516
|
+
patch: { zoom: opts.viewpoint.zoom }
|
|
517
|
+
});
|
|
518
|
+
applyAnchor(opts.viewpoint.anchor);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const placement = () => {
|
|
522
|
+
const sc = buildScene();
|
|
523
|
+
if (!sc.itemCount) return null;
|
|
524
|
+
const item = paged() ? sc.items[0] : sc.items[itemIndexOfPage(target)];
|
|
525
|
+
const zoom = _embedpdf_core_stage.resolveZoom(ctx.getState().zoom, fitBox(item), vp(), pad());
|
|
526
|
+
const subject = isFitAll() ? sceneRect() : fits(itemRect(item), zoom) ? itemRect(item) : pageRectOf(item, target);
|
|
527
|
+
const want = {
|
|
528
|
+
...ctx.getState().arrivalAlign,
|
|
529
|
+
...opts?.arrivalAlign
|
|
530
|
+
};
|
|
531
|
+
const placed = _embedpdf_core_stage.placeCamera(subject, vp(), zoom, pad(), {
|
|
532
|
+
x: want.x === "keep" ? "start" : want.x,
|
|
533
|
+
y: want.y === "keep" ? "start" : want.y
|
|
534
|
+
}, ctx.getState().direction);
|
|
535
|
+
const bounds = boundsFor(item);
|
|
536
|
+
const settled = _embedpdf_core_stage.clampCamera(placed, bounds, vp(), {
|
|
537
|
+
...constraint(),
|
|
538
|
+
bounded: true
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
541
|
+
camera: {
|
|
542
|
+
zoom,
|
|
543
|
+
x: want.x === "keep" ? cam().x : settled.x,
|
|
544
|
+
y: want.y === "keep" ? cam().y : settled.y
|
|
545
|
+
},
|
|
546
|
+
bounds
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
if ((opts?.behavior ?? ctx.getState().scrollBehavior) === "smooth") {
|
|
550
|
+
const p = placement();
|
|
551
|
+
if (p) animateTo(p.camera, p.bounds);
|
|
552
|
+
} else stabilized(() => {
|
|
553
|
+
const p = placement();
|
|
554
|
+
if (p) setCam(p.camera, p.bounds);
|
|
555
|
+
});
|
|
556
|
+
};
|
|
557
|
+
/** Step by the navigation unit: the ITEM when it fits the viewport, else the PAGE. */
|
|
558
|
+
const step = (direction, opts) => {
|
|
559
|
+
const st = ctx.getState();
|
|
560
|
+
const item = cursorItem();
|
|
561
|
+
if (!item) return;
|
|
562
|
+
const { grouping: g } = grouping();
|
|
563
|
+
if (isFitAll() || fits(itemRect(item), cam().zoom)) {
|
|
564
|
+
const idx = Math.max(0, Math.min(itemIndexOfPage(st.cursor) + direction, g.length - 1));
|
|
565
|
+
goToTarget(g[idx][0], opts);
|
|
566
|
+
} else goToTarget(st.cursor + direction, opts);
|
|
567
|
+
};
|
|
568
|
+
const ponForIndex = (index) => ctx.document()?.pages[index]?.pageObjectNumber ?? index + 1;
|
|
569
|
+
const unrotatedPoints = (box) => {
|
|
570
|
+
const content = (0, _embedpdf_core_geometry.displaySize)({
|
|
571
|
+
width: box.width,
|
|
572
|
+
height: box.height
|
|
573
|
+
}, box.rotation);
|
|
574
|
+
return {
|
|
575
|
+
width: content.width / box.contentScale,
|
|
576
|
+
height: content.height / box.contentScale
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
const withTransform = (box) => {
|
|
580
|
+
const reg = ctx.document()?.pages[box.pageIndex];
|
|
581
|
+
const pageSize = reg ? {
|
|
582
|
+
width: reg.size.width,
|
|
583
|
+
height: reg.size.height
|
|
584
|
+
} : unrotatedPoints(box);
|
|
585
|
+
const c = cam();
|
|
586
|
+
const ratio = dpr();
|
|
587
|
+
const screenX = (0, _embedpdf_core_geometry.snapToDevice)((box.x - c.x) * c.zoom, ratio);
|
|
588
|
+
const screenY = (0, _embedpdf_core_geometry.snapToDevice)((box.y - c.y) * c.zoom, ratio);
|
|
589
|
+
const transform = (0, _embedpdf_core_geometry.pageTransform)({
|
|
590
|
+
pageSize,
|
|
591
|
+
rotation: box.rotation,
|
|
592
|
+
scale: box.contentScale * c.zoom,
|
|
593
|
+
baseScale: ctx.getState().viewUnitsPerPoint * (reg?.userUnit ?? 1),
|
|
594
|
+
dpr: ratio
|
|
595
|
+
});
|
|
596
|
+
const v = vp();
|
|
597
|
+
const onScreen = (0, _embedpdf_core_geometry.intersectRects)({
|
|
598
|
+
x: -screenX,
|
|
599
|
+
y: -screenY,
|
|
600
|
+
width: v.width,
|
|
601
|
+
height: v.height
|
|
602
|
+
}, {
|
|
603
|
+
x: 0,
|
|
604
|
+
y: 0,
|
|
605
|
+
width: transform.viewWidth,
|
|
606
|
+
height: transform.viewHeight
|
|
607
|
+
});
|
|
608
|
+
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.viewToPageRect(onScreen) : {
|
|
609
|
+
x: 0,
|
|
610
|
+
y: 0,
|
|
611
|
+
width: 0,
|
|
612
|
+
height: 0
|
|
613
|
+
};
|
|
614
|
+
return {
|
|
615
|
+
...box,
|
|
616
|
+
pon: ponForIndex(box.pageIndex),
|
|
617
|
+
screenX,
|
|
618
|
+
screenY,
|
|
619
|
+
transform,
|
|
620
|
+
visibleRect
|
|
621
|
+
};
|
|
622
|
+
};
|
|
623
|
+
let scrollMemo = null;
|
|
624
|
+
const scrollMetricsNow = () => {
|
|
625
|
+
const sc = buildScene();
|
|
626
|
+
const m = _embedpdf_core_stage.scrollMetrics(cam(), sc.itemCount ? stayBounds() : {
|
|
627
|
+
x: 0,
|
|
628
|
+
y: 0,
|
|
629
|
+
width: 0,
|
|
630
|
+
height: 0
|
|
631
|
+
}, vp(), pad());
|
|
632
|
+
const p = scrollMemo;
|
|
633
|
+
if (p && p.scrollLeft === m.scrollLeft && p.scrollTop === m.scrollTop && p.scrollWidth === m.scrollWidth && p.scrollHeight === m.scrollHeight && p.clientWidth === m.clientWidth && p.clientHeight === m.clientHeight) return p;
|
|
634
|
+
scrollMemo = m;
|
|
635
|
+
return m;
|
|
636
|
+
};
|
|
637
|
+
let visSig = "";
|
|
638
|
+
let vis = [];
|
|
639
|
+
const visiblePages = () => {
|
|
640
|
+
const c = cam();
|
|
641
|
+
const v = vp();
|
|
642
|
+
const sc = buildScene();
|
|
643
|
+
const items = paged() ? sc.items.slice(0, 1) : sc.query(_embedpdf_core_stage.cameraWorldRect(c, v));
|
|
644
|
+
const sig = `${sceneCache.key}|${ctx.getState().flow}|${c.x},${c.y},${c.zoom}|${v.width}x${v.height}|${dpr()}`;
|
|
645
|
+
if (sig === visSig) return vis;
|
|
646
|
+
visSig = sig;
|
|
647
|
+
vis = items.flatMap((it) => it.pages).map(withTransform);
|
|
648
|
+
return vis;
|
|
649
|
+
};
|
|
650
|
+
const pickSettings = (src) => {
|
|
651
|
+
const out = {};
|
|
652
|
+
for (const k of SETTING_KEYS) out[k] = src[k];
|
|
653
|
+
return out;
|
|
654
|
+
};
|
|
655
|
+
const snapshotSettings = () => pickSettings(ctx.getState());
|
|
656
|
+
const initialViewProviders = [];
|
|
657
|
+
let hasPlaced = false;
|
|
658
|
+
const api = {
|
|
659
|
+
camera: cam,
|
|
660
|
+
viewport: vp,
|
|
661
|
+
scrollMetrics: scrollMetricsNow,
|
|
662
|
+
pageCount: () => ctx.document()?.pageCount ?? 0,
|
|
663
|
+
visiblePages,
|
|
664
|
+
currentPage: () => ctx.getState().cursor,
|
|
665
|
+
currentItemPages: () => {
|
|
666
|
+
const item = cursorItem();
|
|
667
|
+
return item ? [...item.pageIndexes] : [];
|
|
668
|
+
},
|
|
669
|
+
pages: () => (ctx.document()?.pages ?? []).map((p) => ({
|
|
670
|
+
index: p.index,
|
|
671
|
+
pon: p.pageObjectNumber,
|
|
672
|
+
label: p.label ?? null
|
|
673
|
+
})),
|
|
674
|
+
pageRect: (pon) => {
|
|
675
|
+
const meta = ctx.document();
|
|
676
|
+
const index = meta ? meta.pages.findIndex((p) => p.pageObjectNumber === pon) : -1;
|
|
677
|
+
if (index < 0) return null;
|
|
678
|
+
const sc = buildScene();
|
|
679
|
+
if (!sc.itemCount) return null;
|
|
680
|
+
const box = sc.items[sc.itemOfPage(index)].pages.find((p) => p.pageIndex === index);
|
|
681
|
+
return box ? withTransform(box) : null;
|
|
682
|
+
},
|
|
683
|
+
pageAt: (screen) => {
|
|
684
|
+
for (const p of visiblePages()) {
|
|
685
|
+
const lx = screen.x - p.screenX;
|
|
686
|
+
const ly = screen.y - p.screenY;
|
|
687
|
+
if (lx >= 0 && ly >= 0 && lx <= p.transform.viewWidth && ly <= p.transform.viewHeight) return {
|
|
688
|
+
pon: p.pon,
|
|
689
|
+
point: p.transform.viewToPage({
|
|
690
|
+
x: lx,
|
|
691
|
+
y: ly
|
|
692
|
+
}),
|
|
693
|
+
scale: p.transform.viewScale,
|
|
694
|
+
rotation: p.rotation,
|
|
695
|
+
zoom: p.transform.zoom
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
},
|
|
700
|
+
pointOnPage: (pon, screen) => {
|
|
701
|
+
const p = visiblePages().find((v) => v.pon === pon);
|
|
702
|
+
if (!p) return null;
|
|
703
|
+
return p.transform.viewToPage({
|
|
704
|
+
x: screen.x - p.screenX,
|
|
705
|
+
y: screen.y - p.screenY
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
pageToWorld: (pon, pt) => {
|
|
709
|
+
const pr = api.pageRect(pon);
|
|
710
|
+
if (!pr) return null;
|
|
711
|
+
const content = (0, _embedpdf_core_geometry.displaySize)({
|
|
712
|
+
width: pr.width,
|
|
713
|
+
height: pr.height
|
|
714
|
+
}, pr.rotation);
|
|
715
|
+
const offset = (0, _embedpdf_core_geometry.applyPoint)((0, _embedpdf_core_geometry.rotateScaleMatrix)(pr.contentScale, content.width, content.height, pr.rotation), pt);
|
|
716
|
+
return {
|
|
717
|
+
x: pr.x + offset.x,
|
|
718
|
+
y: pr.y + offset.y
|
|
719
|
+
};
|
|
720
|
+
},
|
|
721
|
+
pageRectToScreen: (pon, rect) => {
|
|
722
|
+
const pr = api.pageRect(pon);
|
|
723
|
+
if (!pr) return null;
|
|
724
|
+
const content = (0, _embedpdf_core_geometry.displaySize)({
|
|
725
|
+
width: pr.width,
|
|
726
|
+
height: pr.height
|
|
727
|
+
}, pr.rotation);
|
|
728
|
+
const wr = (0, _embedpdf_core_geometry.applyRect)((0, _embedpdf_core_geometry.rotateScaleMatrix)(pr.contentScale, content.width, content.height, pr.rotation), rect);
|
|
729
|
+
const c = cam();
|
|
730
|
+
const tl = _embedpdf_core_stage.toScreen(c, {
|
|
731
|
+
x: pr.x + wr.x,
|
|
732
|
+
y: pr.y + wr.y
|
|
733
|
+
});
|
|
734
|
+
return {
|
|
735
|
+
x: tl.x,
|
|
736
|
+
y: tl.y,
|
|
737
|
+
width: wr.width * c.zoom,
|
|
738
|
+
height: wr.height * c.zoom
|
|
739
|
+
};
|
|
740
|
+
},
|
|
741
|
+
toScreen: (w) => _embedpdf_core_stage.toScreen(cam(), w),
|
|
742
|
+
toWorld: (s) => _embedpdf_core_stage.toWorld(cam(), s),
|
|
743
|
+
flow: () => ctx.getState().flow,
|
|
744
|
+
layout: () => ctx.getState().layout,
|
|
745
|
+
spread: () => ctx.getState().spread,
|
|
746
|
+
sizing: () => ctx.getState().sizing,
|
|
747
|
+
columns: () => ctx.getState().columns,
|
|
748
|
+
bounded: () => ctx.getState().bounded,
|
|
749
|
+
padding: () => ctx.getState().padding,
|
|
750
|
+
gap: () => ctx.getState().gap,
|
|
751
|
+
pageFrame: () => ctx.getState().pageFrame,
|
|
752
|
+
fitAlign: () => ctx.getState().fitAlign,
|
|
753
|
+
arrivalAlign: () => ctx.getState().arrivalAlign,
|
|
754
|
+
zoomAlign: () => ctx.getState().zoomAlign,
|
|
755
|
+
anchorAlign: () => ctx.getState().anchorAlign,
|
|
756
|
+
direction: () => ctx.getState().direction,
|
|
757
|
+
scrollBehavior: () => ctx.getState().scrollBehavior,
|
|
758
|
+
viewRotation: () => ctx.getState().viewRotation,
|
|
759
|
+
zoomLevel: () => cam().zoom,
|
|
760
|
+
zoomMode: () => {
|
|
761
|
+
const z = ctx.getState().zoom;
|
|
762
|
+
return "mode" in z ? z.mode : "custom";
|
|
763
|
+
},
|
|
764
|
+
viewpoint: () => ({
|
|
765
|
+
anchor: currentAnchor(),
|
|
766
|
+
zoom: ctx.getState().zoom
|
|
767
|
+
}),
|
|
768
|
+
settings: snapshotSettings,
|
|
769
|
+
viewState: () => ({
|
|
770
|
+
...snapshotSettings(),
|
|
771
|
+
cursor: ctx.getState().cursor,
|
|
772
|
+
anchor: currentAnchor()
|
|
773
|
+
}),
|
|
774
|
+
setViewport: (v) => {
|
|
775
|
+
if (!hasPlaced) {
|
|
776
|
+
ctx.dispatch({
|
|
777
|
+
type: "VP",
|
|
778
|
+
vp: v
|
|
779
|
+
});
|
|
780
|
+
if (v.width > 0 && v.height > 0 && (ctx.document()?.pageCount ?? 0) > 0) api.placeInitial();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
cancelAnim();
|
|
784
|
+
const anchor = currentAnchor();
|
|
785
|
+
ctx.dispatch({
|
|
786
|
+
type: "VP",
|
|
787
|
+
vp: v
|
|
788
|
+
});
|
|
789
|
+
reapply(anchor);
|
|
790
|
+
},
|
|
791
|
+
setDevicePixelRatio: (ratio) => {
|
|
792
|
+
if (ratio > 0 && ratio !== dpr()) ctx.dispatch({
|
|
793
|
+
type: "DPR",
|
|
794
|
+
dpr: ratio
|
|
795
|
+
});
|
|
796
|
+
},
|
|
797
|
+
setCamera: (c) => {
|
|
798
|
+
cancelAnim();
|
|
799
|
+
setCam(c);
|
|
800
|
+
syncCursorFromCamera();
|
|
801
|
+
},
|
|
802
|
+
panBy: (dx, dy) => {
|
|
803
|
+
cancelAnim();
|
|
804
|
+
setCam(_embedpdf_core_stage.panByScreen(cam(), dx, dy));
|
|
805
|
+
syncCursorFromCamera();
|
|
806
|
+
},
|
|
807
|
+
scrollTo: (opts) => {
|
|
808
|
+
cancelAnim();
|
|
809
|
+
if (!buildScene().itemCount) return;
|
|
810
|
+
const next = _embedpdf_core_stage.cameraFromScroll(cam(), stayBounds(), vp(), pad(), opts);
|
|
811
|
+
if (opts.behavior === "smooth") animateTo(next, stayBounds(), 240, syncCursorFromCamera);
|
|
812
|
+
else {
|
|
813
|
+
setCam(next);
|
|
814
|
+
syncCursorFromCamera();
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
scrollBy: ({ left, top, behavior }) => {
|
|
818
|
+
const m = scrollMetricsNow();
|
|
819
|
+
api.scrollTo({
|
|
820
|
+
left: left === void 0 ? void 0 : m.scrollLeft + left,
|
|
821
|
+
top: top === void 0 ? void 0 : m.scrollTop + top,
|
|
822
|
+
behavior
|
|
823
|
+
});
|
|
824
|
+
},
|
|
825
|
+
zoomAround: (pt, factor) => {
|
|
826
|
+
cancelAnim();
|
|
827
|
+
const before = buildScene();
|
|
828
|
+
const focal = before.itemCount ? _embedpdf_core_stage.anchorAtPoint(before, _embedpdf_core_stage.toWorld(cam(), pt)) : null;
|
|
829
|
+
setCam(_embedpdf_core_stage.zoomAround(cam(), pt, factor));
|
|
830
|
+
ctx.dispatch({
|
|
831
|
+
type: "PATCH",
|
|
832
|
+
patch: { zoom: { level: cam().zoom } }
|
|
833
|
+
});
|
|
834
|
+
const after = buildScene();
|
|
835
|
+
if (after !== before && focal && after.itemCount) setCam(_embedpdf_core_stage.cameraForAnchorAtScreen(focal, after, pt, cam().zoom));
|
|
836
|
+
syncCursorFromCamera();
|
|
837
|
+
},
|
|
838
|
+
zoomIn: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1.2),
|
|
839
|
+
zoomOut: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1 / 1.2),
|
|
840
|
+
zoomTo: (spec) => api.update({ zoom: spec }),
|
|
841
|
+
fitWidth: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitWidth } }),
|
|
842
|
+
fitPage: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitPage } }),
|
|
843
|
+
fitAll: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitAll } }),
|
|
844
|
+
automatic: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.Automatic } }),
|
|
845
|
+
refit: () => {
|
|
846
|
+
if (!hasPlaced) return;
|
|
847
|
+
cancelAnim();
|
|
848
|
+
reapply(currentAnchor());
|
|
849
|
+
},
|
|
850
|
+
goToPage: (pageIndex, opts) => goToTarget(pageIndex, opts),
|
|
851
|
+
reveal: (pageIndex, opts) => {
|
|
852
|
+
const doc = ctx.document();
|
|
853
|
+
if (!doc || doc.pageCount === 0) return;
|
|
854
|
+
const target = Math.max(0, Math.min(pageIndex, doc.pageCount - 1));
|
|
855
|
+
if (!(!!opts && (opts.rect !== void 0 || opts.anchor !== void 0 || opts.zoom !== void 0 && opts.zoom !== "keep"))) {
|
|
856
|
+
if (paged()) {
|
|
857
|
+
goToTarget(target, opts);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const sc = buildScene();
|
|
861
|
+
if (!sc.itemCount) return;
|
|
862
|
+
const page = pageRectOf(sc.items[itemIndexOfPage(target)], target);
|
|
863
|
+
const m = worldPageFrame();
|
|
864
|
+
const box = {
|
|
865
|
+
x: page.x - m.left,
|
|
866
|
+
y: page.y - m.top,
|
|
867
|
+
width: page.width + m.left + m.right,
|
|
868
|
+
height: page.height + m.top + m.bottom
|
|
869
|
+
};
|
|
870
|
+
const camera = _embedpdf_core_stage.revealCamera(cam(), box, vp(), pad());
|
|
871
|
+
const current = cam();
|
|
872
|
+
if (camera.x === current.x && camera.y === current.y) return;
|
|
873
|
+
cancelAnim();
|
|
874
|
+
if ((opts?.behavior ?? ctx.getState().scrollBehavior) === "smooth") animateTo(camera, sceneRect());
|
|
875
|
+
else setCam(camera, sceneRect());
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
cancelAnim();
|
|
879
|
+
if (target !== ctx.getState().cursor) ctx.dispatch({
|
|
880
|
+
type: "CURSOR",
|
|
881
|
+
cursor: target
|
|
882
|
+
});
|
|
883
|
+
const place = () => {
|
|
884
|
+
const sc = buildScene();
|
|
885
|
+
if (!sc.itemCount) return null;
|
|
886
|
+
const item = paged() ? sc.items[0] : sc.items[itemIndexOfPage(target)];
|
|
887
|
+
const world = opts.rect ? worldRectForContent(item, target, opts.rect) : pageRectOf(item, target);
|
|
888
|
+
const zd = opts.zoom ?? "keep";
|
|
889
|
+
const availW = Math.max(1, vp().width - 2 * pad());
|
|
890
|
+
const availH = Math.max(1, vp().height - 2 * pad());
|
|
891
|
+
let zoom = typeof zd === "object" ? zd.level : zd === "fit" ? Math.min(availW / world.width, availH / world.height) : zd === "fit-width" ? availW / world.width : zd === "fit-height" ? availH / world.height : cam().zoom;
|
|
892
|
+
if (!Number.isFinite(zoom) || zoom <= 0) zoom = cam().zoom;
|
|
893
|
+
const zoomChanged = zd !== "keep";
|
|
894
|
+
const a = opts.anchor ?? {};
|
|
895
|
+
return {
|
|
896
|
+
camera: {
|
|
897
|
+
x: revealAxis(a.x, cam().x, world.x, world.width, vp().width, zoom, zoomChanged),
|
|
898
|
+
y: revealAxis(a.y, cam().y, world.y, world.height, vp().height, zoom, zoomChanged),
|
|
899
|
+
zoom
|
|
900
|
+
},
|
|
901
|
+
bounds: boundsFor(item),
|
|
902
|
+
zoomChanged
|
|
903
|
+
};
|
|
904
|
+
};
|
|
905
|
+
const first = place();
|
|
906
|
+
if (!first) return;
|
|
907
|
+
if (first.zoomChanged) ctx.dispatch({
|
|
908
|
+
type: "PATCH",
|
|
909
|
+
patch: { zoom: { level: first.camera.zoom } }
|
|
910
|
+
});
|
|
911
|
+
if ((opts.behavior ?? ctx.getState().scrollBehavior) === "smooth") {
|
|
912
|
+
const p = place() ?? first;
|
|
913
|
+
animateTo(p.camera, p.bounds);
|
|
914
|
+
} else stabilized(() => {
|
|
915
|
+
const p = place();
|
|
916
|
+
if (p) setCam(p.camera, p.bounds);
|
|
917
|
+
});
|
|
918
|
+
},
|
|
919
|
+
next: (opts) => step(1, opts),
|
|
920
|
+
prev: (opts) => step(-1, opts),
|
|
921
|
+
update: (patch) => {
|
|
922
|
+
cancelAnim();
|
|
923
|
+
const touched = (effect) => SETTING_KEYS.some((k) => patch[k] !== void 0 && SETTINGS_EFFECT[k] === effect);
|
|
924
|
+
const align = touched("refit") && !touched("scene") && !touched("reflow") ? ctx.getState().zoomAlign : ctx.getState().anchorAlign;
|
|
925
|
+
const anchor = anchorAt(alignPoint(align, vp()));
|
|
926
|
+
ctx.dispatch({
|
|
927
|
+
type: "PATCH",
|
|
928
|
+
patch
|
|
929
|
+
});
|
|
930
|
+
if (touched("scene")) sceneCache = null;
|
|
931
|
+
if (touched("reflow")) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
932
|
+
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
933
|
+
else if (touched("reclamp")) setCam(cam());
|
|
934
|
+
},
|
|
935
|
+
setFlow: (flow) => api.update({ flow }),
|
|
936
|
+
setLayout: (layout) => api.update({ layout }),
|
|
937
|
+
setSpread: (spread) => api.update({ spread }),
|
|
938
|
+
setSizing: (sizing) => api.update({ sizing }),
|
|
939
|
+
setColumns: (columns) => api.update({ columns }),
|
|
940
|
+
setBounded: (bounded) => api.update({ bounded }),
|
|
941
|
+
setPadding: (padding) => api.update({ padding }),
|
|
942
|
+
setGap: (gap) => api.update({ gap }),
|
|
943
|
+
setPageFrame: (pageFrame) => api.update({ pageFrame }),
|
|
944
|
+
setFitAlign: (fitAlign) => api.update({ fitAlign }),
|
|
945
|
+
setArrivalAlign: (arrivalAlign) => api.update({ arrivalAlign }),
|
|
946
|
+
setZoomAlign: (zoomAlign) => api.update({ zoomAlign }),
|
|
947
|
+
setAnchorAlign: (anchorAlign) => api.update({ anchorAlign }),
|
|
948
|
+
setDirection: (direction) => api.update({ direction }),
|
|
949
|
+
setViewRotation: (viewRotation) => api.update({ viewRotation }),
|
|
950
|
+
rotateView: (delta) => api.setViewRotation((0, _embedpdf_core_geometry.addRotations)(ctx.getState().viewRotation, delta === 90 ? 90 : 270)),
|
|
951
|
+
setScrollBehavior: (behavior) => api.update({ scrollBehavior: behavior }),
|
|
952
|
+
applyViewState: (view) => {
|
|
953
|
+
cancelAnim();
|
|
954
|
+
ctx.dispatch({
|
|
955
|
+
type: "PATCH",
|
|
956
|
+
patch: pickSettings(view)
|
|
957
|
+
});
|
|
958
|
+
ctx.dispatch({
|
|
959
|
+
type: "CURSOR",
|
|
960
|
+
cursor: view.cursor ?? 0
|
|
961
|
+
});
|
|
962
|
+
sceneCache = null;
|
|
963
|
+
applyAnchor(view.anchor);
|
|
964
|
+
},
|
|
965
|
+
provideInitialView: (priority, fn) => {
|
|
966
|
+
initialViewProviders.push({
|
|
967
|
+
priority,
|
|
968
|
+
fn
|
|
969
|
+
});
|
|
970
|
+
},
|
|
971
|
+
placeInitial: () => {
|
|
972
|
+
hasPlaced = true;
|
|
973
|
+
const sorted = [...initialViewProviders].sort((a, b) => b.priority - a.priority);
|
|
974
|
+
for (const p of sorted) {
|
|
975
|
+
const view = p.fn();
|
|
976
|
+
if (view) {
|
|
977
|
+
api.applyViewState(view);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
api.resetView();
|
|
982
|
+
},
|
|
983
|
+
resetView: () => goToTarget(0, { behavior: "instant" })
|
|
984
|
+
};
|
|
985
|
+
return api;
|
|
986
|
+
}
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/scroll-handler.ts
|
|
989
|
+
/**
|
|
990
|
+
* Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
|
|
991
|
+
* (the `scroll` tag) and — with `panFallback` — under every tool, where it then
|
|
992
|
+
* captures ONLY page-gap drags (on-page downs belong to the active tool, which
|
|
993
|
+
* claims them first at higher priority). It uses the sample's viewport delta, so
|
|
994
|
+
* it pans over the whole viewport (pages and gaps).
|
|
995
|
+
*
|
|
996
|
+
* Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
|
|
997
|
+
* hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
|
|
998
|
+
* from its `Tool.cursor`, so hovering it needs no claim here.
|
|
999
|
+
*/
|
|
1000
|
+
function createScrollHandler(stage, interaction, options = {}) {
|
|
1001
|
+
const panFallback = options.panFallback ?? true;
|
|
1002
|
+
let last = {
|
|
1003
|
+
x: 0,
|
|
1004
|
+
y: 0
|
|
1005
|
+
};
|
|
1006
|
+
const isPanTool = () => interaction.activeTool().enables.has("scroll");
|
|
1007
|
+
return {
|
|
1008
|
+
id: "stage-scroll",
|
|
1009
|
+
priority: 10,
|
|
1010
|
+
enabledFor: (tool) => tool.enables.has("scroll") || panFallback,
|
|
1011
|
+
onDown: (s) => {
|
|
1012
|
+
if (!isPanTool() && s.page) return false;
|
|
1013
|
+
last = s.viewport;
|
|
1014
|
+
interaction.setCursor("stage-grab", "grabbing", 40);
|
|
1015
|
+
return true;
|
|
1016
|
+
},
|
|
1017
|
+
onMove: (s) => {
|
|
1018
|
+
stage.panBy(s.viewport.x - last.x, s.viewport.y - last.y);
|
|
1019
|
+
last = s.viewport;
|
|
1020
|
+
},
|
|
1021
|
+
onUp: () => interaction.setCursor("stage-grab", null),
|
|
1022
|
+
onHover: (s) => {
|
|
1023
|
+
if (panFallback && !isPanTool()) interaction.setCursor("stage-pan-fallback", s.page ? null : "grab", 5);
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/effects.ts
|
|
1029
|
+
/**
|
|
1030
|
+
* The one effect a stage lens needs: when the document's page registry changes
|
|
1031
|
+
* (rotate/move/delete bump `DocumentMeta.revision`), the page geometry under
|
|
1032
|
+
* this lens just changed, so re-fit — re-resolve the active zoom intent and
|
|
1033
|
+
* re-place against the new footprint.
|
|
1034
|
+
*
|
|
1035
|
+
* Without it, fit/pixel zoom modes stay anchored to the pre-mutation dimensions:
|
|
1036
|
+
* `pageWidth: 110` renders wider after a 90° rotation (width↔height swap), and
|
|
1037
|
+
* `fitPage` over- or under-fills. The scene already re-keys on the revision, so
|
|
1038
|
+
* only the resolved `cam.zoom` was stale; `refit()` recomputes it.
|
|
1039
|
+
*
|
|
1040
|
+
* Steady-state only: this reacts to revision CHANGES, never the initial value,
|
|
1041
|
+
* so it can't race the capability's level-triggered initial placement (the
|
|
1042
|
+
* reason the rest of the plugin is effect-free).
|
|
1043
|
+
*/
|
|
1044
|
+
function registerStageEffects(ctx, token) {
|
|
1045
|
+
ctx.watch(() => ctx.document()?.revision ?? 0, () => ctx.get(token).refit());
|
|
1046
|
+
}
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/reducer.ts
|
|
1049
|
+
const initialStageState = (config) => {
|
|
1050
|
+
const { scheduler: _scheduler, ...overrides } = config;
|
|
1051
|
+
return {
|
|
1052
|
+
camera: {
|
|
1053
|
+
x: 0,
|
|
1054
|
+
y: 0,
|
|
1055
|
+
zoom: 1
|
|
1056
|
+
},
|
|
1057
|
+
vp: {
|
|
1058
|
+
width: 0,
|
|
1059
|
+
height: 0
|
|
1060
|
+
},
|
|
1061
|
+
dpr: 1,
|
|
1062
|
+
cursor: 0,
|
|
1063
|
+
...DEFAULT_SETTINGS,
|
|
1064
|
+
...overrides
|
|
1065
|
+
};
|
|
1066
|
+
};
|
|
1067
|
+
/** Merge a settings patch, ignoring undefined values (safe for partial restores). */
|
|
1068
|
+
const applyPatch = (state, patch) => {
|
|
1069
|
+
const next = { ...state };
|
|
1070
|
+
let key;
|
|
1071
|
+
for (key in patch) {
|
|
1072
|
+
const value = patch[key];
|
|
1073
|
+
if (value !== void 0) Object.assign(next, { [key]: value });
|
|
1074
|
+
}
|
|
1075
|
+
return next;
|
|
1076
|
+
};
|
|
1077
|
+
/**
|
|
1078
|
+
* Pure. Every transition is here; nothing else mutates Stage state. Settings are a
|
|
1079
|
+
* flat bag of primitives — one PATCH action sets any subset (the capability decides
|
|
1080
|
+
* what camera follow-up, if any, each change needs).
|
|
1081
|
+
*/
|
|
1082
|
+
const stageReducer = (state, a) => {
|
|
1083
|
+
switch (a.type) {
|
|
1084
|
+
case "CAMERA": return {
|
|
1085
|
+
...state,
|
|
1086
|
+
camera: a.camera
|
|
1087
|
+
};
|
|
1088
|
+
case "VP": return {
|
|
1089
|
+
...state,
|
|
1090
|
+
vp: a.vp
|
|
1091
|
+
};
|
|
1092
|
+
case "DPR": return {
|
|
1093
|
+
...state,
|
|
1094
|
+
dpr: a.dpr
|
|
1095
|
+
};
|
|
1096
|
+
case "CURSOR": return {
|
|
1097
|
+
...state,
|
|
1098
|
+
cursor: a.cursor
|
|
1099
|
+
};
|
|
1100
|
+
case "PATCH": return applyPatch(state, a.patch);
|
|
1101
|
+
default: return state;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
//#endregion
|
|
1105
|
+
//#region src/types.ts
|
|
1106
|
+
const StageToken = (0, _embedpdf_core.createCapabilityToken)("stage");
|
|
1107
|
+
//#endregion
|
|
1108
|
+
//#region src/stage.plugin.ts
|
|
1109
|
+
/**
|
|
1110
|
+
* Wires the parts into a kernel plugin. This file is the "manifest": it says what
|
|
1111
|
+
* the plugin IS (id, token, state, reducer, capability) — the how lives in the
|
|
1112
|
+
* sibling files.
|
|
1113
|
+
*/
|
|
1114
|
+
const stagePlugin = (options = {}) => {
|
|
1115
|
+
const { id = "stage", token = StageToken, interaction = false, panFallback = true, ...config } = options;
|
|
1116
|
+
return (0, _embedpdf_core.definePlugin)({
|
|
1117
|
+
id,
|
|
1118
|
+
token,
|
|
1119
|
+
scope: "document",
|
|
1120
|
+
optional: interaction ? [_embedpdf_plugin_interaction.InteractionToken] : void 0,
|
|
1121
|
+
initialState: () => initialStageState(config),
|
|
1122
|
+
reduce: stageReducer,
|
|
1123
|
+
capability: (ctx) => createStageCapability(ctx, config),
|
|
1124
|
+
init: interaction ? (ctx) => {
|
|
1125
|
+
const ix = ctx.tryGet(_embedpdf_plugin_interaction.InteractionToken);
|
|
1126
|
+
if (ix) ix.registerHandler(createScrollHandler(ctx.get(token), ix, { panFallback }));
|
|
1127
|
+
} : void 0,
|
|
1128
|
+
effects: (ctx) => registerStageEffects(ctx, token)
|
|
1129
|
+
});
|
|
1130
|
+
};
|
|
1131
|
+
//#endregion
|
|
1132
|
+
//#region src/destination.ts
|
|
1133
|
+
/**
|
|
1134
|
+
* PDF destination → reveal: the pure translator between the PDF protocol's
|
|
1135
|
+
* navigation vocabulary (ISO 32000-1 §12.3.2.2) and the Stage's arrival
|
|
1136
|
+
* primitive. Outline clicks, link annotations, and `/OpenAction` all
|
|
1137
|
+
* resolve (engine-side) to an explicit `PdfDestination`; this maps it onto
|
|
1138
|
+
* ONE `reveal(pageIndex, options)` call — no destination-specific camera
|
|
1139
|
+
* code exists anywhere else.
|
|
1140
|
+
*
|
|
1141
|
+
* The whole protocol collapses onto three knobs:
|
|
1142
|
+
* rect — what to look at (a point for /XYZ, a rect for /FitR, the
|
|
1143
|
+
* page for /Fit, the content bounding box for /FitB*)
|
|
1144
|
+
* zoom — 'keep' | {level} | 'fit' | 'fit-width' | 'fit-height'
|
|
1145
|
+
* anchor — where it lands; 'keep' encodes the spec's null-means-retain
|
|
1146
|
+
*
|
|
1147
|
+
* Coordinates convert PDF user space (y-up, absolute) → content space
|
|
1148
|
+
* (y-down, crop-relative) through the SAME `pdfToContent` matrix selection
|
|
1149
|
+
* and search use, so a destination and an overlay can never disagree.
|
|
1150
|
+
*/
|
|
1151
|
+
/**
|
|
1152
|
+
* Translate one explicit destination against its target page's layout.
|
|
1153
|
+
* `bbox` is the page's content BOUNDING box in CONTENT space, for the
|
|
1154
|
+
* `/FitB*` kinds — pass it when known; the crop box is the spec-tolerated
|
|
1155
|
+
* fallback until the engine exposes it.
|
|
1156
|
+
*/
|
|
1157
|
+
function destinationToReveal(dest, layout, bbox) {
|
|
1158
|
+
const geo = (0, _embedpdf_core_geometry.pageGeometry)({
|
|
1159
|
+
crop: layout.boxes.crop,
|
|
1160
|
+
rotation: layout.rotation,
|
|
1161
|
+
userUnit: layout.userUnit
|
|
1162
|
+
}, 1);
|
|
1163
|
+
const crop = layout.boxes.crop;
|
|
1164
|
+
const toContent = (x, y) => (0, _embedpdf_core_geometry.applyPoint)(geo.pdfToContent, {
|
|
1165
|
+
x,
|
|
1166
|
+
y
|
|
1167
|
+
});
|
|
1168
|
+
const page = {
|
|
1169
|
+
x: 0,
|
|
1170
|
+
y: 0,
|
|
1171
|
+
width: layout.size.width,
|
|
1172
|
+
height: layout.size.height
|
|
1173
|
+
};
|
|
1174
|
+
const box = bbox ?? page;
|
|
1175
|
+
const options = (() => {
|
|
1176
|
+
switch (dest.kind) {
|
|
1177
|
+
case "xyz": {
|
|
1178
|
+
const hasLeft = dest.left != null;
|
|
1179
|
+
const hasTop = dest.top != null;
|
|
1180
|
+
const p = toContent(dest.left ?? crop.left, dest.top ?? crop.top);
|
|
1181
|
+
return {
|
|
1182
|
+
rect: {
|
|
1183
|
+
x: p.x,
|
|
1184
|
+
y: p.y,
|
|
1185
|
+
width: 0,
|
|
1186
|
+
height: 0
|
|
1187
|
+
},
|
|
1188
|
+
anchor: {
|
|
1189
|
+
x: hasLeft ? "start" : "keep",
|
|
1190
|
+
y: hasTop ? "start" : "keep"
|
|
1191
|
+
},
|
|
1192
|
+
zoom: dest.zoom != null && dest.zoom !== 0 ? { level: dest.zoom } : "keep"
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
case "fit": return { zoom: "fit" };
|
|
1196
|
+
case "fitH": {
|
|
1197
|
+
const hasTop = dest.top != null;
|
|
1198
|
+
return {
|
|
1199
|
+
rect: {
|
|
1200
|
+
x: 0,
|
|
1201
|
+
y: hasTop ? toContent(crop.left, dest.top).y : 0,
|
|
1202
|
+
width: page.width,
|
|
1203
|
+
height: 0
|
|
1204
|
+
},
|
|
1205
|
+
zoom: "fit-width",
|
|
1206
|
+
anchor: { y: hasTop ? "start" : "keep" }
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
case "fitV": {
|
|
1210
|
+
const hasLeft = dest.left != null;
|
|
1211
|
+
return {
|
|
1212
|
+
rect: {
|
|
1213
|
+
x: hasLeft ? toContent(dest.left, crop.top).x : 0,
|
|
1214
|
+
y: 0,
|
|
1215
|
+
width: 0,
|
|
1216
|
+
height: page.height
|
|
1217
|
+
},
|
|
1218
|
+
zoom: "fit-height",
|
|
1219
|
+
anchor: { x: hasLeft ? "start" : "keep" }
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
case "fitR": {
|
|
1223
|
+
const tl = toContent(dest.left, dest.top);
|
|
1224
|
+
const br = toContent(dest.right, dest.bottom);
|
|
1225
|
+
return {
|
|
1226
|
+
rect: {
|
|
1227
|
+
x: Math.min(tl.x, br.x),
|
|
1228
|
+
y: Math.min(tl.y, br.y),
|
|
1229
|
+
width: Math.abs(br.x - tl.x),
|
|
1230
|
+
height: Math.abs(br.y - tl.y)
|
|
1231
|
+
},
|
|
1232
|
+
zoom: "fit"
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
case "fitB": return {
|
|
1236
|
+
rect: box,
|
|
1237
|
+
zoom: "fit"
|
|
1238
|
+
};
|
|
1239
|
+
case "fitBH": {
|
|
1240
|
+
const hasTop = dest.top != null;
|
|
1241
|
+
const y = hasTop ? toContent(crop.left, dest.top).y : box.y;
|
|
1242
|
+
return {
|
|
1243
|
+
rect: {
|
|
1244
|
+
x: box.x,
|
|
1245
|
+
y,
|
|
1246
|
+
width: box.width,
|
|
1247
|
+
height: 0
|
|
1248
|
+
},
|
|
1249
|
+
zoom: "fit-width",
|
|
1250
|
+
anchor: { y: hasTop ? "start" : "keep" }
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
case "fitBV": {
|
|
1254
|
+
const hasLeft = dest.left != null;
|
|
1255
|
+
return {
|
|
1256
|
+
rect: {
|
|
1257
|
+
x: hasLeft ? toContent(dest.left, crop.top).x : box.x,
|
|
1258
|
+
y: box.y,
|
|
1259
|
+
width: 0,
|
|
1260
|
+
height: box.height
|
|
1261
|
+
},
|
|
1262
|
+
zoom: "fit-height",
|
|
1263
|
+
anchor: { x: hasLeft ? "start" : "keep" }
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
})();
|
|
1268
|
+
return {
|
|
1269
|
+
pageIndex: layout.index,
|
|
1270
|
+
options
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/wheel.ts
|
|
1275
|
+
/** The per-notch step for discrete wheels — the zoom buttons' ratio. */
|
|
1276
|
+
const NOTCH = 1.2;
|
|
1277
|
+
/** Zoom factor for a ctrl/cmd wheel event (the caller decides zoom vs pan). */
|
|
1278
|
+
function wheelZoomFactor(e) {
|
|
1279
|
+
if (e.deltaMode !== 0) return Math.pow(NOTCH, -Math.sign(e.deltaY));
|
|
1280
|
+
if (e.ctrlKey) {
|
|
1281
|
+
if (Math.abs(e.deltaY) >= 100) return Math.pow(NOTCH, -Math.sign(e.deltaY));
|
|
1282
|
+
return Math.exp(-e.deltaY / 100);
|
|
1283
|
+
}
|
|
1284
|
+
return Math.exp(-e.deltaY * .0015);
|
|
1285
|
+
}
|
|
1286
|
+
//#endregion
|
|
1287
|
+
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
1288
|
+
exports.StageToken = StageToken;
|
|
1289
|
+
Object.defineProperty(exports, "ZoomMode", {
|
|
1290
|
+
enumerable: true,
|
|
1291
|
+
get: function() {
|
|
1292
|
+
return _embedpdf_core_stage.ZoomMode;
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
exports.destinationToReveal = destinationToReveal;
|
|
1296
|
+
exports.settingsEqual = settingsEqual;
|
|
1297
|
+
exports.stagePlugin = stagePlugin;
|
|
1298
|
+
exports.wheelZoomFactor = wheelZoomFactor;
|
|
1299
|
+
|
|
1300
|
+
//# sourceMappingURL=index.cjs.map
|