@embedpdf/plugin-stage 3.0.0-next.14 → 3.0.0-next.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -23
- package/dist/contract/host.cjs +16 -0
- package/dist/contract/host.cjs.map +1 -0
- package/dist/contract/host.d.cts +3 -0
- package/dist/contract/host.d.ts +3 -0
- package/dist/contract/host.js +8 -0
- package/dist/contract/host.js.map +1 -0
- package/dist/contract-B3QLVDlb.d.cts +575 -0
- package/dist/contract-BoOt8rJM.js +11 -0
- package/dist/contract-BoOt8rJM.js.map +1 -0
- package/dist/contract-CqrsZNsY.d.ts +575 -0
- package/dist/contract-iSNhoXLC.cjs +16 -0
- package/dist/contract-iSNhoXLC.cjs.map +1 -0
- package/dist/contract.cjs +1 -1
- package/dist/contract.d.cts +2 -3
- package/dist/contract.d.ts +2 -3
- package/dist/contract.js +1 -1
- package/dist/host-contract-C7ofnyzA.d.ts +95 -0
- package/dist/host-contract-Dm5_acFs.d.cts +95 -0
- package/dist/index.cjs +1815 -1419
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -64
- package/dist/index.d.ts +57 -64
- package/dist/index.js +1797 -1379
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/dist/contract-BU33R4oL.cjs +0 -13
- package/dist/contract-BU33R4oL.cjs.map +0 -1
- package/dist/contract-CPSX8ANN.js +0 -8
- package/dist/contract-CPSX8ANN.js.map +0 -1
- package/dist/destination.cjs +0 -147
- package/dist/destination.cjs.map +0 -1
- package/dist/destination.d.cts +0 -19
- package/dist/destination.d.ts +0 -19
- package/dist/destination.js +0 -146
- package/dist/destination.js.map +0 -1
- package/dist/types-CdIFKQnj.d.ts +0 -656
- package/dist/types-qdO_v-V-.d.cts +0 -656
package/dist/index.cjs
CHANGED
|
@@ -1,43 +1,150 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
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
|
-
const require_destination = require("./destination.cjs");
|
|
25
|
-
const require_contract = require("./contract-BU33R4oL.cjs");
|
|
2
|
+
const require_contract = require("./contract-iSNhoXLC.cjs");
|
|
26
3
|
let _embedpdf_core = require("@embedpdf/core");
|
|
27
4
|
let _embedpdf_plugin_actions_contract = require("@embedpdf/plugin-actions/contract");
|
|
28
5
|
let _embedpdf_plugin_actions_contract_host = require("@embedpdf/plugin-actions/contract/host");
|
|
29
6
|
let _embedpdf_core_stage = require("@embedpdf/core-stage");
|
|
30
|
-
_embedpdf_core_stage = __toESM(_embedpdf_core_stage, 1);
|
|
31
7
|
let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
8
|
+
//#region src/working-set.ts
|
|
9
|
+
/** A lens's working set, from the pages it lays out. */
|
|
10
|
+
function workingSetOf(pages) {
|
|
11
|
+
return pages.map(({ ref, visibleRect, transform }) => {
|
|
12
|
+
if (visibleRect.width <= 0 || visibleRect.height <= 0) return {
|
|
13
|
+
page: ref,
|
|
14
|
+
role: "near",
|
|
15
|
+
pixels: 0
|
|
16
|
+
};
|
|
17
|
+
const pixels = visibleRect.width * visibleRect.height * transform.renderScale ** 2;
|
|
18
|
+
return {
|
|
19
|
+
page: ref,
|
|
20
|
+
role: "visible",
|
|
21
|
+
visible: visibleRect,
|
|
22
|
+
pixels: Math.round(pixels)
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Tells the engine what the lens shows, now and whenever it changes: once per
|
|
28
|
+
* camera move, so at most once per input event while the camera moves.
|
|
29
|
+
*/
|
|
30
|
+
function connectWorkingSet(ctx, stage) {
|
|
31
|
+
let told = "";
|
|
32
|
+
const tell = (shown) => {
|
|
33
|
+
const pages = workingSetOf(shown);
|
|
34
|
+
const key = keyOf(pages);
|
|
35
|
+
if (key === told) return;
|
|
36
|
+
told = key;
|
|
37
|
+
ctx.doc.setWorkingSet(ctx.id, pages);
|
|
38
|
+
};
|
|
39
|
+
ctx.watch(() => stage.listVisiblePages(), tell);
|
|
40
|
+
tell(stage.listVisiblePages());
|
|
41
|
+
}
|
|
42
|
+
/** A working set as one comparable string. */
|
|
43
|
+
function keyOf(pages) {
|
|
44
|
+
return pages.map(({ page, role, visible, pixels }) => {
|
|
45
|
+
const part = visible ? `${Math.round(visible.x)},${Math.round(visible.y)},${Math.round(visible.width)},${Math.round(visible.height)}` : "";
|
|
46
|
+
return `${page.objectNumber}:${role}:${pixels}:${part}`;
|
|
47
|
+
}).join("|");
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/connect.ts
|
|
51
|
+
/** The id of the main lens; additional lenses register under their own id. */
|
|
52
|
+
const DEFAULT_LENS_ID = "stage";
|
|
53
|
+
function connectStage(ctx, stage) {
|
|
54
|
+
ctx.watch(() => ctx.document()?.revision ?? 0, () => stage.refit());
|
|
55
|
+
connectWorkingSet(ctx, stage);
|
|
56
|
+
if (ctx.id !== "stage") return;
|
|
57
|
+
const actions = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
|
|
58
|
+
if (!actions) return;
|
|
59
|
+
feedPageState(ctx, stage, actions);
|
|
60
|
+
registerNavigationExecutors(ctx, stage, actions);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The page-state feed. The stage is authoritative for what page the viewer is
|
|
64
|
+
* on; the actions plugin's lifecycle coordinator owns when page-lifecycle
|
|
65
|
+
* triggers fire. The feed reports `{ currentPage, visiblePages, placed, cause }`
|
|
66
|
+
* (page refs, not indexes, so reordering is safe) on placement and on every
|
|
67
|
+
* change; the coordinator buffers reports until the document has opened and
|
|
68
|
+
* diffs them against what it last emitted, so this side only reports.
|
|
69
|
+
*/
|
|
70
|
+
function feedPageState(ctx, stage, actions) {
|
|
71
|
+
const snapshot = () => {
|
|
72
|
+
const state = ctx.state.get();
|
|
73
|
+
if (!state.placed) return null;
|
|
74
|
+
return {
|
|
75
|
+
currentPage: stage.getCurrentPage(),
|
|
76
|
+
visiblePages: stage.listVisiblePages().map((page) => page.ref),
|
|
77
|
+
cause: state.motionCause
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
ctx.watch(() => {
|
|
81
|
+
const current = snapshot();
|
|
82
|
+
return current === null ? "unplaced" : `${current.currentPage?.objectNumber ?? -1}|${current.visiblePages.map((page) => page.objectNumber).sort((left, right) => left - right).join(",")}`;
|
|
83
|
+
}, () => {
|
|
84
|
+
const current = snapshot();
|
|
85
|
+
if (current === null) return;
|
|
86
|
+
actions.reportPageState({
|
|
87
|
+
currentPage: current.currentPage,
|
|
88
|
+
visiblePages: current.visiblePages,
|
|
89
|
+
placed: true,
|
|
90
|
+
cause: current.cause
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The GoTo and Named page verbs for the action engine. The dispatcher runs
|
|
96
|
+
* them as deferred navigation effects, after every document node of a tree
|
|
97
|
+
* succeeded, never in the middle of a walk. They move the view by its own
|
|
98
|
+
* `scrollBehavior` setting, as a link does. /N Print is not handled here: the
|
|
99
|
+
* dispatcher owns it (policy and the UI adapter).
|
|
100
|
+
*/
|
|
101
|
+
function registerNavigationExecutors(ctx, stage, actions) {
|
|
102
|
+
ctx.cleanup(actions.registerExecutor("goto", (node) => {
|
|
103
|
+
if (node.type !== "goto") return {
|
|
104
|
+
status: "inert",
|
|
105
|
+
reason: "not a goto node"
|
|
106
|
+
};
|
|
107
|
+
if (!ctx.getPage(node.destination.page)) return {
|
|
108
|
+
status: "failed",
|
|
109
|
+
error: "the destination page is not in this document"
|
|
110
|
+
};
|
|
111
|
+
stage.goToDestination(node.destination);
|
|
112
|
+
return { status: "executed" };
|
|
113
|
+
}));
|
|
114
|
+
ctx.cleanup(actions.registerExecutor("named", (node) => {
|
|
115
|
+
if (node.type !== "named") return {
|
|
116
|
+
status: "inert",
|
|
117
|
+
reason: "not a named node"
|
|
118
|
+
};
|
|
119
|
+
switch (node.name) {
|
|
120
|
+
case "NextPage":
|
|
121
|
+
stage.nextPage();
|
|
122
|
+
return { status: "executed" };
|
|
123
|
+
case "PrevPage":
|
|
124
|
+
stage.previousPage();
|
|
125
|
+
return { status: "executed" };
|
|
126
|
+
case "FirstPage":
|
|
127
|
+
stage.goToFirstPage();
|
|
128
|
+
return { status: "executed" };
|
|
129
|
+
case "LastPage":
|
|
130
|
+
stage.goToLastPage();
|
|
131
|
+
return { status: "executed" };
|
|
132
|
+
default: return {
|
|
133
|
+
status: "inert",
|
|
134
|
+
reason: `unknown named action '${node.name}'`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
32
140
|
//#region src/motion.ts
|
|
33
141
|
/**
|
|
34
|
-
* The camera's
|
|
142
|
+
* The camera's motion vocabulary, every formula pure.
|
|
35
143
|
*
|
|
36
|
-
* The capability owns the impure drivers (
|
|
37
|
-
* state); the
|
|
38
|
-
* with no state
|
|
39
|
-
*
|
|
40
|
-
* ever wants them.
|
|
144
|
+
* The capability owns the impure drivers (the clock's frames, state writes,
|
|
145
|
+
* gesture state); the laws those drivers integrate live here, parameterized
|
|
146
|
+
* by elapsed time with no state and no clock, so they are unit-testable in
|
|
147
|
+
* isolation and have no plugin dependencies.
|
|
41
148
|
*
|
|
42
149
|
* Two families:
|
|
43
150
|
* • ballistic — one per-axis law for every free motion: `glideStep` (the
|
|
@@ -45,119 +152,307 @@ let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
|
45
152
|
* critically-damped return to the clamp). A glide that reaches a content
|
|
46
153
|
* edge converts to a spring with its velocity carried in, so the bounce
|
|
47
154
|
* grows out of physics rather than a scripted overshoot.
|
|
48
|
-
* • resistance — the iOS rubber-band curve `rubberOut` and its exact
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
155
|
+
* • resistance — the iOS rubber-band curve `rubberOut` and its exact inverse
|
|
156
|
+
* `rubberIn`: dragging past an edge displays an asymptotically bounded
|
|
157
|
+
* stretch, and re-entering a gesture over a stretched camera (catching it
|
|
158
|
+
* mid-bounce) reconstructs the finger-integrated position the curve came
|
|
159
|
+
* from.
|
|
53
160
|
* Plus the tween interpolators: `easeOutCubic` for coordinate tweens and
|
|
54
|
-
* `zoomLerp
|
|
55
|
-
* ratios per unit time, not equal deltas
|
|
161
|
+
* `zoomLerp`, a geometric zoom interpolation (zoom is multiplicative: equal
|
|
162
|
+
* ratios per unit time, not equal deltas).
|
|
56
163
|
*/
|
|
57
164
|
/** Resistance coefficient of the rubber-band curve (the iOS feel). */
|
|
58
165
|
const RUBBER = .55;
|
|
59
|
-
/** Glide decay per ms
|
|
166
|
+
/** Glide decay per ms: UIScrollView's normal deceleration rate. */
|
|
60
167
|
const FLING_DECAY = .998;
|
|
61
|
-
/** rad/ms
|
|
168
|
+
/** rad/ms; critically damped, settling in about 400 ms. */
|
|
62
169
|
const SPRING_OMEGA = .015;
|
|
63
170
|
/**
|
|
64
|
-
* Overshoot distance → displayed stretch
|
|
65
|
-
* dimension
|
|
66
|
-
*
|
|
67
|
-
* invisible.
|
|
171
|
+
* Overshoot distance → displayed stretch, asymptotic to the viewport
|
|
172
|
+
* dimension, so the content never leaves the screen however far the finger
|
|
173
|
+
* travels. `rubberOut(0) === 0`: inside the bounds the curve is invisible.
|
|
68
174
|
*/
|
|
69
|
-
const rubberOut = (
|
|
70
|
-
/**
|
|
71
|
-
const rubberIn = (
|
|
175
|
+
const rubberOut = (distance, dimension) => (1 - 1 / (distance * RUBBER / dimension + 1)) * dimension;
|
|
176
|
+
/** The exact inverse of `rubberOut` (defined for `stretch < dimension`). */
|
|
177
|
+
const rubberIn = (stretch, dimension) => stretch * dimension / (RUBBER * (dimension - stretch));
|
|
72
178
|
/**
|
|
73
|
-
* One frame of free glide: exponential velocity decay, position advanced
|
|
74
|
-
* the midpoint integral of the decay over `
|
|
75
|
-
* frame-rate independent).
|
|
179
|
+
* One frame of free glide: exponential velocity decay, the position advanced
|
|
180
|
+
* by the midpoint integral of the decay over `elapsedMs` (exact for the
|
|
181
|
+
* exponential, so frame-rate independent).
|
|
76
182
|
*/
|
|
77
|
-
const glideStep = (
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
return Math.abs(
|
|
82
|
-
|
|
83
|
-
|
|
183
|
+
const glideStep = (position, velocity, elapsedMs) => {
|
|
184
|
+
const decay = Math.pow(FLING_DECAY, elapsedMs);
|
|
185
|
+
const nextPosition = position + velocity * (elapsedMs * (1 + decay) / 2);
|
|
186
|
+
const nextVelocity = velocity * decay;
|
|
187
|
+
return Math.abs(nextVelocity) < .02 ? {
|
|
188
|
+
position: nextPosition,
|
|
189
|
+
velocity: 0,
|
|
84
190
|
done: true
|
|
85
191
|
} : {
|
|
86
|
-
|
|
87
|
-
|
|
192
|
+
position: nextPosition,
|
|
193
|
+
velocity: nextVelocity,
|
|
88
194
|
done: false
|
|
89
195
|
};
|
|
90
196
|
};
|
|
91
197
|
/**
|
|
92
|
-
* One frame of critically-damped spring toward `edge
|
|
93
|
-
* on x'' = −ω²(x−e) − 2ωx'. Lands exactly
|
|
94
|
-
* velocity zeroed) once
|
|
198
|
+
* One frame of a critically-damped spring toward `edge`: semi-implicit Euler
|
|
199
|
+
* on x'' = −ω²(x−e) − 2ωx'. Lands exactly on the edge (position snapped,
|
|
200
|
+
* velocity zeroed) once within half a pixel at negligible speed.
|
|
95
201
|
*/
|
|
96
|
-
const springStep = (
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
if (Math.abs(
|
|
100
|
-
|
|
101
|
-
|
|
202
|
+
const springStep = (position, velocity, edge, elapsedMs) => {
|
|
203
|
+
const nextVelocity = velocity + (-.015 * SPRING_OMEGA * (position - edge) - 2 * SPRING_OMEGA * velocity) * elapsedMs;
|
|
204
|
+
const nextPosition = position + nextVelocity * elapsedMs;
|
|
205
|
+
if (Math.abs(nextPosition - edge) < .5 && Math.abs(nextVelocity) < .01) return {
|
|
206
|
+
position: edge,
|
|
207
|
+
velocity: 0,
|
|
102
208
|
done: true
|
|
103
209
|
};
|
|
104
210
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
211
|
+
position: nextPosition,
|
|
212
|
+
velocity: nextVelocity,
|
|
107
213
|
done: false
|
|
108
214
|
};
|
|
109
215
|
};
|
|
110
|
-
const easeOutCubic = (
|
|
216
|
+
const easeOutCubic = (progress) => 1 - Math.pow(1 - progress, 3);
|
|
217
|
+
/**
|
|
218
|
+
* Geometric zoom interpolation: `from·(to/from)^progress`. Zoom is
|
|
219
|
+
* multiplicative, so a constant-rate zoom moves equal ratios per unit time;
|
|
220
|
+
* interpolating the level linearly makes the start rush and the end crawl
|
|
221
|
+
* (and, under a focal anchor, swings the anchored point along a curve).
|
|
222
|
+
*/
|
|
223
|
+
const zoomLerp = (from, to, progress) => from * Math.pow(to / from, progress);
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/camera/animation.ts
|
|
111
226
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
227
|
+
* Camera motion over the host's frames (`ctx.clock`): the coordinate tween,
|
|
228
|
+
* the zoom-anchored tween (it animates the invariant, so the focal page-point
|
|
229
|
+
* holds still by construction), and the momentum and edge physics (glide,
|
|
230
|
+
* spring). One frame is waited for at a time: `cancelAnimation`, every verb's
|
|
231
|
+
* first act, is also "catch".
|
|
116
232
|
*/
|
|
117
|
-
|
|
233
|
+
/** The default tween duration. */
|
|
234
|
+
const TWEEN_MS = 240;
|
|
235
|
+
const lerp = (from, to, progress) => from + (to - from) * progress;
|
|
236
|
+
function createAnimation({ events, clock, scene }, write) {
|
|
237
|
+
const { motionEnded } = events;
|
|
238
|
+
const { camera, viewport, buildScene, stayBounds, constraint } = scene;
|
|
239
|
+
const { writeCamera, writeCameraUnclamped, axisTravels, syncCursorFromCamera } = write;
|
|
240
|
+
let cancelFrame = null;
|
|
241
|
+
const cancelAnimation = () => {
|
|
242
|
+
cancelFrame?.();
|
|
243
|
+
cancelFrame = null;
|
|
244
|
+
};
|
|
245
|
+
/** A tween or fling reached its natural end. */
|
|
246
|
+
const endMotion = () => {
|
|
247
|
+
cancelFrame = null;
|
|
248
|
+
motionEnded.emit({ camera: camera() });
|
|
249
|
+
};
|
|
250
|
+
const animateTo = (target, bounds, durationMs = TWEEN_MS, then) => {
|
|
251
|
+
if (!clock.hasFrames) {
|
|
252
|
+
writeCamera(target, bounds);
|
|
253
|
+
then?.();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
cancelAnimation();
|
|
257
|
+
const from = camera();
|
|
258
|
+
let started = false;
|
|
259
|
+
let startedAt = 0;
|
|
260
|
+
const tick = (now) => {
|
|
261
|
+
if (!started) {
|
|
262
|
+
started = true;
|
|
263
|
+
startedAt = now;
|
|
264
|
+
}
|
|
265
|
+
const progress = easeOutCubic(Math.min(1, (now - startedAt) / durationMs));
|
|
266
|
+
writeCamera({
|
|
267
|
+
x: lerp(from.x, target.x, progress),
|
|
268
|
+
y: lerp(from.y, target.y, progress),
|
|
269
|
+
zoom: lerp(from.zoom, target.zoom, progress)
|
|
270
|
+
}, bounds);
|
|
271
|
+
if (progress < 1) cancelFrame = clock.nextFrame(tick);
|
|
272
|
+
else {
|
|
273
|
+
endMotion();
|
|
274
|
+
then?.();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
cancelFrame = clock.nextFrame(tick);
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* The zoom-anchored tween animates the invariant, not the coordinates: the
|
|
281
|
+
* focal page-point stays at `point` while the scale changes. Lerping x, y
|
|
282
|
+
* and zoom independently breaks that mid-flight, because holding a screen
|
|
283
|
+
* point requires `x(t) = focal.x − point.x / zoom(t)`, hyperbolic in the
|
|
284
|
+
* zoom, so linear coordinates swing the tapped point along a curve (a
|
|
285
|
+
* visible dip). Here the zoom interpolates geometrically (zoom is
|
|
286
|
+
* multiplicative) and each frame's camera derives from the anchor at that
|
|
287
|
+
* zoom, then clamps: the focal point is stationary by construction, and a
|
|
288
|
+
* zoom-out rails into its fit as the shrinking travel range lets the clamp
|
|
289
|
+
* take over.
|
|
290
|
+
*/
|
|
291
|
+
const animateZoomAnchored = (focal, point, targetZoom, durationMs = TWEEN_MS, then) => {
|
|
292
|
+
if (!clock.hasFrames) {
|
|
293
|
+
const scene = buildScene();
|
|
294
|
+
if (scene.itemCount) writeCamera((0, _embedpdf_core_stage.cameraForAnchorAtScreen)(focal, scene, point, targetZoom));
|
|
295
|
+
then?.();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
cancelAnimation();
|
|
299
|
+
const startZoom = camera().zoom;
|
|
300
|
+
let started = false;
|
|
301
|
+
let startedAt = 0;
|
|
302
|
+
const tick = (now) => {
|
|
303
|
+
if (!started) {
|
|
304
|
+
started = true;
|
|
305
|
+
startedAt = now;
|
|
306
|
+
}
|
|
307
|
+
const progress = easeOutCubic(Math.min(1, (now - startedAt) / durationMs));
|
|
308
|
+
const zoom = zoomLerp(startZoom, targetZoom, progress);
|
|
309
|
+
const scene = buildScene();
|
|
310
|
+
if (scene.itemCount) writeCamera((0, _embedpdf_core_stage.cameraForAnchorAtScreen)(focal, scene, point, zoom));
|
|
311
|
+
if (progress < 1) cancelFrame = clock.nextFrame(tick);
|
|
312
|
+
else {
|
|
313
|
+
endMotion();
|
|
314
|
+
then?.();
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
cancelFrame = clock.nextFrame(tick);
|
|
318
|
+
};
|
|
319
|
+
const startCameraPhysics = (fingerVelocityX, fingerVelocityY) => {
|
|
320
|
+
if (!clock.hasFrames) {
|
|
321
|
+
writeCamera(camera());
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
cancelAnimation();
|
|
325
|
+
const zoom = camera().zoom;
|
|
326
|
+
const start = camera();
|
|
327
|
+
const startClamped = (0, _embedpdf_core_stage.clampCamera)(start, stayBounds(), viewport(), constraint());
|
|
328
|
+
const startBounds = stayBounds();
|
|
329
|
+
const startViewport = viewport();
|
|
330
|
+
const axisState = (cameraPosition, clampedPosition, fingerVelocity) => ({
|
|
331
|
+
position: cameraPosition * zoom,
|
|
332
|
+
velocity: -fingerVelocity / 1e3,
|
|
333
|
+
springing: cameraPosition !== clampedPosition,
|
|
334
|
+
done: false
|
|
335
|
+
});
|
|
336
|
+
const axisX = axisState(start.x, startClamped.x, axisTravels(startBounds.x, startBounds.width, startViewport.width, zoom) ? fingerVelocityX : 0);
|
|
337
|
+
const axisY = axisState(start.y, startClamped.y, axisTravels(startBounds.y, startBounds.height, startViewport.height, zoom) ? fingerVelocityY : 0);
|
|
338
|
+
if (!axisX.springing && !axisY.springing && Math.hypot(axisX.velocity, axisY.velocity) < .02) return;
|
|
339
|
+
let started = false;
|
|
340
|
+
let lastFrameAt = 0;
|
|
341
|
+
const tick = (now) => {
|
|
342
|
+
cancelFrame = null;
|
|
343
|
+
const elapsedMs = started ? Math.min(64, Math.max(1, now - lastFrameAt)) : 16;
|
|
344
|
+
started = true;
|
|
345
|
+
lastFrameAt = now;
|
|
346
|
+
const home = (0, _embedpdf_core_stage.clampCamera)({
|
|
347
|
+
x: axisX.position / zoom,
|
|
348
|
+
y: axisY.position / zoom,
|
|
349
|
+
zoom
|
|
350
|
+
}, stayBounds(), viewport(), constraint());
|
|
351
|
+
const step = (axis, edgeWorld) => {
|
|
352
|
+
if (axis.done) return;
|
|
353
|
+
const next = axis.springing ? springStep(axis.position, axis.velocity, edgeWorld * zoom, elapsedMs) : glideStep(axis.position, axis.velocity, elapsedMs);
|
|
354
|
+
axis.position = next.position;
|
|
355
|
+
axis.velocity = next.velocity;
|
|
356
|
+
axis.done = next.done;
|
|
357
|
+
};
|
|
358
|
+
step(axisX, home.x);
|
|
359
|
+
step(axisY, home.y);
|
|
360
|
+
const nextHome = (0, _embedpdf_core_stage.clampCamera)({
|
|
361
|
+
x: axisX.position / zoom,
|
|
362
|
+
y: axisY.position / zoom,
|
|
363
|
+
zoom
|
|
364
|
+
}, stayBounds(), viewport(), constraint());
|
|
365
|
+
const convert = (axis, clampedPosition) => {
|
|
366
|
+
if (!axis.springing && Math.abs(clampedPosition * zoom - axis.position) > 1e-6) {
|
|
367
|
+
axis.position = clampedPosition * zoom;
|
|
368
|
+
axis.springing = true;
|
|
369
|
+
axis.done = false;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
convert(axisX, nextHome.x);
|
|
373
|
+
convert(axisY, nextHome.y);
|
|
374
|
+
writeCameraUnclamped({
|
|
375
|
+
x: axisX.position / zoom,
|
|
376
|
+
y: axisY.position / zoom,
|
|
377
|
+
zoom
|
|
378
|
+
});
|
|
379
|
+
syncCursorFromCamera();
|
|
380
|
+
if (axisX.done && axisY.done) {
|
|
381
|
+
writeCamera(camera());
|
|
382
|
+
motionEnded.emit({ camera: camera() });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
cancelFrame = clock.nextFrame(tick);
|
|
386
|
+
};
|
|
387
|
+
cancelFrame = clock.nextFrame(tick);
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
cancelAnimation,
|
|
391
|
+
animateTo,
|
|
392
|
+
animateZoomAnchored,
|
|
393
|
+
startCameraPhysics,
|
|
394
|
+
api: {
|
|
395
|
+
fling: (velocityX, velocityY) => startCameraPhysics(velocityX, velocityY),
|
|
396
|
+
isMoving: () => cancelFrame !== null,
|
|
397
|
+
stopMotion: () => {
|
|
398
|
+
if (!cancelFrame) return;
|
|
399
|
+
cancelAnimation();
|
|
400
|
+
motionEnded.emit({ camera: camera() });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
118
405
|
//#endregion
|
|
119
406
|
//#region src/responsive.ts
|
|
120
407
|
/**
|
|
121
|
-
* Container queries for the settings bag
|
|
122
|
-
* the driver (when to re-resolve, how to react); the
|
|
123
|
-
* box is, when a query matches, and what the effective settings are. No
|
|
124
|
-
*
|
|
408
|
+
* Container queries for the settings bag, the pure half. The capability owns
|
|
409
|
+
* the driver (when to re-resolve, how to react); the laws live here: what a
|
|
410
|
+
* box is, when a query matches, and what the effective settings are. No
|
|
411
|
+
* state, so they are property-testable in isolation.
|
|
125
412
|
*
|
|
126
|
-
* The query vocabulary is
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* (`PointerSample.pointerType`) or the app's own business via
|
|
413
|
+
* The query vocabulary is only the box: a headless, DOM-free stage knows
|
|
414
|
+
* exactly one environmental fact, the viewport it was told. Anything else
|
|
415
|
+
* (pointer coarseness, platform, app state) is either per event
|
|
416
|
+
* (`PointerSample.pointerType`) or the app's own business via
|
|
417
|
+
* `updateSettings()`.
|
|
130
418
|
*/
|
|
131
419
|
/** The reported viewport as a queryable box. A square box is 'portrait' (CSS). */
|
|
132
|
-
const boxOf = (
|
|
133
|
-
width:
|
|
134
|
-
height:
|
|
135
|
-
orientation:
|
|
420
|
+
const boxOf = (size) => ({
|
|
421
|
+
width: size.width,
|
|
422
|
+
height: size.height,
|
|
423
|
+
orientation: size.height >= size.width ? "portrait" : "landscape"
|
|
136
424
|
});
|
|
137
|
-
/** All bounds inclusive, all fields optional,
|
|
138
|
-
const matchesQuery = (
|
|
425
|
+
/** All bounds inclusive, all fields optional, every condition must hold (CSS ranges). */
|
|
426
|
+
const matchesQuery = (query, box) => (query.minWidth === void 0 || box.width >= query.minWidth) && (query.maxWidth === void 0 || box.width <= query.maxWidth) && (query.minHeight === void 0 || box.height >= query.minHeight) && (query.maxHeight === void 0 || box.height <= query.maxHeight) && (query.orientation === void 0 || box.orientation === query.orientation);
|
|
139
427
|
const ruleMatches = (rule, box) => typeof rule.when === "function" ? !!rule.when(box) : matchesQuery(rule.when, box);
|
|
140
|
-
/**
|
|
428
|
+
/**
|
|
429
|
+
* Merge a patch over settings, skipping undefined values (Partial semantics).
|
|
430
|
+
* Each value replaces the setting whole; a `pageFrame` that names only some
|
|
431
|
+
* sides reserves 0 on the others.
|
|
432
|
+
*/
|
|
141
433
|
const mergeSettings = (into, patch) => {
|
|
142
|
-
const
|
|
434
|
+
const merged = { ...into };
|
|
143
435
|
let key;
|
|
144
436
|
for (key in patch) {
|
|
145
|
-
const value = patch[key];
|
|
146
|
-
if (value !== void 0) Object.assign(
|
|
437
|
+
const value = key === "pageFrame" ? frameOf(patch.pageFrame) : patch[key];
|
|
438
|
+
if (value !== void 0) Object.assign(merged, { [key]: value });
|
|
147
439
|
}
|
|
148
|
-
return
|
|
440
|
+
return merged;
|
|
441
|
+
};
|
|
442
|
+
const frameOf = (frame) => frame === void 0 ? void 0 : {
|
|
443
|
+
..._embedpdf_core_geometry.NO_FRAME,
|
|
444
|
+
...frame
|
|
149
445
|
};
|
|
150
446
|
/**
|
|
151
447
|
* Setting-value equality: primitives by identity, the flat objects the
|
|
152
448
|
* settings vocabulary uses (`{ px }`, `{ x, y }`, page frames, zoom specs) by
|
|
153
449
|
* one level of own-key comparison. Settings values are never nested deeper.
|
|
154
450
|
*/
|
|
155
|
-
const eqSetting = (
|
|
156
|
-
if (
|
|
157
|
-
if (typeof
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
|
|
451
|
+
const eqSetting = (left, right) => {
|
|
452
|
+
if (left === right) return true;
|
|
453
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
|
|
454
|
+
const leftKeys = Object.keys(left);
|
|
455
|
+
return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => left[key] === right[key]);
|
|
161
456
|
};
|
|
162
457
|
const resolveResponsive = (base, rules, box) => {
|
|
163
458
|
let effective = base;
|
|
@@ -175,11 +470,11 @@ const resolveResponsive = (base, rules, box) => {
|
|
|
175
470
|
//#endregion
|
|
176
471
|
//#region src/settings.ts
|
|
177
472
|
/**
|
|
178
|
-
* Out-of-the-box defaults
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
473
|
+
* Out-of-the-box defaults: a document-reading feel. Every field is overridable
|
|
474
|
+
* in `stagePlugin(config)` and at runtime through `updateSettings()`. The
|
|
475
|
+
* plugin ships no named presets ("document", "canvas", …): a preset is an
|
|
476
|
+
* object the app keeps and passes to `updateSettings()`, so that taxonomy
|
|
477
|
+
* stays the app's concern.
|
|
183
478
|
*/
|
|
184
479
|
const DEFAULT_SETTINGS = {
|
|
185
480
|
flow: "continuous",
|
|
@@ -216,14 +511,17 @@ const DEFAULT_SETTINGS = {
|
|
|
216
511
|
viewRotation: 0,
|
|
217
512
|
zoom: { mode: _embedpdf_core_stage.ZoomMode.Automatic },
|
|
218
513
|
scrollBehavior: "smooth",
|
|
514
|
+
interaction: true,
|
|
515
|
+
panFallback: true,
|
|
516
|
+
zoomGestures: true,
|
|
219
517
|
viewUnitsPerPoint: 96 / 72
|
|
220
518
|
};
|
|
221
519
|
/**
|
|
222
|
-
* Out-of-the-box responsive rules
|
|
223
|
-
* a compact container gets the thin gutter phones use (space, not device
|
|
520
|
+
* Out-of-the-box responsive rules, the platform feel with zero configuration:
|
|
521
|
+
* a compact container gets the thin gutter phones use (space, not device: an
|
|
224
522
|
* embedded 500px pane on a desktop is compact too). Override the whole list
|
|
225
523
|
* with `stagePlugin({ responsive: [...] })`; `responsive: []` opts out. The
|
|
226
|
-
* 'compact' name is queryable app
|
|
524
|
+
* 'compact' name is queryable by the app through `stage.matchesRule('compact')`.
|
|
227
525
|
*/
|
|
228
526
|
const DEFAULT_RESPONSIVE = [{
|
|
229
527
|
name: "compact",
|
|
@@ -248,690 +546,702 @@ const SETTINGS_EFFECT = {
|
|
|
248
546
|
viewRotation: "scene",
|
|
249
547
|
zoom: "refit",
|
|
250
548
|
scrollBehavior: "none",
|
|
549
|
+
interaction: "none",
|
|
550
|
+
panFallback: "none",
|
|
551
|
+
zoomGestures: "none",
|
|
251
552
|
viewUnitsPerPoint: "scene"
|
|
252
553
|
};
|
|
253
554
|
const SETTING_KEYS = Object.keys(SETTINGS_EFFECT);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
555
|
+
/** Field-by-field settings equality, derived from the registry, so a new
|
|
556
|
+
* setting is covered automatically: what an adapter compares settings with. */
|
|
557
|
+
const settingsEqual = (left, right) => SETTING_KEYS.every((key) => eqSetting(left[key], right[key]));
|
|
558
|
+
/** The settings slice of a larger object (state, or a saved view), derived
|
|
559
|
+
* from the registry so the shape is never spelled out by hand. */
|
|
560
|
+
const pickSettings = (source) => {
|
|
561
|
+
const settings = {};
|
|
562
|
+
for (const key of SETTING_KEYS) settings[key] = source[key];
|
|
563
|
+
return settings;
|
|
564
|
+
};
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/model.ts
|
|
567
|
+
const initialStageState = (config) => {
|
|
568
|
+
const { responsive: _responsive, ...overrides } = config;
|
|
569
|
+
return {
|
|
570
|
+
camera: {
|
|
571
|
+
x: 0,
|
|
572
|
+
y: 0,
|
|
573
|
+
zoom: 1
|
|
574
|
+
},
|
|
575
|
+
placed: false,
|
|
576
|
+
cameraResting: true,
|
|
577
|
+
viewport: {
|
|
578
|
+
width: 0,
|
|
579
|
+
height: 0
|
|
580
|
+
},
|
|
581
|
+
dpr: 1,
|
|
582
|
+
cursor: 0,
|
|
583
|
+
motionCause: "user",
|
|
584
|
+
activeRules: [],
|
|
585
|
+
...mergeSettings(DEFAULT_SETTINGS, overrides)
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
const setCamera = (state, camera) => state.camera === camera ? state : {
|
|
589
|
+
...state,
|
|
590
|
+
camera
|
|
591
|
+
};
|
|
592
|
+
const setCameraResting = (state, resting) => state.cameraResting === resting ? state : {
|
|
593
|
+
...state,
|
|
594
|
+
cameraResting: resting
|
|
595
|
+
};
|
|
596
|
+
const markPlaced = (state) => state.placed ? state : {
|
|
597
|
+
...state,
|
|
598
|
+
placed: true
|
|
599
|
+
};
|
|
600
|
+
const setViewport = (state, viewport) => state.viewport === viewport ? state : {
|
|
601
|
+
...state,
|
|
602
|
+
viewport
|
|
603
|
+
};
|
|
604
|
+
const setDpr = (state, dpr) => state.dpr === dpr ? state : {
|
|
605
|
+
...state,
|
|
606
|
+
dpr
|
|
607
|
+
};
|
|
608
|
+
const setCursor = (state, cursor) => state.cursor === cursor ? state : {
|
|
609
|
+
...state,
|
|
610
|
+
cursor
|
|
611
|
+
};
|
|
612
|
+
const setMotionCause = (state, motionCause) => state.motionCause === motionCause ? state : {
|
|
613
|
+
...state,
|
|
614
|
+
motionCause
|
|
615
|
+
};
|
|
616
|
+
/**
|
|
617
|
+
* Merge a settings patch. Undefined values are skipped, so a partial restore
|
|
618
|
+
* never clears a setting; the area that patches decides what camera
|
|
619
|
+
* follow-up the change needs.
|
|
620
|
+
*/
|
|
621
|
+
function patchSettings(state, patch) {
|
|
622
|
+
let next = state;
|
|
623
|
+
let key;
|
|
624
|
+
for (key in patch) {
|
|
625
|
+
const value = patch[key];
|
|
626
|
+
if (value === void 0 || Object.is(next[key], value)) continue;
|
|
627
|
+
if (next === state) next = { ...state };
|
|
628
|
+
Object.assign(next, { [key]: value });
|
|
629
|
+
}
|
|
630
|
+
return next;
|
|
631
|
+
}
|
|
632
|
+
const setActiveRules = (state, activeRules) => state.activeRules === activeRules ? state : {
|
|
633
|
+
...state,
|
|
634
|
+
activeRules
|
|
259
635
|
};
|
|
260
|
-
/** Field-by-field settings equality, derived from the registry — a new setting is
|
|
261
|
-
* covered automatically. (The React `useStageSettings` selector equality.) */
|
|
262
|
-
const settingsEqual = (a, b) => SETTING_KEYS.every((k) => valueEq(a[k], b[k]));
|
|
263
636
|
//#endregion
|
|
264
|
-
//#region src/
|
|
637
|
+
//#region src/camera/gestures.ts
|
|
265
638
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* • stage-core — PURE spatial math (no DOM, no time). Ports to Rust later.
|
|
270
|
-
* • this capability — the IMPURE platform shell. It dispatches, caches, and owns
|
|
271
|
-
* the camera tween. It is NOT pure; its one host dependency (frame timing)
|
|
272
|
-
* enters through the Scheduler seam — injectable, defaulting to the host's
|
|
273
|
-
* own frame clock when one exists (see the seam below).
|
|
274
|
-
*
|
|
275
|
-
* The model: EVERY camera move is defined by what it holds fixed.
|
|
276
|
-
* • gestures (pan/pinch/wheel) — the content under the pointer. Physics, no setting.
|
|
277
|
-
* • focal zoom (zoomIn/zoomTo/fit-mode switches) — the zoomAlign viewport point.
|
|
278
|
-
* • reframes (resize/rotate/re-layout) — the page-point at the anchorAlign
|
|
279
|
-
* viewport point: captured there before the change, restored there after.
|
|
280
|
-
* • arrivals (goToPage/next/prev/reset) — nothing prior: a fresh landing at
|
|
281
|
-
* arrivalAlign, THE SAME at every zoom (landing is policy, never a zoom side effect).
|
|
282
|
-
* • explicit (reveal/destination/viewpoint) — whatever the call specifies.
|
|
283
|
-
* fitAlign stands apart: the clamp's standing rest constraint wherever an axis has no
|
|
284
|
-
* freedom against the TRUE bounds — it shapes every move above, and is why a fitting
|
|
285
|
-
* axis settles identically whatever arrivalAlign says.
|
|
286
|
-
*
|
|
287
|
-
* "Does it fit the viewport?" still decides SIZE and SUBJECT — never alignment:
|
|
288
|
-
* • step size: next/prev step by ITEM (spread) when the item fits, by PAGE when
|
|
289
|
-
* zoomed in past it.
|
|
290
|
-
* • subject: you arrive AT the unit that fits (item, page — or the whole scene
|
|
291
|
-
* under fit-all).
|
|
292
|
-
*
|
|
293
|
-
* The `cursor` is THE current page in both flows: navigation sets it; continuous
|
|
294
|
-
* scrolling syncs it from the camera; paged panning never moves it.
|
|
639
|
+
* Direct manipulation: set, pan, scroll and pinch-zoom the camera, and the
|
|
640
|
+
* gesture bracket. The camera moves freely and the cursor is derived from it
|
|
641
|
+
* (the manipulation direction of cursor reconciliation).
|
|
295
642
|
*/
|
|
296
|
-
function
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const dpr = () => ctx.getState().dpr;
|
|
309
|
-
const pad = () => ctx.getState().padding;
|
|
310
|
-
const paged = () => ctx.getState().flow === "paged";
|
|
311
|
-
const isFitAll = () => {
|
|
312
|
-
const z = ctx.getState().zoom;
|
|
313
|
-
return "mode" in z && z.mode === _embedpdf_core_stage.ZoomMode.FitAll;
|
|
314
|
-
};
|
|
315
|
-
let groupingCache = null;
|
|
316
|
-
const grouping = () => {
|
|
317
|
-
const doc = ctx.document();
|
|
318
|
-
const st = ctx.getState();
|
|
319
|
-
const key = `${doc ? doc.pageCount : 0}|${st.spread}`;
|
|
320
|
-
if (groupingCache && groupingCache.key === key) return groupingCache;
|
|
321
|
-
const g = _embedpdf_core_stage.groupPages(doc ? doc.pageCount : 0, st.spread);
|
|
322
|
-
groupingCache = {
|
|
323
|
-
key,
|
|
324
|
-
grouping: g,
|
|
325
|
-
firstPages: g.map((item) => item[0])
|
|
326
|
-
};
|
|
327
|
-
return groupingCache;
|
|
328
|
-
};
|
|
329
|
-
const itemIndexOfPage = (pageIndex) => {
|
|
330
|
-
const fp = grouping().firstPages;
|
|
331
|
-
let lo = 0;
|
|
332
|
-
let hi = fp.length;
|
|
333
|
-
while (lo < hi) {
|
|
334
|
-
const mid = lo + hi >> 1;
|
|
335
|
-
if (fp[mid] <= pageIndex) lo = mid + 1;
|
|
336
|
-
else hi = mid;
|
|
643
|
+
function createGestures(ctx, { scene }, write, { cancelAnimation, animateTo, startCameraPhysics }, { scrollMetricsNow }) {
|
|
644
|
+
const { camera, viewport, padding, buildScene, stayBounds, constraint } = scene;
|
|
645
|
+
const { gesture, armRest, writeCamera, writeCameraUnclamped, rubberize, unrubberize, syncCursorFromCamera, markCause } = write;
|
|
646
|
+
const scrollTo = (options) => {
|
|
647
|
+
markCause("user");
|
|
648
|
+
cancelAnimation();
|
|
649
|
+
if (!buildScene().itemCount) return;
|
|
650
|
+
const next = (0, _embedpdf_core_stage.cameraFromScroll)(camera(), stayBounds(), viewport(), padding(), options);
|
|
651
|
+
if (options.behavior === "smooth") animateTo(next, stayBounds(), void 0, syncCursorFromCamera);
|
|
652
|
+
else {
|
|
653
|
+
writeCamera(next);
|
|
654
|
+
syncCursorFromCamera();
|
|
337
655
|
}
|
|
338
|
-
return Math.max(0, lo - 1);
|
|
339
|
-
};
|
|
340
|
-
const effectiveZoom = () => {
|
|
341
|
-
const z = ctx.getState().zoom;
|
|
342
|
-
return "level" in z ? z.level : Math.max(ctx.getState().camera.zoom, 1e-4);
|
|
343
656
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
657
|
+
return { api: {
|
|
658
|
+
setCamera: (next) => {
|
|
659
|
+
markCause("user");
|
|
660
|
+
cancelAnimation();
|
|
661
|
+
writeCamera(next);
|
|
662
|
+
syncCursorFromCamera();
|
|
663
|
+
},
|
|
664
|
+
panBy: (dx, dy) => {
|
|
665
|
+
markCause("user");
|
|
666
|
+
cancelAnimation();
|
|
667
|
+
if (gesture.depth > 0 && gesture.elastic) {
|
|
668
|
+
gesture.raw = (0, _embedpdf_core_stage.panByScreen)(gesture.raw ?? unrubberize(camera()), dx, dy);
|
|
669
|
+
writeCameraUnclamped(rubberize(gesture.raw));
|
|
670
|
+
} else writeCamera((0, _embedpdf_core_stage.panByScreen)(camera(), dx, dy));
|
|
671
|
+
syncCursorFromCamera();
|
|
672
|
+
},
|
|
673
|
+
scrollTo,
|
|
674
|
+
scrollBy: ({ left, top, behavior }) => {
|
|
675
|
+
const metrics = scrollMetricsNow();
|
|
676
|
+
scrollTo({
|
|
677
|
+
left: left === void 0 ? void 0 : metrics.scrollLeft + left,
|
|
678
|
+
top: top === void 0 ? void 0 : metrics.scrollTop + top,
|
|
679
|
+
behavior
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
zoomAround: (point, factor) => {
|
|
683
|
+
cancelAnimation();
|
|
684
|
+
const before = buildScene();
|
|
685
|
+
const focal = before.itemCount ? (0, _embedpdf_core_stage.anchorAtPoint)(before, (0, _embedpdf_core_stage.toWorld)(camera(), point)) : null;
|
|
686
|
+
writeCamera((0, _embedpdf_core_stage.zoomAround)(camera(), point, factor));
|
|
687
|
+
if (gesture.depth === 0) ctx.state.update(patchSettings, { zoom: { level: camera().zoom } });
|
|
688
|
+
const after = buildScene();
|
|
689
|
+
if (after !== before && focal && after.itemCount) writeCamera((0, _embedpdf_core_stage.cameraForAnchorAtScreen)(focal, after, point, camera().zoom));
|
|
690
|
+
if (gesture.depth > 0 && gesture.elastic) gesture.raw = unrubberize(camera());
|
|
691
|
+
syncCursorFromCamera();
|
|
692
|
+
},
|
|
693
|
+
beginGesture: (options) => {
|
|
694
|
+
gesture.depth++;
|
|
695
|
+
if (gesture.depth === 1) {
|
|
696
|
+
cancelAnimation();
|
|
697
|
+
gesture.zoomed = false;
|
|
698
|
+
gesture.elastic = options?.elastic === true;
|
|
699
|
+
gesture.raw = null;
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
endGesture: () => {
|
|
703
|
+
if (gesture.depth === 0) return;
|
|
704
|
+
gesture.depth--;
|
|
705
|
+
if (gesture.depth > 0) return;
|
|
706
|
+
gesture.raw = null;
|
|
707
|
+
const wasElastic = gesture.elastic;
|
|
708
|
+
gesture.elastic = false;
|
|
709
|
+
if (gesture.zoomed) {
|
|
710
|
+
gesture.zoomed = false;
|
|
711
|
+
ctx.state.update(patchSettings, { zoom: { level: camera().zoom } });
|
|
712
|
+
armRest();
|
|
713
|
+
}
|
|
714
|
+
syncCursorFromCamera();
|
|
715
|
+
if (wasElastic) {
|
|
716
|
+
const current = camera();
|
|
717
|
+
const clamped = (0, _embedpdf_core_stage.clampCamera)(current, stayBounds(), viewport(), constraint());
|
|
718
|
+
if (clamped.x !== current.x || clamped.y !== current.y) startCameraPhysics(0, 0);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} };
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/camera/write.ts
|
|
725
|
+
/**
|
|
726
|
+
* The camera write path: the one low-level clamped write (`writeCamera`), the
|
|
727
|
+
* elastic unclamped write and its rubber-band curves, the rest detector that
|
|
728
|
+
* gates device snapping, the gesture bracket state, and cursor reconciliation
|
|
729
|
+
* from the camera (manipulation only; navigation sets the cursor as intent).
|
|
730
|
+
*/
|
|
731
|
+
/** How long the zoom must hold still before the camera counts as resting. */
|
|
732
|
+
const REST_MS = 150;
|
|
733
|
+
function createCameraWrite(ctx, { clock, placement, scene }) {
|
|
734
|
+
const { camera, viewport, paged, buildScene, stayBounds, constraint } = scene;
|
|
735
|
+
const gesture = {
|
|
736
|
+
depth: 0,
|
|
737
|
+
zoomed: false,
|
|
738
|
+
elastic: false,
|
|
739
|
+
raw: null
|
|
365
740
|
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
741
|
+
let cancelRest = null;
|
|
742
|
+
const stopRestCountdown = () => {
|
|
743
|
+
cancelRest?.();
|
|
744
|
+
cancelRest = null;
|
|
369
745
|
};
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}));
|
|
378
|
-
const pageFrame = worldPageFrame();
|
|
379
|
-
const gap = worldGap();
|
|
380
|
-
const vupp = st.viewUnitsPerPoint;
|
|
381
|
-
if (st.layout === "grid") return _embedpdf_core_stage.gridLayout(pages, groups, {
|
|
382
|
-
gap,
|
|
383
|
-
sizing: st.sizing,
|
|
384
|
-
direction: st.direction,
|
|
385
|
-
pageFrame,
|
|
386
|
-
viewUnitsPerPoint: vupp,
|
|
387
|
-
columns: typeof st.columns === "number" ? st.columns : void 0,
|
|
388
|
-
lineWidth: st.columns === "auto" ? wrapLineWidth() : void 0
|
|
389
|
-
});
|
|
390
|
-
return st.layout === "horizontal" ? _embedpdf_core_stage.linearLayout(pages, groups, {
|
|
391
|
-
axis: "x",
|
|
392
|
-
gap,
|
|
393
|
-
sizing: st.sizing,
|
|
394
|
-
direction: st.direction,
|
|
395
|
-
pageFrame,
|
|
396
|
-
viewUnitsPerPoint: vupp
|
|
397
|
-
}) : _embedpdf_core_stage.linearLayout(pages, groups, {
|
|
398
|
-
axis: "y",
|
|
399
|
-
gap,
|
|
400
|
-
sizing: st.sizing,
|
|
401
|
-
direction: st.direction,
|
|
402
|
-
pageFrame,
|
|
403
|
-
viewUnitsPerPoint: vupp
|
|
746
|
+
const armRest = () => {
|
|
747
|
+
if (!clock.hasFrames || !placement.started) return;
|
|
748
|
+
ctx.state.update(setCameraResting, false);
|
|
749
|
+
stopRestCountdown();
|
|
750
|
+
cancelRest = clock.after(REST_MS, () => {
|
|
751
|
+
cancelRest = null;
|
|
752
|
+
ctx.state.update(setCameraResting, true);
|
|
404
753
|
});
|
|
405
754
|
};
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
pageFrame: frameKey
|
|
415
|
-
};
|
|
416
|
-
const SCENE_KEYS = SETTING_KEYS.filter((k) => SETTINGS_EFFECT[k] === "scene");
|
|
417
|
-
const settingsKey = () => SCENE_KEYS.map((k) => {
|
|
418
|
-
const fn = SCENE_KEY_FNS[k];
|
|
419
|
-
if (fn) return fn();
|
|
420
|
-
const v = ctx.getState()[k];
|
|
421
|
-
return typeof v === "object" && v !== null ? JSON.stringify(v) : String(v);
|
|
422
|
-
}).join("|");
|
|
423
|
-
let sceneCache = null;
|
|
424
|
-
const docKey = () => {
|
|
425
|
-
const doc = ctx.document();
|
|
426
|
-
return doc ? `${doc.pageCount}.${doc.revision}` : "0.0";
|
|
755
|
+
const writeCamera = (next, bounds = stayBounds()) => {
|
|
756
|
+
const clamped = (0, _embedpdf_core_stage.clampCamera)(next, bounds, viewport(), constraint());
|
|
757
|
+
if (clamped.zoom !== camera().zoom) if (gesture.depth > 0) {
|
|
758
|
+
gesture.zoomed = true;
|
|
759
|
+
ctx.state.update(setCameraResting, false);
|
|
760
|
+
stopRestCountdown();
|
|
761
|
+
} else armRest();
|
|
762
|
+
ctx.state.update(setCamera, clamped);
|
|
427
763
|
};
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
return
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (sceneCache && sceneCache.key === key) return sceneCache.scene;
|
|
444
|
-
const scene = layoutFor(g);
|
|
445
|
-
sceneCache = {
|
|
446
|
-
key,
|
|
447
|
-
scene
|
|
764
|
+
const axisTravels = (origin, content, view, zoom) => !(0, _embedpdf_core_stage.travelRange)(origin, content, view, zoom, constraint().padding).fits;
|
|
765
|
+
/** The unclamped finger-integrated camera → the displayed camera: clamp,
|
|
766
|
+
* then re-apply the overshoot through the resistance curve, per travelling
|
|
767
|
+
* axis. Inside the bounds this is exactly the clamp, and on a fitting axis
|
|
768
|
+
* it is always the clamp. */
|
|
769
|
+
const rubberize = (raw) => {
|
|
770
|
+
const clamped = (0, _embedpdf_core_stage.clampCamera)(raw, stayBounds(), viewport(), constraint());
|
|
771
|
+
const size = viewport();
|
|
772
|
+
const bounds = stayBounds();
|
|
773
|
+
const axis = (rawPosition, clampedPosition, dimension, travels) => {
|
|
774
|
+
if (!travels) return clampedPosition;
|
|
775
|
+
const overshootWorld = rawPosition - clampedPosition;
|
|
776
|
+
if (overshootWorld === 0) return clampedPosition;
|
|
777
|
+
const stretch = rubberOut(Math.abs(overshootWorld) * raw.zoom, Math.max(1, dimension));
|
|
778
|
+
return clampedPosition + Math.sign(overshootWorld) * stretch / raw.zoom;
|
|
448
779
|
};
|
|
449
|
-
return scene;
|
|
450
|
-
};
|
|
451
|
-
const sceneRect = () => {
|
|
452
|
-
const { width, height } = buildScene().size;
|
|
453
780
|
return {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
height
|
|
781
|
+
zoom: raw.zoom,
|
|
782
|
+
x: axis(raw.x, clamped.x, size.width, axisTravels(bounds.x, bounds.width, size.width, raw.zoom)),
|
|
783
|
+
y: axis(raw.y, clamped.y, size.height, axisTravels(bounds.y, bounds.height, size.height, raw.zoom))
|
|
458
784
|
};
|
|
459
785
|
};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
786
|
+
/** The displayed camera → the raw position `rubberize` would have produced
|
|
787
|
+
* it from. The overshoot is capped just under the asymptote so the inverse
|
|
788
|
+
* stays finite whatever state a catch finds the camera in. */
|
|
789
|
+
const unrubberize = (displayed) => {
|
|
790
|
+
const clamped = (0, _embedpdf_core_stage.clampCamera)(displayed, stayBounds(), viewport(), constraint());
|
|
791
|
+
const size = viewport();
|
|
792
|
+
const bounds = stayBounds();
|
|
793
|
+
const axis = (displayedPosition, clampedPosition, dimension, travels) => {
|
|
794
|
+
if (!travels) return clampedPosition;
|
|
795
|
+
const overshootWorld = displayedPosition - clampedPosition;
|
|
796
|
+
if (overshootWorld === 0) return clampedPosition;
|
|
797
|
+
const span = Math.max(1, dimension);
|
|
798
|
+
const stretch = Math.min(Math.abs(overshootWorld) * displayed.zoom, span - 1);
|
|
799
|
+
return clampedPosition + Math.sign(overshootWorld) * rubberIn(stretch, span) / displayed.zoom;
|
|
800
|
+
};
|
|
468
801
|
return {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
height: box.height
|
|
802
|
+
zoom: displayed.zoom,
|
|
803
|
+
x: axis(displayed.x, clamped.x, size.width, axisTravels(bounds.x, bounds.width, size.width, displayed.zoom)),
|
|
804
|
+
y: axis(displayed.y, clamped.y, size.height, axisTravels(bounds.y, bounds.height, size.height, displayed.zoom))
|
|
473
805
|
};
|
|
474
806
|
};
|
|
475
|
-
|
|
476
|
-
const cursorItem = () => {
|
|
477
|
-
const sc = buildScene();
|
|
478
|
-
return paged() ? sc.items[0] : sc.items[itemIndexOfPage(ctx.getState().cursor)];
|
|
479
|
-
};
|
|
807
|
+
const writeCameraUnclamped = (next) => ctx.state.update(setCamera, next);
|
|
480
808
|
/**
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
width: box.width,
|
|
489
|
-
height: box.height
|
|
490
|
-
}, box.rotation);
|
|
491
|
-
const wr = (0, _embedpdf_core_geometry.applyRect)((0, _embedpdf_core_geometry.rotateScaleMatrix)(box.contentScale, content.width, content.height, box.rotation), rect);
|
|
492
|
-
return {
|
|
493
|
-
x: box.x + wr.x,
|
|
494
|
-
y: box.y + wr.y,
|
|
495
|
-
width: wr.width,
|
|
496
|
-
height: wr.height
|
|
497
|
-
};
|
|
498
|
-
};
|
|
499
|
-
/**
|
|
500
|
-
* One axis of a positioned-reveal camera. `undefined` = 'nearest' (only
|
|
501
|
-
* move if the target is outside the padded view) — unless the zoom just
|
|
502
|
-
* changed, where "don't move" is meaningless and the spec's slack-axis
|
|
503
|
-
* rule (center) applies. 'keep' never moves the axis (PDF /XYZ null).
|
|
504
|
-
*/
|
|
505
|
-
const revealAxis = (a, camPos, rectPos, rectExtent, vpExtent, zoom, zoomChanged) => {
|
|
506
|
-
if (a === "keep") return camPos;
|
|
507
|
-
const p = pad();
|
|
508
|
-
if (a === void 0) {
|
|
509
|
-
if (!zoomChanged) {
|
|
510
|
-
const lo = camPos + p / zoom;
|
|
511
|
-
const hi = camPos + (vpExtent - p) / zoom;
|
|
512
|
-
if (rectPos >= lo && rectPos + rectExtent <= hi) return camPos;
|
|
513
|
-
if (rectExtent > hi - lo || rectPos < lo) return rectPos - p / zoom;
|
|
514
|
-
return rectPos + rectExtent - (vpExtent - p) / zoom;
|
|
515
|
-
}
|
|
516
|
-
a = "center";
|
|
517
|
-
}
|
|
518
|
-
if (a === "start") return rectPos - p / zoom;
|
|
519
|
-
if (a === "end") return rectPos + rectExtent - (vpExtent - p) / zoom;
|
|
520
|
-
const f = a === "center" ? .5 : Math.min(1, Math.max(0, a));
|
|
521
|
-
return rectPos + rectExtent / 2 - vpExtent * f / zoom;
|
|
522
|
-
};
|
|
523
|
-
const fits = (rect, zoom) => {
|
|
524
|
-
const v = vp();
|
|
525
|
-
const p = pad();
|
|
526
|
-
const eps = .5;
|
|
527
|
-
return rect.width * zoom <= v.width - 2 * p + eps && rect.height * zoom <= v.height - 2 * p + eps;
|
|
528
|
-
};
|
|
529
|
-
const alignFraction = (a) => a === "start" ? 0 : a === "center" ? .5 : a === "end" ? 1 : Math.min(1, Math.max(0, a));
|
|
530
|
-
const alignPoint = (al, v) => {
|
|
531
|
-
const rtl = ctx.getState().direction === "rtl";
|
|
532
|
-
const ax = rtl && al.x === "start" ? "end" : rtl && al.x === "end" ? "start" : al.x;
|
|
533
|
-
const p = pad();
|
|
534
|
-
return {
|
|
535
|
-
x: p + (v.width - 2 * p) * alignFraction(ax),
|
|
536
|
-
y: p + (v.height - 2 * p) * alignFraction(al.y)
|
|
537
|
-
};
|
|
538
|
-
};
|
|
539
|
-
const anchorPoint = () => alignPoint(ctx.getState().anchorAlign, vp());
|
|
540
|
-
/** Fit-box for resolving the zoom intent: whole scene (fit-all), the current item
|
|
541
|
-
* (paged — per-page fit), or the document max (continuous — doc-stable zoom). */
|
|
542
|
-
const fitBox = (item) => {
|
|
543
|
-
if (isFitAll()) return buildScene().size;
|
|
544
|
-
return paged() ? {
|
|
545
|
-
width: item.width,
|
|
546
|
-
height: item.height
|
|
547
|
-
} : buildScene().maxItemSize;
|
|
548
|
-
};
|
|
549
|
-
/** Clamp rect for a camera write targeting this item (scene-wide in continuous). */
|
|
550
|
-
const boundsFor = (it) => paged() ? itemRect(it) : sceneRect();
|
|
551
|
-
/** Clamp rect for a "stay" write (pan/zoom): the slice item (paged) / the scene. */
|
|
552
|
-
const stayBounds = () => paged() ? itemRect(buildScene().items[0]) : sceneRect();
|
|
553
|
-
const constraint = () => ({
|
|
554
|
-
bounded: ctx.getState().bounded,
|
|
555
|
-
padding: pad(),
|
|
556
|
-
fitAlign: ctx.getState().fitAlign,
|
|
557
|
-
direction: ctx.getState().direction
|
|
558
|
-
});
|
|
559
|
-
let placementStarted = false;
|
|
560
|
-
let gestureDepth = 0;
|
|
561
|
-
let gestureZoomed = false;
|
|
562
|
-
let gestureElastic = false;
|
|
563
|
-
let gestureRaw = null;
|
|
564
|
-
const REST_MS = 150;
|
|
565
|
-
let restRaf = 0;
|
|
566
|
-
const armRest = () => {
|
|
567
|
-
if (!canAnimate || !placementStarted) return;
|
|
568
|
-
if (ctx.getState().cameraResting) ctx.dispatch({
|
|
569
|
-
type: "CAMERA_REST",
|
|
570
|
-
resting: false
|
|
571
|
-
});
|
|
572
|
-
if (restRaf) scheduler.caf(restRaf);
|
|
573
|
-
let t0 = 0;
|
|
574
|
-
const tick = (ts) => {
|
|
575
|
-
restRaf = 0;
|
|
576
|
-
if (!t0) t0 = ts;
|
|
577
|
-
if (ts - t0 >= REST_MS) {
|
|
578
|
-
ctx.dispatch({
|
|
579
|
-
type: "CAMERA_REST",
|
|
580
|
-
resting: true
|
|
581
|
-
});
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
restRaf = scheduler.raf(tick);
|
|
585
|
-
};
|
|
586
|
-
restRaf = scheduler.raf(tick);
|
|
587
|
-
};
|
|
588
|
-
const setCam = (next, bounds = stayBounds()) => {
|
|
589
|
-
const clamped = _embedpdf_core_stage.clampCamera(next, bounds, vp(), constraint());
|
|
590
|
-
if (clamped.zoom !== cam().zoom) if (gestureDepth > 0) {
|
|
591
|
-
gestureZoomed = true;
|
|
592
|
-
if (ctx.getState().cameraResting) ctx.dispatch({
|
|
593
|
-
type: "CAMERA_REST",
|
|
594
|
-
resting: false
|
|
595
|
-
});
|
|
596
|
-
if (restRaf) {
|
|
597
|
-
scheduler.caf(restRaf);
|
|
598
|
-
restRaf = 0;
|
|
599
|
-
}
|
|
600
|
-
} else armRest();
|
|
601
|
-
ctx.dispatch({
|
|
602
|
-
type: "CAMERA",
|
|
603
|
-
camera: clamped
|
|
604
|
-
});
|
|
605
|
-
};
|
|
606
|
-
const axisTravels = (origin, content, view, zoom) => !_embedpdf_core_stage.travelRange(origin, content, view, zoom, constraint().padding).fits;
|
|
607
|
-
/** Unclamped finger-integrated camera → the DISPLAYED camera: clamp, then
|
|
608
|
-
* re-apply the overshoot through the resistance curve, per travelling axis.
|
|
609
|
-
* Inside the bounds this is exactly the clamp (rubber of zero is zero);
|
|
610
|
-
* on a fitting axis it is exactly the clamp ALWAYS. */
|
|
611
|
-
const rubberize = (raw) => {
|
|
612
|
-
const clamped = _embedpdf_core_stage.clampCamera(raw, stayBounds(), vp(), constraint());
|
|
613
|
-
const v = vp();
|
|
614
|
-
const b = stayBounds();
|
|
615
|
-
const axis = (rawA, clampedA, dim, travels) => {
|
|
616
|
-
if (!travels) return clampedA;
|
|
617
|
-
const dWorld = rawA - clampedA;
|
|
618
|
-
if (dWorld === 0) return clampedA;
|
|
619
|
-
const out = rubberOut(Math.abs(dWorld) * raw.zoom, Math.max(1, dim));
|
|
620
|
-
return clampedA + Math.sign(dWorld) * out / raw.zoom;
|
|
621
|
-
};
|
|
622
|
-
return {
|
|
623
|
-
zoom: raw.zoom,
|
|
624
|
-
x: axis(raw.x, clamped.x, v.width, axisTravels(b.x, b.width, v.width, raw.zoom)),
|
|
625
|
-
y: axis(raw.y, clamped.y, v.height, axisTravels(b.y, b.height, v.height, raw.zoom))
|
|
626
|
-
};
|
|
627
|
-
};
|
|
628
|
-
/** Displayed camera → the raw position `rubberize` would have produced it
|
|
629
|
-
* from. Overshoot is capped just under the asymptote so the inverse stays
|
|
630
|
-
* finite whatever state a catch finds the camera in. */
|
|
631
|
-
const unrubberize = (displayed) => {
|
|
632
|
-
const clamped = _embedpdf_core_stage.clampCamera(displayed, stayBounds(), vp(), constraint());
|
|
633
|
-
const v = vp();
|
|
634
|
-
const b = stayBounds();
|
|
635
|
-
const axis = (dispA, clampedA, dim, travels) => {
|
|
636
|
-
if (!travels) return clampedA;
|
|
637
|
-
const dWorld = dispA - clampedA;
|
|
638
|
-
if (dWorld === 0) return clampedA;
|
|
639
|
-
const d = Math.max(1, dim);
|
|
640
|
-
const out = Math.min(Math.abs(dWorld) * displayed.zoom, d - 1);
|
|
641
|
-
return clampedA + Math.sign(dWorld) * rubberIn(out, d) / displayed.zoom;
|
|
642
|
-
};
|
|
643
|
-
return {
|
|
644
|
-
zoom: displayed.zoom,
|
|
645
|
-
x: axis(displayed.x, clamped.x, v.width, axisTravels(b.x, b.width, v.width, displayed.zoom)),
|
|
646
|
-
y: axis(displayed.y, clamped.y, v.height, axisTravels(b.y, b.height, v.height, displayed.zoom))
|
|
647
|
-
};
|
|
648
|
-
};
|
|
649
|
-
const setCamRaw = (c) => ctx.dispatch({
|
|
650
|
-
type: "CAMERA",
|
|
651
|
-
camera: c
|
|
652
|
-
});
|
|
653
|
-
/**
|
|
654
|
-
* Cursor reconciliation, one direction per interaction:
|
|
655
|
-
* navigation → cursor is INTENT, set explicitly; the camera honors it as far
|
|
656
|
-
* as the clamp allows — and a clamped camera never revokes it.
|
|
657
|
-
* manipulation → (pan / drag / pinch) the camera moves freely; the cursor is
|
|
658
|
-
* DERIVED from it. Only those verbs call this. Paged never syncs.
|
|
809
|
+
* Cursor reconciliation, one direction per interaction:
|
|
810
|
+
* navigation → the cursor is intent, set explicitly; the camera honors
|
|
811
|
+
* it as far as the clamp allows, and a clamped camera
|
|
812
|
+
* never revokes it.
|
|
813
|
+
* manipulation → (pan, drag, pinch) the camera moves freely and the
|
|
814
|
+
* cursor is derived from it. Only those verbs call this.
|
|
815
|
+
* Paged flow never syncs.
|
|
659
816
|
*/
|
|
660
817
|
const syncCursorFromCamera = () => {
|
|
661
818
|
if (paged()) return;
|
|
662
|
-
const
|
|
663
|
-
if (!
|
|
664
|
-
|
|
665
|
-
if (page !== ctx.getState().cursor) ctx.dispatch({
|
|
666
|
-
type: "CURSOR",
|
|
667
|
-
cursor: page
|
|
668
|
-
});
|
|
669
|
-
};
|
|
670
|
-
let raf = 0;
|
|
671
|
-
const cancelAnim = () => {
|
|
672
|
-
if (raf) {
|
|
673
|
-
scheduler.caf(raf);
|
|
674
|
-
raf = 0;
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
const lerp = (a, b, k) => a + (b - a) * k;
|
|
678
|
-
const animateTo = (target, bounds, ms = 240, then) => {
|
|
679
|
-
if (!canAnimate) {
|
|
680
|
-
setCam(target, bounds);
|
|
681
|
-
then?.();
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
cancelAnim();
|
|
685
|
-
const from = cam();
|
|
686
|
-
let started = false;
|
|
687
|
-
let t0 = 0;
|
|
688
|
-
const tick = (now) => {
|
|
689
|
-
if (!started) {
|
|
690
|
-
started = true;
|
|
691
|
-
t0 = now;
|
|
692
|
-
}
|
|
693
|
-
const k = easeOutCubic(Math.min(1, (now - t0) / ms));
|
|
694
|
-
setCam({
|
|
695
|
-
x: lerp(from.x, target.x, k),
|
|
696
|
-
y: lerp(from.y, target.y, k),
|
|
697
|
-
zoom: lerp(from.zoom, target.zoom, k)
|
|
698
|
-
}, bounds);
|
|
699
|
-
if (k < 1) raf = scheduler.raf(tick);
|
|
700
|
-
else {
|
|
701
|
-
raf = 0;
|
|
702
|
-
then?.();
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
raf = scheduler.raf(tick);
|
|
706
|
-
};
|
|
707
|
-
/**
|
|
708
|
-
* Zoom-anchored tween — animates THE INVARIANT, not the coordinates. A
|
|
709
|
-
* camera move is defined by what it holds fixed; for an anchored zoom that
|
|
710
|
-
* is "the focal page-point stays at `pt` while scale changes". Lerping
|
|
711
|
-
* x/y/zoom independently (the plain tween) breaks that mid-flight: holding
|
|
712
|
-
* a screen point requires `x(t) = focal.x − pt.x / zoom(t)` — hyperbolic in
|
|
713
|
-
* the zoom — so linear coordinates swing the tapped point along a curved
|
|
714
|
-
* path (the visible "dip"). Here the zoom interpolates GEOMETRICALLY
|
|
715
|
-
* (constant rate — zoom is multiplicative) and each frame's camera derives
|
|
716
|
-
* from the anchor at that zoom, then clamps: the focal point is stationary
|
|
717
|
-
* by construction, and a zoom-out rails into its fit progressively as the
|
|
718
|
-
* shrinking travel range lets the clamp take over.
|
|
719
|
-
*/
|
|
720
|
-
const animateZoomAnchored = (focal, pt, targetZoom, ms = 240, then) => {
|
|
721
|
-
if (!canAnimate) {
|
|
722
|
-
const sc = buildScene();
|
|
723
|
-
if (sc.itemCount) setCam(_embedpdf_core_stage.cameraForAnchorAtScreen(focal, sc, pt, targetZoom));
|
|
724
|
-
then?.();
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
cancelAnim();
|
|
728
|
-
const z0 = cam().zoom;
|
|
729
|
-
let started = false;
|
|
730
|
-
let t0 = 0;
|
|
731
|
-
const tick = (now) => {
|
|
732
|
-
if (!started) {
|
|
733
|
-
started = true;
|
|
734
|
-
t0 = now;
|
|
735
|
-
}
|
|
736
|
-
const k = easeOutCubic(Math.min(1, (now - t0) / ms));
|
|
737
|
-
const z = zoomLerp(z0, targetZoom, k);
|
|
738
|
-
const sc = buildScene();
|
|
739
|
-
if (sc.itemCount) setCam(_embedpdf_core_stage.cameraForAnchorAtScreen(focal, sc, pt, z));
|
|
740
|
-
if (k < 1) raf = scheduler.raf(tick);
|
|
741
|
-
else {
|
|
742
|
-
raf = 0;
|
|
743
|
-
then?.();
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
raf = scheduler.raf(tick);
|
|
747
|
-
};
|
|
748
|
-
const startCameraPhysics = (vxFinger, vyFinger) => {
|
|
749
|
-
if (!canAnimate) {
|
|
750
|
-
setCam(cam());
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
cancelAnim();
|
|
754
|
-
const zoom = cam().zoom;
|
|
755
|
-
const c0 = cam();
|
|
756
|
-
const cl0 = _embedpdf_core_stage.clampCamera(c0, stayBounds(), vp(), constraint());
|
|
757
|
-
const b0 = stayBounds();
|
|
758
|
-
const v0 = vp();
|
|
759
|
-
const mk = (camA, clampedA, fingerV) => ({
|
|
760
|
-
p: camA * zoom,
|
|
761
|
-
v: -fingerV / 1e3,
|
|
762
|
-
springing: camA !== clampedA,
|
|
763
|
-
done: false
|
|
764
|
-
});
|
|
765
|
-
const ax = mk(c0.x, cl0.x, axisTravels(b0.x, b0.width, v0.width, zoom) ? vxFinger : 0);
|
|
766
|
-
const ay = mk(c0.y, cl0.y, axisTravels(b0.y, b0.height, v0.height, zoom) ? vyFinger : 0);
|
|
767
|
-
if (!ax.springing && !ay.springing && Math.hypot(ax.v, ay.v) < .02) return;
|
|
768
|
-
let started = false;
|
|
769
|
-
let last = 0;
|
|
770
|
-
const tick = (now) => {
|
|
771
|
-
raf = 0;
|
|
772
|
-
const dt = started ? Math.min(64, Math.max(1, now - last)) : 16;
|
|
773
|
-
started = true;
|
|
774
|
-
last = now;
|
|
775
|
-
const cl = _embedpdf_core_stage.clampCamera({
|
|
776
|
-
x: ax.p / zoom,
|
|
777
|
-
y: ay.p / zoom,
|
|
778
|
-
zoom
|
|
779
|
-
}, stayBounds(), vp(), constraint());
|
|
780
|
-
const step = (a, edgeWorld) => {
|
|
781
|
-
if (a.done) return;
|
|
782
|
-
const r = a.springing ? springStep(a.p, a.v, edgeWorld * zoom, dt) : glideStep(a.p, a.v, dt);
|
|
783
|
-
a.p = r.p;
|
|
784
|
-
a.v = r.v;
|
|
785
|
-
a.done = r.done;
|
|
786
|
-
};
|
|
787
|
-
step(ax, cl.x);
|
|
788
|
-
step(ay, cl.y);
|
|
789
|
-
const ncl = _embedpdf_core_stage.clampCamera({
|
|
790
|
-
x: ax.p / zoom,
|
|
791
|
-
y: ay.p / zoom,
|
|
792
|
-
zoom
|
|
793
|
-
}, stayBounds(), vp(), constraint());
|
|
794
|
-
const convert = (a, clampedA) => {
|
|
795
|
-
if (!a.springing && Math.abs(clampedA * zoom - a.p) > 1e-6) {
|
|
796
|
-
a.p = clampedA * zoom;
|
|
797
|
-
a.springing = true;
|
|
798
|
-
a.done = false;
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
convert(ax, ncl.x);
|
|
802
|
-
convert(ay, ncl.y);
|
|
803
|
-
setCamRaw({
|
|
804
|
-
x: ax.p / zoom,
|
|
805
|
-
y: ay.p / zoom,
|
|
806
|
-
zoom
|
|
807
|
-
});
|
|
808
|
-
syncCursorFromCamera();
|
|
809
|
-
if (ax.done && ay.done) {
|
|
810
|
-
setCam(cam());
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
raf = scheduler.raf(tick);
|
|
814
|
-
};
|
|
815
|
-
raf = scheduler.raf(tick);
|
|
819
|
+
const scene = buildScene();
|
|
820
|
+
if (!scene.itemCount) return;
|
|
821
|
+
ctx.state.update(setCursor, (0, _embedpdf_core_stage.anchorFromCamera)(camera(), scene, viewport()).pageIndex);
|
|
816
822
|
};
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
823
|
+
/** Tag what drives the next camera or cursor change (see `StageState.motionCause`). */
|
|
824
|
+
const markCause = (cause) => ctx.state.update(setMotionCause, cause);
|
|
825
|
+
return {
|
|
826
|
+
gesture,
|
|
827
|
+
armRest,
|
|
828
|
+
writeCamera,
|
|
829
|
+
writeCameraUnclamped,
|
|
830
|
+
axisTravels,
|
|
831
|
+
rubberize,
|
|
832
|
+
unrubberize,
|
|
833
|
+
syncCursorFromCamera,
|
|
834
|
+
markCause
|
|
824
835
|
};
|
|
825
|
-
|
|
836
|
+
}
|
|
837
|
+
//#endregion
|
|
838
|
+
//#region src/services/placement-latch.ts
|
|
839
|
+
const createPlacementLatch = () => ({
|
|
840
|
+
started: false,
|
|
841
|
+
pending: null
|
|
842
|
+
});
|
|
843
|
+
/**
|
|
844
|
+
* Run a navigation verb now, or, before the first placement, when it happens.
|
|
845
|
+
* Before placement there is no viewport to move a camera in, and placement
|
|
846
|
+
* would reset it anyway, so the last call waits and lands instantly after the
|
|
847
|
+
* initial view: an app that reveals something as the document opens sees it
|
|
848
|
+
* on the first frame.
|
|
849
|
+
*/
|
|
850
|
+
function whenPlaced(latch, options, run) {
|
|
851
|
+
if (latch.started) {
|
|
852
|
+
run(options);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
latch.pending = () => run({
|
|
856
|
+
...options,
|
|
857
|
+
behavior: "instant"
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/navigation/arrive.ts
|
|
862
|
+
/**
|
|
863
|
+
* Anchors and arrivals. The anchor is the durable "what am I looking at":
|
|
864
|
+
* captured before a structural change and re-applied after, one mechanism for
|
|
865
|
+
* layout, spread, zoom, resize and restore. Navigation is canonical:
|
|
866
|
+
* goToPage(N) always ends in the same camera state whatever the zoom or the
|
|
867
|
+
* starting point.
|
|
868
|
+
*/
|
|
869
|
+
function createArrival(ctx, { scene, placement }, { writeCamera, markCause }, { cancelAnimation, animateTo }) {
|
|
870
|
+
const { camera, viewport, padding, paged, isFitAll, grouping, itemIndexOfPage, buildScene, sceneRect, itemRect, pageRectOf, cursorItem, fits, alignPoint, fitBox, boundsFor, constraint, indexOfTarget } = scene;
|
|
871
|
+
const state = () => ctx.state.get();
|
|
872
|
+
const pageCount = () => ctx.document()?.pageCount ?? 0;
|
|
826
873
|
/**
|
|
827
|
-
* Run a placement whose
|
|
828
|
-
* a layout input)
|
|
829
|
-
* results
|
|
830
|
-
*
|
|
831
|
-
* Capped at 3 passes for the one
|
|
874
|
+
* Run a placement whose resolved zoom may change the scene (in wrapped mode
|
|
875
|
+
* zoom is a layout input), until the scene it placed against is the scene
|
|
876
|
+
* that results. Every non-wrapped mode is stable after the first pass, and
|
|
877
|
+
* wrapped fit modes converge on the second (the fit box does not depend on
|
|
878
|
+
* wrapping). Capped at 3 passes for the one circular case (fit-all +
|
|
879
|
+
* wrapped).
|
|
832
880
|
*/
|
|
833
881
|
const stabilized = (place) => {
|
|
834
882
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
835
|
-
const
|
|
883
|
+
const before = buildScene();
|
|
836
884
|
place();
|
|
837
|
-
if (buildScene() ===
|
|
885
|
+
if (buildScene() === before) return;
|
|
838
886
|
}
|
|
839
887
|
};
|
|
840
888
|
const applyAnchor = (anchor, align) => {
|
|
841
889
|
stabilized(() => {
|
|
842
|
-
const
|
|
843
|
-
if (!
|
|
844
|
-
const item =
|
|
845
|
-
const zoom = _embedpdf_core_stage.resolveZoom(
|
|
846
|
-
const at = alignPoint(align ??
|
|
847
|
-
|
|
890
|
+
const current = buildScene();
|
|
891
|
+
if (!current.itemCount) return;
|
|
892
|
+
const item = current.items[current.itemOfPage(anchor.pageIndex)];
|
|
893
|
+
const zoom = (0, _embedpdf_core_stage.resolveZoom)(state().zoom, fitBox(item), viewport(), padding());
|
|
894
|
+
const at = alignPoint(align ?? state().anchorAlign, viewport());
|
|
895
|
+
writeCamera((0, _embedpdf_core_stage.cameraFromAnchor)(anchor, current, viewport(), zoom, at), boundsFor(item));
|
|
848
896
|
});
|
|
849
897
|
};
|
|
850
898
|
/**
|
|
851
|
-
* Re-apply the view after a structural
|
|
852
|
-
* preserving (keep looking at the same spot, held at the invariant
|
|
853
|
-
* Under fit-all the subject is the
|
|
854
|
-
* meaningless
|
|
855
|
-
* unbounded).
|
|
899
|
+
* Re-apply the view after a structural, zoom or viewport change. Normally
|
|
900
|
+
* anchor-preserving (keep looking at the same spot, held at the invariant
|
|
901
|
+
* point). Under fit-all the subject is the whole scene, so keeping an
|
|
902
|
+
* anchor is meaningless: re-place instead (which also centers the scene
|
|
903
|
+
* when unbounded).
|
|
856
904
|
*/
|
|
857
905
|
const reapply = (anchor, align) => {
|
|
858
|
-
if (isFitAll()) goToTarget(
|
|
906
|
+
if (isFitAll()) goToTarget(state().cursor, { behavior: "instant" });
|
|
859
907
|
else applyAnchor(anchor, align);
|
|
860
908
|
};
|
|
861
|
-
|
|
862
|
-
* only, read by the page-state feed (see StageState.motionCause). */
|
|
863
|
-
const markCause = (cause) => {
|
|
864
|
-
if (ctx.getState().motionCause !== cause) ctx.dispatch({
|
|
865
|
-
type: "MOTION_CAUSE",
|
|
866
|
-
cause
|
|
867
|
-
});
|
|
868
|
-
};
|
|
869
|
-
const goToTarget = (pageIndex, opts) => {
|
|
909
|
+
const goToTarget = (pageIndex, options) => {
|
|
870
910
|
markCause("programmatic");
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
});
|
|
879
|
-
if (opts?.viewpoint) {
|
|
880
|
-
ctx.dispatch({
|
|
881
|
-
type: "PATCH",
|
|
882
|
-
patch: { zoom: opts.viewpoint.zoom }
|
|
883
|
-
});
|
|
884
|
-
applyAnchor(opts.viewpoint.anchor);
|
|
911
|
+
cancelAnimation();
|
|
912
|
+
if (pageCount() === 0) return;
|
|
913
|
+
const target = Math.max(0, Math.min(pageIndex, pageCount() - 1));
|
|
914
|
+
ctx.state.update(setCursor, target);
|
|
915
|
+
if (options?.viewpoint) {
|
|
916
|
+
ctx.state.update(patchSettings, { zoom: options.viewpoint.zoom });
|
|
917
|
+
applyAnchor(options.viewpoint.anchor);
|
|
885
918
|
return;
|
|
886
919
|
}
|
|
887
920
|
const placement = () => {
|
|
888
|
-
const
|
|
889
|
-
if (!
|
|
890
|
-
const item = paged() ?
|
|
891
|
-
const zoom = _embedpdf_core_stage.resolveZoom(
|
|
921
|
+
const current = buildScene();
|
|
922
|
+
if (!current.itemCount) return null;
|
|
923
|
+
const item = paged() ? current.items[0] : current.items[itemIndexOfPage(target)];
|
|
924
|
+
const zoom = (0, _embedpdf_core_stage.resolveZoom)(state().zoom, fitBox(item), viewport(), padding());
|
|
892
925
|
const subject = isFitAll() ? sceneRect() : fits(itemRect(item), zoom) ? itemRect(item) : pageRectOf(item, target);
|
|
893
|
-
const
|
|
894
|
-
...
|
|
895
|
-
...
|
|
926
|
+
const landing = {
|
|
927
|
+
...state().arrivalAlign,
|
|
928
|
+
...options?.arrivalAlign
|
|
896
929
|
};
|
|
897
|
-
const placed = _embedpdf_core_stage.placeCamera(subject,
|
|
898
|
-
x:
|
|
899
|
-
y:
|
|
900
|
-
},
|
|
930
|
+
const placed = (0, _embedpdf_core_stage.placeCamera)(subject, viewport(), zoom, padding(), {
|
|
931
|
+
x: landing.x === "keep" ? "start" : landing.x,
|
|
932
|
+
y: landing.y === "keep" ? "start" : landing.y
|
|
933
|
+
}, state().direction);
|
|
901
934
|
const bounds = boundsFor(item);
|
|
902
|
-
const settled = _embedpdf_core_stage.clampCamera(placed, bounds,
|
|
935
|
+
const settled = (0, _embedpdf_core_stage.clampCamera)(placed, bounds, viewport(), {
|
|
903
936
|
...constraint(),
|
|
904
937
|
bounded: true
|
|
905
938
|
});
|
|
906
939
|
return {
|
|
907
940
|
camera: {
|
|
908
941
|
zoom,
|
|
909
|
-
x:
|
|
910
|
-
y:
|
|
942
|
+
x: landing.x === "keep" ? camera().x : settled.x,
|
|
943
|
+
y: landing.y === "keep" ? camera().y : settled.y
|
|
911
944
|
},
|
|
912
945
|
bounds
|
|
913
946
|
};
|
|
914
947
|
};
|
|
915
|
-
if ((
|
|
916
|
-
const
|
|
917
|
-
if (
|
|
948
|
+
if ((options?.behavior ?? state().scrollBehavior) === "smooth") {
|
|
949
|
+
const landing = placement();
|
|
950
|
+
if (landing) animateTo(landing.camera, landing.bounds);
|
|
918
951
|
} else stabilized(() => {
|
|
919
|
-
const
|
|
920
|
-
if (
|
|
952
|
+
const landing = placement();
|
|
953
|
+
if (landing) writeCamera(landing.camera, landing.bounds);
|
|
921
954
|
});
|
|
922
955
|
};
|
|
923
|
-
/** Step by the navigation unit: the
|
|
924
|
-
const step = (direction,
|
|
925
|
-
const st = ctx.getState();
|
|
956
|
+
/** Step by the navigation unit: the item when it fits the viewport, else the page. */
|
|
957
|
+
const step = (direction, options) => {
|
|
926
958
|
const item = cursorItem();
|
|
927
959
|
if (!item) return;
|
|
928
|
-
const { grouping:
|
|
929
|
-
if (isFitAll() || fits(itemRect(item),
|
|
930
|
-
const
|
|
931
|
-
goToTarget(
|
|
932
|
-
} else goToTarget(
|
|
960
|
+
const { grouping: groups } = grouping();
|
|
961
|
+
if (isFitAll() || fits(itemRect(item), camera().zoom)) {
|
|
962
|
+
const index = Math.max(0, Math.min(itemIndexOfPage(state().cursor) + direction, groups.length - 1));
|
|
963
|
+
goToTarget(groups[index][0], options);
|
|
964
|
+
} else goToTarget(state().cursor + direction, options);
|
|
965
|
+
};
|
|
966
|
+
const resetView = () => goToTarget(0, { behavior: "instant" });
|
|
967
|
+
return {
|
|
968
|
+
stabilized,
|
|
969
|
+
applyAnchor,
|
|
970
|
+
reapply,
|
|
971
|
+
goToTarget,
|
|
972
|
+
resetView,
|
|
973
|
+
api: {
|
|
974
|
+
goToPage: (page, options) => {
|
|
975
|
+
const index = indexOfTarget(page);
|
|
976
|
+
if (index !== null) whenPlaced(placement, options, (go) => goToTarget(index, go));
|
|
977
|
+
},
|
|
978
|
+
goToFirstPage: (options) => whenPlaced(placement, options, (go) => goToTarget(0, go)),
|
|
979
|
+
goToLastPage: (options) => whenPlaced(placement, options, (go) => goToTarget(Math.max(0, (ctx.document()?.pageCount ?? 1) - 1), go)),
|
|
980
|
+
canGoNext: () => state().cursor < pageCount() - 1,
|
|
981
|
+
canGoPrevious: () => state().cursor > 0,
|
|
982
|
+
nextPage: (options) => whenPlaced(placement, options, (go) => step(1, go)),
|
|
983
|
+
previousPage: (options) => whenPlaced(placement, options, (go) => step(-1, go)),
|
|
984
|
+
resetView
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/destination.ts
|
|
990
|
+
function revealOfDestination(destination, size) {
|
|
991
|
+
const { width, height } = size;
|
|
992
|
+
switch (destination.kind) {
|
|
993
|
+
case "xyz": {
|
|
994
|
+
const { x, y, zoom } = destination;
|
|
995
|
+
return {
|
|
996
|
+
rect: {
|
|
997
|
+
x: x ?? 0,
|
|
998
|
+
y: y ?? 0,
|
|
999
|
+
width: 0,
|
|
1000
|
+
height: 0
|
|
1001
|
+
},
|
|
1002
|
+
anchor: {
|
|
1003
|
+
x: x != null ? "start" : "keep",
|
|
1004
|
+
y: y != null ? "start" : "keep"
|
|
1005
|
+
},
|
|
1006
|
+
zoom: zoom != null && zoom !== 0 ? { level: zoom } : "keep"
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
case "fit": return { zoom: "fit" };
|
|
1010
|
+
case "fitB": return {
|
|
1011
|
+
rect: {
|
|
1012
|
+
x: 0,
|
|
1013
|
+
y: 0,
|
|
1014
|
+
width,
|
|
1015
|
+
height
|
|
1016
|
+
},
|
|
1017
|
+
zoom: "fit"
|
|
1018
|
+
};
|
|
1019
|
+
case "fitH":
|
|
1020
|
+
case "fitBH": {
|
|
1021
|
+
const { y } = destination;
|
|
1022
|
+
return {
|
|
1023
|
+
rect: {
|
|
1024
|
+
x: 0,
|
|
1025
|
+
y: y ?? 0,
|
|
1026
|
+
width,
|
|
1027
|
+
height: 0
|
|
1028
|
+
},
|
|
1029
|
+
zoom: "fit-width",
|
|
1030
|
+
anchor: { y: y != null ? "start" : "keep" }
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
case "fitV":
|
|
1034
|
+
case "fitBV": {
|
|
1035
|
+
const { x } = destination;
|
|
1036
|
+
return {
|
|
1037
|
+
rect: {
|
|
1038
|
+
x: x ?? 0,
|
|
1039
|
+
y: 0,
|
|
1040
|
+
width: 0,
|
|
1041
|
+
height
|
|
1042
|
+
},
|
|
1043
|
+
zoom: "fit-height",
|
|
1044
|
+
anchor: { x: x != null ? "start" : "keep" }
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
case "fitR": {
|
|
1048
|
+
const { x, y, width: rectWidth, height: rectHeight } = destination;
|
|
1049
|
+
return {
|
|
1050
|
+
rect: {
|
|
1051
|
+
x,
|
|
1052
|
+
y,
|
|
1053
|
+
width: rectWidth,
|
|
1054
|
+
height: rectHeight
|
|
1055
|
+
},
|
|
1056
|
+
zoom: "fit"
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
//#endregion
|
|
1062
|
+
//#region src/navigation/reveal.ts
|
|
1063
|
+
/**
|
|
1064
|
+
* Reveals and destinations. A bare reveal is not navigation: minimal
|
|
1065
|
+
* visibility, cursor untouched (in paged flow, revealing a page is navigating
|
|
1066
|
+
* to it). A positioned reveal (rect, anchor or zoom: a search hit) is an
|
|
1067
|
+
* arrival: the cursor is set up front, the camera lands per the anchor
|
|
1068
|
+
* policy, and a resolved zoom becomes the zoom intent. A destination is a
|
|
1069
|
+
* positioned reveal of the spot it names.
|
|
1070
|
+
*/
|
|
1071
|
+
function createReveal(ctx, { scene, placement }, { writeCamera, markCause }, { cancelAnimation, animateTo }, { stabilized, goToTarget }) {
|
|
1072
|
+
const { camera, viewport, padding, paged, itemIndexOfPage, worldPageFrame, buildScene, sceneRect, pageRectOf, worldRectForContent, boundsFor, indexOfTarget } = scene;
|
|
1073
|
+
const state = () => ctx.state.get();
|
|
1074
|
+
/**
|
|
1075
|
+
* One axis of a positioned-reveal camera. `undefined` is 'nearest' (move
|
|
1076
|
+
* only when the target is outside the padded view), unless the zoom just
|
|
1077
|
+
* changed: then "do not move" is meaningless and the slack-axis rule
|
|
1078
|
+
* (center) applies. 'keep' never moves the axis (PDF /XYZ null).
|
|
1079
|
+
*/
|
|
1080
|
+
const revealAxis = (anchor, cameraPosition, rectPosition, rectExtent, viewportExtent, zoom, zoomChanged) => {
|
|
1081
|
+
if (anchor === "keep") return cameraPosition;
|
|
1082
|
+
const inset = padding();
|
|
1083
|
+
if (anchor === void 0) {
|
|
1084
|
+
if (!zoomChanged) {
|
|
1085
|
+
const low = cameraPosition + inset / zoom;
|
|
1086
|
+
const high = cameraPosition + (viewportExtent - inset) / zoom;
|
|
1087
|
+
if (rectPosition >= low && rectPosition + rectExtent <= high) return cameraPosition;
|
|
1088
|
+
if (rectExtent > high - low || rectPosition < low) return rectPosition - inset / zoom;
|
|
1089
|
+
return rectPosition + rectExtent - (viewportExtent - inset) / zoom;
|
|
1090
|
+
}
|
|
1091
|
+
anchor = "center";
|
|
1092
|
+
}
|
|
1093
|
+
if (anchor === "start") return rectPosition - inset / zoom;
|
|
1094
|
+
if (anchor === "end") return rectPosition + rectExtent - (viewportExtent - inset) / zoom;
|
|
1095
|
+
const fraction = anchor === "center" ? .5 : Math.min(1, Math.max(0, anchor));
|
|
1096
|
+
return rectPosition + rectExtent / 2 - viewportExtent * fraction / zoom;
|
|
1097
|
+
};
|
|
1098
|
+
const revealAt = (pageIndex, options) => {
|
|
1099
|
+
markCause("programmatic");
|
|
1100
|
+
const pageCount = ctx.document()?.pageCount ?? 0;
|
|
1101
|
+
if (pageCount === 0) return;
|
|
1102
|
+
const target = Math.max(0, Math.min(pageIndex, pageCount - 1));
|
|
1103
|
+
if (!(!!options && (options.rect != null || options.anchor !== void 0 || options.zoom !== void 0 && options.zoom !== "keep"))) {
|
|
1104
|
+
if (paged()) {
|
|
1105
|
+
goToTarget(target, options);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const current = buildScene();
|
|
1109
|
+
if (!current.itemCount) return;
|
|
1110
|
+
const page = pageRectOf(current.items[itemIndexOfPage(target)], target);
|
|
1111
|
+
const frame = worldPageFrame();
|
|
1112
|
+
const box = {
|
|
1113
|
+
x: page.x - frame.left,
|
|
1114
|
+
y: page.y - frame.top,
|
|
1115
|
+
width: page.width + frame.left + frame.right,
|
|
1116
|
+
height: page.height + frame.top + frame.bottom
|
|
1117
|
+
};
|
|
1118
|
+
const next = (0, _embedpdf_core_stage.revealCamera)(camera(), box, viewport(), padding());
|
|
1119
|
+
const now = camera();
|
|
1120
|
+
if (next.x === now.x && next.y === now.y) return;
|
|
1121
|
+
cancelAnimation();
|
|
1122
|
+
if ((options?.behavior ?? state().scrollBehavior) === "smooth") animateTo(next, sceneRect());
|
|
1123
|
+
else writeCamera(next, sceneRect());
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
cancelAnimation();
|
|
1127
|
+
ctx.state.update(setCursor, target);
|
|
1128
|
+
const place = () => {
|
|
1129
|
+
const current = buildScene();
|
|
1130
|
+
if (!current.itemCount) return null;
|
|
1131
|
+
const item = paged() ? current.items[0] : current.items[itemIndexOfPage(target)];
|
|
1132
|
+
const world = options.rect ? worldRectForContent(item, target, options.rect) : pageRectOf(item, target);
|
|
1133
|
+
const zoomDirective = options.zoom ?? "keep";
|
|
1134
|
+
const availableWidth = Math.max(1, viewport().width - 2 * padding());
|
|
1135
|
+
const availableHeight = Math.max(1, viewport().height - 2 * padding());
|
|
1136
|
+
let zoom = typeof zoomDirective === "object" ? zoomDirective.level : zoomDirective === "fit" ? Math.min(availableWidth / world.width, availableHeight / world.height) : zoomDirective === "fit-width" ? availableWidth / world.width : zoomDirective === "fit-height" ? availableHeight / world.height : camera().zoom;
|
|
1137
|
+
if (!Number.isFinite(zoom) || zoom <= 0) zoom = camera().zoom;
|
|
1138
|
+
const zoomChanged = zoomDirective !== "keep";
|
|
1139
|
+
const anchor = options.anchor ?? {};
|
|
1140
|
+
return {
|
|
1141
|
+
camera: {
|
|
1142
|
+
x: revealAxis(anchor.x, camera().x, world.x, world.width, viewport().width, zoom, zoomChanged),
|
|
1143
|
+
y: revealAxis(anchor.y, camera().y, world.y, world.height, viewport().height, zoom, zoomChanged),
|
|
1144
|
+
zoom
|
|
1145
|
+
},
|
|
1146
|
+
bounds: boundsFor(item),
|
|
1147
|
+
zoomChanged
|
|
1148
|
+
};
|
|
1149
|
+
};
|
|
1150
|
+
const first = place();
|
|
1151
|
+
if (!first) return;
|
|
1152
|
+
if (first.zoomChanged) ctx.state.update(patchSettings, { zoom: { level: first.camera.zoom } });
|
|
1153
|
+
if ((options.behavior ?? state().scrollBehavior) === "smooth") {
|
|
1154
|
+
const landing = place() ?? first;
|
|
1155
|
+
animateTo(landing.camera, landing.bounds);
|
|
1156
|
+
} else stabilized(() => {
|
|
1157
|
+
const landing = place();
|
|
1158
|
+
if (landing) writeCamera(landing.camera, landing.bounds);
|
|
1159
|
+
});
|
|
933
1160
|
};
|
|
934
|
-
const
|
|
1161
|
+
const reveal = (page, options) => {
|
|
1162
|
+
const index = indexOfTarget(page);
|
|
1163
|
+
if (index !== null) whenPlaced(placement, options, (go) => revealAt(index, go));
|
|
1164
|
+
};
|
|
1165
|
+
const goToDestination = (destination, options) => {
|
|
1166
|
+
const layout = ctx.getPage(destination.page);
|
|
1167
|
+
if (!layout) return;
|
|
1168
|
+
whenPlaced(placement, {
|
|
1169
|
+
...revealOfDestination(destination, layout.size),
|
|
1170
|
+
behavior: options?.behavior
|
|
1171
|
+
}, (go) => revealAt(layout.index, go));
|
|
1172
|
+
};
|
|
1173
|
+
return { api: {
|
|
1174
|
+
reveal,
|
|
1175
|
+
goToDestination
|
|
1176
|
+
} };
|
|
1177
|
+
}
|
|
1178
|
+
//#endregion
|
|
1179
|
+
//#region src/navigation/zoom.ts
|
|
1180
|
+
/**
|
|
1181
|
+
* Zoom verbs. Pointer-less zooms magnify around the zoomAlign focal point
|
|
1182
|
+
* (pinch and wheel pass their own pointer to `zoomAround`: physics beats
|
|
1183
|
+
* policy); fit modes are zoom intents written through the settings; the
|
|
1184
|
+
* double-tap ladder ascends reading postures.
|
|
1185
|
+
*/
|
|
1186
|
+
/** The zoom factor of one zoomIn or zoomOut step. */
|
|
1187
|
+
const ZOOM_STEP = 1.2;
|
|
1188
|
+
function createZoom(ctx, { scene }, { syncCursorFromCamera }, { cancelAnimation, animateZoomAnchored }, gestures, settings) {
|
|
1189
|
+
const { camera, viewport, padding, paged, itemIndexOfPage, buildScene, alignPoint, fitBox } = scene;
|
|
1190
|
+
const state = () => ctx.state.get();
|
|
1191
|
+
const zoomAlignPoint = () => alignPoint(state().zoomAlign, viewport());
|
|
1192
|
+
const setViewRotation = (viewRotation) => settings.updateSettings({ viewRotation });
|
|
1193
|
+
return { api: {
|
|
1194
|
+
doubleTapZoom: (point) => {
|
|
1195
|
+
cancelAnimation();
|
|
1196
|
+
const before = buildScene();
|
|
1197
|
+
if (!before.itemCount) return;
|
|
1198
|
+
const item = paged() ? before.items[0] : before.items[itemIndexOfPage(state().cursor)];
|
|
1199
|
+
const fit = fitBox(item);
|
|
1200
|
+
const automatic = (0, _embedpdf_core_stage.resolveZoom)({ mode: _embedpdf_core_stage.ZoomMode.Automatic }, fit, viewport(), padding());
|
|
1201
|
+
const stops = [
|
|
1202
|
+
automatic,
|
|
1203
|
+
(0, _embedpdf_core_stage.resolveZoom)({ mode: _embedpdf_core_stage.ZoomMode.FitWidth }, fit, viewport(), padding()),
|
|
1204
|
+
Math.min(automatic * 2.5, _embedpdf_core_stage.ZOOM_MAX)
|
|
1205
|
+
].sort((left, right) => left - right).filter((zoom, index, all) => index === 0 || zoom > all[index - 1] * 1.1);
|
|
1206
|
+
const near = (zoom, stop) => zoom > stop / 1.1 && zoom < stop * 1.1;
|
|
1207
|
+
const onRung = stops.findIndex((stop) => near(camera().zoom, stop));
|
|
1208
|
+
const target = onRung >= 0 ? stops[(onRung + 1) % stops.length] : stops[0];
|
|
1209
|
+
ctx.state.update(patchSettings, { zoom: { level: target } });
|
|
1210
|
+
const after = buildScene();
|
|
1211
|
+
if (!after.itemCount) return;
|
|
1212
|
+
animateZoomAnchored((0, _embedpdf_core_stage.anchorAtPoint)(after, (0, _embedpdf_core_stage.toWorld)(camera(), point)), point, target, void 0, syncCursorFromCamera);
|
|
1213
|
+
},
|
|
1214
|
+
zoomIn: () => gestures.zoomAround(zoomAlignPoint(), ZOOM_STEP),
|
|
1215
|
+
zoomOut: () => gestures.zoomAround(zoomAlignPoint(), 1 / ZOOM_STEP),
|
|
1216
|
+
zoomTo: (zoom, options) => {
|
|
1217
|
+
if (typeof zoom === "number") {
|
|
1218
|
+
if (options?.around) gestures.zoomAround(options.around, zoom / camera().zoom);
|
|
1219
|
+
else settings.updateSettings({ zoom: { level: zoom } });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
settings.updateSettings({ zoom });
|
|
1223
|
+
},
|
|
1224
|
+
zoomBy: (factor, options) => gestures.zoomAround(options?.around ?? zoomAlignPoint(), factor),
|
|
1225
|
+
fitWidth: () => settings.updateSettings({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitWidth } }),
|
|
1226
|
+
fitPage: () => settings.updateSettings({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitPage } }),
|
|
1227
|
+
fitAll: () => settings.updateSettings({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitAll } }),
|
|
1228
|
+
fitAutomatic: () => settings.updateSettings({ zoom: { mode: _embedpdf_core_stage.ZoomMode.Automatic } }),
|
|
1229
|
+
setViewRotation,
|
|
1230
|
+
rotateViewBy: (delta) => setViewRotation((0, _embedpdf_core_geometry.addRotations)(state().viewRotation, delta === 90 ? 90 : 270))
|
|
1231
|
+
} };
|
|
1232
|
+
}
|
|
1233
|
+
//#endregion
|
|
1234
|
+
//#region src/read/pages.ts
|
|
1235
|
+
/**
|
|
1236
|
+
* The page reads: laid-out pages with their transforms (PDF points → view px
|
|
1237
|
+
* → device px), the scroller projection, the visible set (memoized so reads
|
|
1238
|
+
* stay reference-stable), coordinate conversions and the camera, zoom and
|
|
1239
|
+
* viewpoint reads.
|
|
1240
|
+
*/
|
|
1241
|
+
function createPageReads(ctx, { scene }) {
|
|
1242
|
+
const { camera, viewport, dpr, padding, paged, buildScene, cacheKey, stayBounds, cursorItem, currentAnchor } = scene;
|
|
1243
|
+
const state = () => ctx.state.get();
|
|
1244
|
+
const pageObjectNumberAt = (index) => ctx.document()?.pages[index]?.ref.objectNumber ?? index + 1;
|
|
935
1245
|
const unrotatedPoints = (box) => {
|
|
936
1246
|
const content = (0, _embedpdf_core_geometry.displaySize)({
|
|
937
1247
|
width: box.width,
|
|
@@ -943,38 +1253,38 @@ function createStageCapability(ctx, config = {}) {
|
|
|
943
1253
|
};
|
|
944
1254
|
};
|
|
945
1255
|
const withTransform = (box) => {
|
|
946
|
-
const
|
|
947
|
-
const pageSize =
|
|
948
|
-
width:
|
|
949
|
-
height:
|
|
1256
|
+
const registered = ctx.document()?.pages[box.pageIndex];
|
|
1257
|
+
const pageSize = registered ? {
|
|
1258
|
+
width: registered.size.width,
|
|
1259
|
+
height: registered.size.height
|
|
950
1260
|
} : unrotatedPoints(box);
|
|
951
|
-
const
|
|
1261
|
+
const current = camera();
|
|
952
1262
|
const ratio = dpr();
|
|
953
|
-
const rawX = (box.x -
|
|
954
|
-
const rawY = (box.y -
|
|
955
|
-
const resting =
|
|
1263
|
+
const rawX = (box.x - current.x) * current.zoom;
|
|
1264
|
+
const rawY = (box.y - current.y) * current.zoom;
|
|
1265
|
+
const resting = state().cameraResting;
|
|
956
1266
|
const screenX = resting ? (0, _embedpdf_core_geometry.snapToDevice)(rawX, ratio) : rawX;
|
|
957
1267
|
const screenY = resting ? (0, _embedpdf_core_geometry.snapToDevice)(rawY, ratio) : rawY;
|
|
958
1268
|
const transform = (0, _embedpdf_core_geometry.pageTransform)({
|
|
959
1269
|
pageSize,
|
|
960
1270
|
rotation: box.rotation,
|
|
961
|
-
scale: box.contentScale *
|
|
962
|
-
baseScale:
|
|
1271
|
+
scale: box.contentScale * current.zoom,
|
|
1272
|
+
baseScale: state().viewUnitsPerPoint * (registered?.userUnit ?? 1),
|
|
963
1273
|
dpr: ratio
|
|
964
1274
|
});
|
|
965
|
-
const
|
|
1275
|
+
const size = viewport();
|
|
966
1276
|
const onScreen = (0, _embedpdf_core_geometry.intersectRects)({
|
|
967
1277
|
x: -screenX,
|
|
968
1278
|
y: -screenY,
|
|
969
|
-
width:
|
|
970
|
-
height:
|
|
1279
|
+
width: size.width,
|
|
1280
|
+
height: size.height
|
|
971
1281
|
}, {
|
|
972
1282
|
x: 0,
|
|
973
1283
|
y: 0,
|
|
974
1284
|
width: transform.viewWidth,
|
|
975
1285
|
height: transform.viewHeight
|
|
976
1286
|
});
|
|
977
|
-
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.
|
|
1287
|
+
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.viewToPageRect(onScreen) : {
|
|
978
1288
|
x: 0,
|
|
979
1289
|
y: 0,
|
|
980
1290
|
width: 0,
|
|
@@ -982,14 +1292,14 @@ function createStageCapability(ctx, config = {}) {
|
|
|
982
1292
|
};
|
|
983
1293
|
return {
|
|
984
1294
|
...box,
|
|
985
|
-
|
|
1295
|
+
ref: (0, _embedpdf_core.toPageRef)(pageObjectNumberAt(box.pageIndex)),
|
|
986
1296
|
screenX,
|
|
987
1297
|
screenY,
|
|
988
1298
|
transform,
|
|
989
1299
|
visibleRect
|
|
990
1300
|
};
|
|
991
1301
|
};
|
|
992
|
-
const
|
|
1302
|
+
const emptyScrollMetrics = {
|
|
993
1303
|
scrollLeft: 0,
|
|
994
1304
|
scrollTop: 0,
|
|
995
1305
|
scrollWidth: 0,
|
|
@@ -999,717 +1309,803 @@ function createStageCapability(ctx, config = {}) {
|
|
|
999
1309
|
scrollableX: false,
|
|
1000
1310
|
scrollableY: false
|
|
1001
1311
|
};
|
|
1002
|
-
let
|
|
1312
|
+
let lastMetrics = null;
|
|
1003
1313
|
const scrollMetricsNow = () => {
|
|
1004
|
-
if (!
|
|
1005
|
-
const
|
|
1006
|
-
const m = _embedpdf_core_stage.scrollMetrics(cam(), sc.itemCount ? stayBounds() : {
|
|
1314
|
+
if (!state().placed) return emptyScrollMetrics;
|
|
1315
|
+
const metrics = (0, _embedpdf_core_stage.scrollMetrics)(camera(), buildScene().itemCount ? stayBounds() : {
|
|
1007
1316
|
x: 0,
|
|
1008
1317
|
y: 0,
|
|
1009
1318
|
width: 0,
|
|
1010
1319
|
height: 0
|
|
1011
|
-
},
|
|
1012
|
-
const
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1015
|
-
return
|
|
1016
|
-
};
|
|
1017
|
-
const EMPTY_VISIBLE_PAGES = [];
|
|
1018
|
-
let visSig = "";
|
|
1019
|
-
let vis = [];
|
|
1020
|
-
const visiblePages = () => {
|
|
1021
|
-
if (!ctx.getState().placed) return EMPTY_VISIBLE_PAGES;
|
|
1022
|
-
const c = cam();
|
|
1023
|
-
const v = vp();
|
|
1024
|
-
const sc = buildScene();
|
|
1025
|
-
const items = paged() ? sc.items.slice(0, 1) : sc.query(_embedpdf_core_stage.cameraWorldRect(c, v));
|
|
1026
|
-
const sig = `${sceneCache.key}|${ctx.getState().flow}|${c.x},${c.y},${c.zoom}|${v.width}x${v.height}|${dpr()}|r${ctx.getState().cameraResting ? 1 : 0}`;
|
|
1027
|
-
if (sig === visSig) return vis;
|
|
1028
|
-
visSig = sig;
|
|
1029
|
-
vis = items.flatMap((it) => it.pages).map(withTransform);
|
|
1030
|
-
return vis;
|
|
1031
|
-
};
|
|
1032
|
-
const pickSettings = (src) => {
|
|
1033
|
-
const out = {};
|
|
1034
|
-
for (const k of SETTING_KEYS) out[k] = src[k];
|
|
1035
|
-
return out;
|
|
1036
|
-
};
|
|
1037
|
-
const snapshotSettings = () => pickSettings(ctx.getState());
|
|
1038
|
-
/**
|
|
1039
|
-
* Settings change, reacted per the registry — the strongest effect among the
|
|
1040
|
-
* touched settings wins: 'reflow' ⊃ 'scene'/'refit' ⊃ 'reclamp' ⊃ 'none'.
|
|
1041
|
-
* The ONE reactive applier: the public `update()` and the responsive driver
|
|
1042
|
-
* both land here, so a breakpoint crossing gets exactly the invariant a
|
|
1043
|
-
* hand-written update would.
|
|
1044
|
-
*/
|
|
1045
|
-
const applySettings = (patch) => {
|
|
1046
|
-
cancelAnim();
|
|
1047
|
-
const touched = (effect) => SETTING_KEYS.some((k) => patch[k] !== void 0 && SETTINGS_EFFECT[k] === effect);
|
|
1048
|
-
const align = touched("refit") && !touched("scene") && !touched("reflow") ? ctx.getState().zoomAlign : ctx.getState().anchorAlign;
|
|
1049
|
-
const anchor = anchorAt(alignPoint(align, vp()));
|
|
1050
|
-
ctx.dispatch({
|
|
1051
|
-
type: "PATCH",
|
|
1052
|
-
patch
|
|
1053
|
-
});
|
|
1054
|
-
if (touched("scene")) sceneCache = null;
|
|
1055
|
-
if (touched("reflow")) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
1056
|
-
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
1057
|
-
else if (touched("reclamp")) setCam(cam());
|
|
1320
|
+
}, viewport(), padding());
|
|
1321
|
+
const previous = lastMetrics;
|
|
1322
|
+
if (previous && previous.scrollLeft === metrics.scrollLeft && previous.scrollTop === metrics.scrollTop && previous.scrollWidth === metrics.scrollWidth && previous.scrollHeight === metrics.scrollHeight && previous.clientWidth === metrics.clientWidth && previous.clientHeight === metrics.clientHeight) return previous;
|
|
1323
|
+
lastMetrics = metrics;
|
|
1324
|
+
return metrics;
|
|
1058
1325
|
};
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1326
|
+
const noVisiblePages = [];
|
|
1327
|
+
const placedVisiblePages = (0, _embedpdf_core.memo)(() => {
|
|
1328
|
+
const current = camera();
|
|
1329
|
+
const size = viewport();
|
|
1330
|
+
buildScene();
|
|
1331
|
+
return [
|
|
1332
|
+
cacheKey(),
|
|
1333
|
+
state().flow,
|
|
1334
|
+
current.x,
|
|
1335
|
+
current.y,
|
|
1336
|
+
current.zoom,
|
|
1337
|
+
size.width,
|
|
1338
|
+
size.height,
|
|
1339
|
+
dpr(),
|
|
1340
|
+
state().cameraResting
|
|
1341
|
+
];
|
|
1342
|
+
}, (_sceneKey, _flow, x, y, zoom, width, height, _dpr, _resting) => {
|
|
1343
|
+
const scene = buildScene();
|
|
1344
|
+
return (paged() ? scene.items.slice(0, 1) : scene.query((0, _embedpdf_core_stage.cameraWorldRect)({
|
|
1345
|
+
x,
|
|
1346
|
+
y,
|
|
1347
|
+
zoom
|
|
1348
|
+
}, {
|
|
1349
|
+
width,
|
|
1350
|
+
height
|
|
1351
|
+
}))).flatMap((item) => item.pages).map(withTransform);
|
|
1352
|
+
});
|
|
1353
|
+
const visiblePages = () => state().placed ? placedVisiblePages() : noVisiblePages;
|
|
1354
|
+
const getPageFrame = (page) => {
|
|
1355
|
+
if (!state().placed) return null;
|
|
1356
|
+
const index = ctx.getPage(page)?.index ?? -1;
|
|
1357
|
+
if (index < 0) return null;
|
|
1358
|
+
const scene = buildScene();
|
|
1359
|
+
if (!scene.itemCount) return null;
|
|
1360
|
+
const box = scene.items[scene.itemOfPage(index)].pages.find((candidate) => candidate.pageIndex === index);
|
|
1361
|
+
return box ? withTransform(box) : null;
|
|
1068
1362
|
};
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
lastEffective = effective;
|
|
1081
|
-
publishActive(active);
|
|
1082
|
-
const keys = Object.keys(patch);
|
|
1083
|
-
if (keys.length === 0) return "none";
|
|
1084
|
-
if (react) {
|
|
1085
|
-
applySettings(patch);
|
|
1086
|
-
return "none";
|
|
1087
|
-
}
|
|
1088
|
-
ctx.dispatch({
|
|
1089
|
-
type: "PATCH",
|
|
1090
|
-
patch
|
|
1091
|
-
});
|
|
1092
|
-
const rank = {
|
|
1093
|
-
none: 0,
|
|
1094
|
-
reclamp: 1,
|
|
1095
|
-
refit: 2,
|
|
1096
|
-
scene: 3,
|
|
1097
|
-
reflow: 4
|
|
1363
|
+
const pageToWorld = (page, point) => {
|
|
1364
|
+
const frame = getPageFrame(page);
|
|
1365
|
+
if (!frame) return null;
|
|
1366
|
+
const content = (0, _embedpdf_core_geometry.displaySize)({
|
|
1367
|
+
width: frame.width,
|
|
1368
|
+
height: frame.height
|
|
1369
|
+
}, frame.rotation);
|
|
1370
|
+
const offset = (0, _embedpdf_core_geometry.applyPoint)((0, _embedpdf_core_geometry.rotateScaleMatrix)(frame.contentScale, content.width, content.height, frame.rotation), point);
|
|
1371
|
+
return {
|
|
1372
|
+
x: frame.x + offset.x,
|
|
1373
|
+
y: frame.y + offset.y
|
|
1098
1374
|
};
|
|
1099
|
-
let strongest = "none";
|
|
1100
|
-
for (const k of keys) if (rank[SETTINGS_EFFECT[k]] > rank[strongest]) strongest = SETTINGS_EFFECT[k];
|
|
1101
|
-
if (strongest === "scene" || strongest === "reflow") sceneCache = null;
|
|
1102
|
-
return strongest;
|
|
1103
1375
|
};
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1376
|
+
const visiblePageOf = (page) => {
|
|
1377
|
+
const objectNumber = ctx.getPage(page)?.ref.objectNumber;
|
|
1378
|
+
if (objectNumber === void 0) return null;
|
|
1379
|
+
return visiblePages().find((visible) => visible.ref.objectNumber === objectNumber) ?? null;
|
|
1380
|
+
};
|
|
1381
|
+
return {
|
|
1382
|
+
scrollMetricsNow,
|
|
1110
1383
|
visiblePages,
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1384
|
+
api: {
|
|
1385
|
+
getCamera: camera,
|
|
1386
|
+
getViewportSize: viewport,
|
|
1387
|
+
getScrollMetrics: scrollMetricsNow,
|
|
1388
|
+
listVisiblePages: visiblePages,
|
|
1389
|
+
isPageVisible: (page) => visiblePageOf(page) !== null,
|
|
1390
|
+
getCurrentPageIndex: () => state().cursor,
|
|
1391
|
+
getCurrentPage: () => ctx.document()?.pages[state().cursor]?.ref ?? null,
|
|
1392
|
+
getPageCount: () => ctx.document()?.pageCount ?? 0,
|
|
1393
|
+
listCurrentItemPages: () => {
|
|
1394
|
+
const item = cursorItem();
|
|
1395
|
+
const pages = ctx.document()?.pages ?? [];
|
|
1396
|
+
return item ? item.pageIndexes.map((index) => pages[index]).filter((page) => page !== void 0) : [];
|
|
1397
|
+
},
|
|
1398
|
+
getPageFrame,
|
|
1399
|
+
getPageAt: (point) => {
|
|
1400
|
+
for (const page of visiblePages()) {
|
|
1401
|
+
const localX = point.x - page.screenX;
|
|
1402
|
+
const localY = point.y - page.screenY;
|
|
1403
|
+
if (localX >= 0 && localY >= 0 && localX <= page.transform.viewWidth && localY <= page.transform.viewHeight) return {
|
|
1404
|
+
ref: page.ref,
|
|
1405
|
+
point: page.transform.viewToPage({
|
|
1406
|
+
x: localX,
|
|
1407
|
+
y: localY
|
|
1408
|
+
}),
|
|
1409
|
+
scale: page.transform.viewScale,
|
|
1410
|
+
rotation: page.rotation,
|
|
1411
|
+
zoom: page.transform.zoom
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
return null;
|
|
1415
|
+
},
|
|
1416
|
+
viewportToPage: (page, point) => {
|
|
1417
|
+
const target = visiblePageOf(page);
|
|
1418
|
+
if (!target) return null;
|
|
1419
|
+
return target.transform.viewToPage({
|
|
1420
|
+
x: point.x - target.screenX,
|
|
1421
|
+
y: point.y - target.screenY
|
|
1422
|
+
});
|
|
1423
|
+
},
|
|
1424
|
+
pageToWorld,
|
|
1425
|
+
pageRectToViewport: (page, rect) => {
|
|
1426
|
+
const frame = getPageFrame(page);
|
|
1427
|
+
if (!frame) return null;
|
|
1428
|
+
const content = (0, _embedpdf_core_geometry.displaySize)({
|
|
1429
|
+
width: frame.width,
|
|
1430
|
+
height: frame.height
|
|
1431
|
+
}, frame.rotation);
|
|
1432
|
+
const world = (0, _embedpdf_core_geometry.applyRect)((0, _embedpdf_core_geometry.rotateScaleMatrix)(frame.contentScale, content.width, content.height, frame.rotation), rect);
|
|
1433
|
+
const current = camera();
|
|
1434
|
+
const topLeft = (0, _embedpdf_core_stage.toScreen)(current, {
|
|
1435
|
+
x: frame.x + world.x,
|
|
1436
|
+
y: frame.y + world.y
|
|
1437
|
+
});
|
|
1438
|
+
return {
|
|
1439
|
+
x: topLeft.x,
|
|
1440
|
+
y: topLeft.y,
|
|
1441
|
+
width: world.width * current.zoom,
|
|
1442
|
+
height: world.height * current.zoom
|
|
1144
1443
|
};
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const c = cam();
|
|
1178
|
-
const tl = _embedpdf_core_stage.toScreen(c, {
|
|
1179
|
-
x: pr.x + wr.x,
|
|
1180
|
-
y: pr.y + wr.y
|
|
1181
|
-
});
|
|
1182
|
-
return {
|
|
1183
|
-
x: tl.x,
|
|
1184
|
-
y: tl.y,
|
|
1185
|
-
width: wr.width * c.zoom,
|
|
1186
|
-
height: wr.height * c.zoom
|
|
1187
|
-
};
|
|
1188
|
-
},
|
|
1189
|
-
toScreen: (w) => _embedpdf_core_stage.toScreen(cam(), w),
|
|
1190
|
-
toWorld: (s) => _embedpdf_core_stage.toWorld(cam(), s),
|
|
1191
|
-
flow: () => ctx.getState().flow,
|
|
1192
|
-
layout: () => ctx.getState().layout,
|
|
1193
|
-
spread: () => ctx.getState().spread,
|
|
1194
|
-
sizing: () => ctx.getState().sizing,
|
|
1195
|
-
columns: () => ctx.getState().columns,
|
|
1196
|
-
bounded: () => ctx.getState().bounded,
|
|
1197
|
-
padding: () => ctx.getState().padding,
|
|
1198
|
-
gap: () => ctx.getState().gap,
|
|
1199
|
-
pageFrame: () => ctx.getState().pageFrame,
|
|
1200
|
-
fitAlign: () => ctx.getState().fitAlign,
|
|
1201
|
-
arrivalAlign: () => ctx.getState().arrivalAlign,
|
|
1202
|
-
zoomAlign: () => ctx.getState().zoomAlign,
|
|
1203
|
-
anchorAlign: () => ctx.getState().anchorAlign,
|
|
1204
|
-
direction: () => ctx.getState().direction,
|
|
1205
|
-
scrollBehavior: () => ctx.getState().scrollBehavior,
|
|
1206
|
-
viewRotation: () => ctx.getState().viewRotation,
|
|
1207
|
-
zoomLevel: () => cam().zoom,
|
|
1208
|
-
zoomMode: () => {
|
|
1209
|
-
const z = ctx.getState().zoom;
|
|
1210
|
-
return "mode" in z ? z.mode : "custom";
|
|
1211
|
-
},
|
|
1212
|
-
viewpoint: () => ({
|
|
1213
|
-
anchor: currentAnchor(),
|
|
1214
|
-
zoom: ctx.getState().zoom
|
|
1215
|
-
}),
|
|
1216
|
-
settings: snapshotSettings,
|
|
1217
|
-
viewState: () => ({
|
|
1218
|
-
...snapshotSettings(),
|
|
1219
|
-
cursor: ctx.getState().cursor,
|
|
1220
|
-
anchor: currentAnchor()
|
|
1221
|
-
}),
|
|
1222
|
-
setViewport: (v) => {
|
|
1223
|
-
if (!placementStarted) {
|
|
1224
|
-
ctx.dispatch({
|
|
1225
|
-
type: "VP",
|
|
1226
|
-
vp: v
|
|
1444
|
+
},
|
|
1445
|
+
worldToViewport: (world) => (0, _embedpdf_core_stage.toScreen)(camera(), world),
|
|
1446
|
+
viewportToWorld: (point) => (0, _embedpdf_core_stage.toWorld)(camera(), point),
|
|
1447
|
+
pageToViewport: (page, point) => {
|
|
1448
|
+
const world = pageToWorld(page, point);
|
|
1449
|
+
return world ? (0, _embedpdf_core_stage.toScreen)(camera(), world) : null;
|
|
1450
|
+
},
|
|
1451
|
+
getViewRotation: () => state().viewRotation,
|
|
1452
|
+
getZoomLevel: () => camera().zoom,
|
|
1453
|
+
getZoomMode: () => {
|
|
1454
|
+
const zoom = state().zoom;
|
|
1455
|
+
return "mode" in zoom ? zoom.mode : "custom";
|
|
1456
|
+
},
|
|
1457
|
+
getViewpoint: () => ({
|
|
1458
|
+
anchor: currentAnchor(),
|
|
1459
|
+
zoom: state().zoom
|
|
1460
|
+
}),
|
|
1461
|
+
getCurrentDestination: () => {
|
|
1462
|
+
const inset = padding();
|
|
1463
|
+
const current = camera();
|
|
1464
|
+
const first = visiblePages().filter((page) => {
|
|
1465
|
+
const corner = (0, _embedpdf_core_stage.toScreen)(current, {
|
|
1466
|
+
x: page.x + page.width,
|
|
1467
|
+
y: page.y + page.height
|
|
1468
|
+
});
|
|
1469
|
+
return corner.x > inset && corner.y > inset;
|
|
1470
|
+
}).sort((left, right) => left.pageIndex - right.pageIndex)[0];
|
|
1471
|
+
const size = first && ctx.getPage(first.ref)?.size;
|
|
1472
|
+
if (!first || !size) return null;
|
|
1473
|
+
const corner = first.transform.viewToPage({
|
|
1474
|
+
x: inset - first.screenX,
|
|
1475
|
+
y: inset - first.screenY
|
|
1227
1476
|
});
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1477
|
+
const clamp = (value, max) => Math.round(Math.min(Math.max(value, 0), max) * 100) / 100;
|
|
1478
|
+
return {
|
|
1479
|
+
kind: "xyz",
|
|
1480
|
+
page: first.ref,
|
|
1481
|
+
x: clamp(corner.x, size.width),
|
|
1482
|
+
y: clamp(corner.y, size.height),
|
|
1483
|
+
zoom: null
|
|
1484
|
+
};
|
|
1231
1485
|
}
|
|
1232
|
-
|
|
1233
|
-
const anchor = currentAnchor();
|
|
1234
|
-
ctx.dispatch({
|
|
1235
|
-
type: "VP",
|
|
1236
|
-
vp: v
|
|
1237
|
-
});
|
|
1238
|
-
if (syncResponsive(false) === "reflow") goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
1239
|
-
else reapply(anchor);
|
|
1240
|
-
},
|
|
1241
|
-
setDevicePixelRatio: (ratio) => {
|
|
1242
|
-
if (ratio > 0 && ratio !== dpr()) ctx.dispatch({
|
|
1243
|
-
type: "DPR",
|
|
1244
|
-
dpr: ratio
|
|
1245
|
-
});
|
|
1246
|
-
},
|
|
1247
|
-
setCamera: (c) => {
|
|
1248
|
-
markCause("user");
|
|
1249
|
-
cancelAnim();
|
|
1250
|
-
setCam(c);
|
|
1251
|
-
syncCursorFromCamera();
|
|
1252
|
-
},
|
|
1253
|
-
panBy: (dx, dy) => {
|
|
1254
|
-
markCause("user");
|
|
1255
|
-
cancelAnim();
|
|
1256
|
-
if (gestureDepth > 0 && gestureElastic) {
|
|
1257
|
-
gestureRaw = _embedpdf_core_stage.panByScreen(gestureRaw ?? unrubberize(cam()), dx, dy);
|
|
1258
|
-
setCamRaw(rubberize(gestureRaw));
|
|
1259
|
-
} else setCam(_embedpdf_core_stage.panByScreen(cam(), dx, dy));
|
|
1260
|
-
syncCursorFromCamera();
|
|
1261
|
-
},
|
|
1262
|
-
scrollTo: (opts) => {
|
|
1263
|
-
markCause("user");
|
|
1264
|
-
cancelAnim();
|
|
1265
|
-
if (!buildScene().itemCount) return;
|
|
1266
|
-
const next = _embedpdf_core_stage.cameraFromScroll(cam(), stayBounds(), vp(), pad(), opts);
|
|
1267
|
-
if (opts.behavior === "smooth") animateTo(next, stayBounds(), 240, syncCursorFromCamera);
|
|
1268
|
-
else {
|
|
1269
|
-
setCam(next);
|
|
1270
|
-
syncCursorFromCamera();
|
|
1271
|
-
}
|
|
1272
|
-
},
|
|
1273
|
-
scrollBy: ({ left, top, behavior }) => {
|
|
1274
|
-
const m = scrollMetricsNow();
|
|
1275
|
-
api.scrollTo({
|
|
1276
|
-
left: left === void 0 ? void 0 : m.scrollLeft + left,
|
|
1277
|
-
top: top === void 0 ? void 0 : m.scrollTop + top,
|
|
1278
|
-
behavior
|
|
1279
|
-
});
|
|
1280
|
-
},
|
|
1281
|
-
zoomAround: (pt, factor) => {
|
|
1282
|
-
cancelAnim();
|
|
1283
|
-
const before = buildScene();
|
|
1284
|
-
const focal = before.itemCount ? _embedpdf_core_stage.anchorAtPoint(before, _embedpdf_core_stage.toWorld(cam(), pt)) : null;
|
|
1285
|
-
setCam(_embedpdf_core_stage.zoomAround(cam(), pt, factor));
|
|
1286
|
-
if (gestureDepth === 0) ctx.dispatch({
|
|
1287
|
-
type: "PATCH",
|
|
1288
|
-
patch: { zoom: { level: cam().zoom } }
|
|
1289
|
-
});
|
|
1290
|
-
const after = buildScene();
|
|
1291
|
-
if (after !== before && focal && after.itemCount) setCam(_embedpdf_core_stage.cameraForAnchorAtScreen(focal, after, pt, cam().zoom));
|
|
1292
|
-
if (gestureDepth > 0 && gestureElastic) gestureRaw = unrubberize(cam());
|
|
1293
|
-
syncCursorFromCamera();
|
|
1294
|
-
},
|
|
1295
|
-
beginGesture: (options) => {
|
|
1296
|
-
gestureDepth++;
|
|
1297
|
-
if (gestureDepth === 1) {
|
|
1298
|
-
cancelAnim();
|
|
1299
|
-
gestureZoomed = false;
|
|
1300
|
-
gestureElastic = options?.elastic === true;
|
|
1301
|
-
gestureRaw = null;
|
|
1302
|
-
}
|
|
1303
|
-
},
|
|
1304
|
-
endGesture: () => {
|
|
1305
|
-
if (gestureDepth === 0) return;
|
|
1306
|
-
gestureDepth--;
|
|
1307
|
-
if (gestureDepth > 0) return;
|
|
1308
|
-
gestureRaw = null;
|
|
1309
|
-
const wasElastic = gestureElastic;
|
|
1310
|
-
gestureElastic = false;
|
|
1311
|
-
if (gestureZoomed) {
|
|
1312
|
-
gestureZoomed = false;
|
|
1313
|
-
ctx.dispatch({
|
|
1314
|
-
type: "PATCH",
|
|
1315
|
-
patch: { zoom: { level: cam().zoom } }
|
|
1316
|
-
});
|
|
1317
|
-
armRest();
|
|
1318
|
-
}
|
|
1319
|
-
syncCursorFromCamera();
|
|
1320
|
-
if (wasElastic) {
|
|
1321
|
-
const c = cam();
|
|
1322
|
-
const cl = _embedpdf_core_stage.clampCamera(c, stayBounds(), vp(), constraint());
|
|
1323
|
-
if (cl.x !== c.x || cl.y !== c.y) startCameraPhysics(0, 0);
|
|
1324
|
-
}
|
|
1325
|
-
},
|
|
1326
|
-
fling: (vx, vy) => startCameraPhysics(vx, vy),
|
|
1327
|
-
cameraInMotion: () => raf !== 0,
|
|
1328
|
-
doubleTapZoom: (pt) => {
|
|
1329
|
-
cancelAnim();
|
|
1330
|
-
const sc0 = buildScene();
|
|
1331
|
-
if (!sc0.itemCount) return;
|
|
1332
|
-
const item = paged() ? sc0.items[0] : sc0.items[itemIndexOfPage(ctx.getState().cursor)];
|
|
1333
|
-
const fit = fitBox(item);
|
|
1334
|
-
const auto = _embedpdf_core_stage.resolveZoom({ mode: _embedpdf_core_stage.ZoomMode.Automatic }, fit, vp(), pad());
|
|
1335
|
-
const stops = [
|
|
1336
|
-
auto,
|
|
1337
|
-
_embedpdf_core_stage.resolveZoom({ mode: _embedpdf_core_stage.ZoomMode.FitWidth }, fit, vp(), pad()),
|
|
1338
|
-
Math.min(auto * 2.5, _embedpdf_core_stage.ZOOM_MAX)
|
|
1339
|
-
].sort((a, b) => a - b).filter((z, i, all) => i === 0 || z > all[i - 1] * 1.1);
|
|
1340
|
-
const near = (z, stop) => z > stop / 1.1 && z < stop * 1.1;
|
|
1341
|
-
const onRung = stops.findIndex((s) => near(cam().zoom, s));
|
|
1342
|
-
const target = onRung >= 0 ? stops[(onRung + 1) % stops.length] : stops[0];
|
|
1343
|
-
ctx.dispatch({
|
|
1344
|
-
type: "PATCH",
|
|
1345
|
-
patch: { zoom: { level: target } }
|
|
1346
|
-
});
|
|
1347
|
-
const sc = buildScene();
|
|
1348
|
-
if (!sc.itemCount) return;
|
|
1349
|
-
const focal = _embedpdf_core_stage.anchorAtPoint(sc, _embedpdf_core_stage.toWorld(cam(), pt));
|
|
1350
|
-
animateZoomAnchored(focal, pt, target, 240, syncCursorFromCamera);
|
|
1351
|
-
},
|
|
1352
|
-
zoomIn: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1.2),
|
|
1353
|
-
zoomOut: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1 / 1.2),
|
|
1354
|
-
zoomTo: (spec) => api.update({ zoom: spec }),
|
|
1355
|
-
fitWidth: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitWidth } }),
|
|
1356
|
-
fitPage: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitPage } }),
|
|
1357
|
-
fitAll: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitAll } }),
|
|
1358
|
-
automatic: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.Automatic } }),
|
|
1359
|
-
refit: () => {
|
|
1360
|
-
if (!placementStarted) return;
|
|
1361
|
-
cancelAnim();
|
|
1362
|
-
reapply(currentAnchor());
|
|
1363
|
-
},
|
|
1364
|
-
goToPage: (pageIndex, opts) => goToTarget(pageIndex, opts),
|
|
1365
|
-
reveal: (pageIndex, opts) => {
|
|
1366
|
-
markCause("programmatic");
|
|
1367
|
-
const doc = ctx.document();
|
|
1368
|
-
if (!doc || doc.pageCount === 0) return;
|
|
1369
|
-
const target = Math.max(0, Math.min(pageIndex, doc.pageCount - 1));
|
|
1370
|
-
if (!(!!opts && (opts.rect != null || opts.anchor !== void 0 || opts.zoom !== void 0 && opts.zoom !== "keep"))) {
|
|
1371
|
-
if (paged()) {
|
|
1372
|
-
goToTarget(target, opts);
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
const sc = buildScene();
|
|
1376
|
-
if (!sc.itemCount) return;
|
|
1377
|
-
const page = pageRectOf(sc.items[itemIndexOfPage(target)], target);
|
|
1378
|
-
const m = worldPageFrame();
|
|
1379
|
-
const box = {
|
|
1380
|
-
x: page.x - m.left,
|
|
1381
|
-
y: page.y - m.top,
|
|
1382
|
-
width: page.width + m.left + m.right,
|
|
1383
|
-
height: page.height + m.top + m.bottom
|
|
1384
|
-
};
|
|
1385
|
-
const camera = _embedpdf_core_stage.revealCamera(cam(), box, vp(), pad());
|
|
1386
|
-
const current = cam();
|
|
1387
|
-
if (camera.x === current.x && camera.y === current.y) return;
|
|
1388
|
-
cancelAnim();
|
|
1389
|
-
if ((opts?.behavior ?? ctx.getState().scrollBehavior) === "smooth") animateTo(camera, sceneRect());
|
|
1390
|
-
else setCam(camera, sceneRect());
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
cancelAnim();
|
|
1394
|
-
if (target !== ctx.getState().cursor) ctx.dispatch({
|
|
1395
|
-
type: "CURSOR",
|
|
1396
|
-
cursor: target
|
|
1397
|
-
});
|
|
1398
|
-
const place = () => {
|
|
1399
|
-
const sc = buildScene();
|
|
1400
|
-
if (!sc.itemCount) return null;
|
|
1401
|
-
const item = paged() ? sc.items[0] : sc.items[itemIndexOfPage(target)];
|
|
1402
|
-
const world = opts.rect ? worldRectForContent(item, target, opts.rect) : pageRectOf(item, target);
|
|
1403
|
-
const zd = opts.zoom ?? "keep";
|
|
1404
|
-
const availW = Math.max(1, vp().width - 2 * pad());
|
|
1405
|
-
const availH = Math.max(1, vp().height - 2 * pad());
|
|
1406
|
-
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;
|
|
1407
|
-
if (!Number.isFinite(zoom) || zoom <= 0) zoom = cam().zoom;
|
|
1408
|
-
const zoomChanged = zd !== "keep";
|
|
1409
|
-
const a = opts.anchor ?? {};
|
|
1410
|
-
return {
|
|
1411
|
-
camera: {
|
|
1412
|
-
x: revealAxis(a.x, cam().x, world.x, world.width, vp().width, zoom, zoomChanged),
|
|
1413
|
-
y: revealAxis(a.y, cam().y, world.y, world.height, vp().height, zoom, zoomChanged),
|
|
1414
|
-
zoom
|
|
1415
|
-
},
|
|
1416
|
-
bounds: boundsFor(item),
|
|
1417
|
-
zoomChanged
|
|
1418
|
-
};
|
|
1419
|
-
};
|
|
1420
|
-
const first = place();
|
|
1421
|
-
if (!first) return;
|
|
1422
|
-
if (first.zoomChanged) ctx.dispatch({
|
|
1423
|
-
type: "PATCH",
|
|
1424
|
-
patch: { zoom: { level: first.camera.zoom } }
|
|
1425
|
-
});
|
|
1426
|
-
if ((opts.behavior ?? ctx.getState().scrollBehavior) === "smooth") {
|
|
1427
|
-
const p = place() ?? first;
|
|
1428
|
-
animateTo(p.camera, p.bounds);
|
|
1429
|
-
} else stabilized(() => {
|
|
1430
|
-
const p = place();
|
|
1431
|
-
if (p) setCam(p.camera, p.bounds);
|
|
1432
|
-
});
|
|
1433
|
-
},
|
|
1434
|
-
next: (opts) => step(1, opts),
|
|
1435
|
-
prev: (opts) => step(-1, opts),
|
|
1436
|
-
lensId: () => ctx.id,
|
|
1437
|
-
update: (patch) => {
|
|
1438
|
-
base = mergeSettings(base, patch);
|
|
1439
|
-
syncResponsive(true);
|
|
1440
|
-
},
|
|
1441
|
-
setResponsive: (next) => {
|
|
1442
|
-
rules = next;
|
|
1443
|
-
syncResponsive(true);
|
|
1444
|
-
},
|
|
1445
|
-
matches: (name) => ctx.getState().activeRules.includes(name),
|
|
1446
|
-
activeRules: () => ctx.getState().activeRules,
|
|
1447
|
-
setFlow: (flow) => api.update({ flow }),
|
|
1448
|
-
setLayout: (layout) => api.update({ layout }),
|
|
1449
|
-
setSpread: (spread) => api.update({ spread }),
|
|
1450
|
-
setSizing: (sizing) => api.update({ sizing }),
|
|
1451
|
-
setColumns: (columns) => api.update({ columns }),
|
|
1452
|
-
setBounded: (bounded) => api.update({ bounded }),
|
|
1453
|
-
setPadding: (padding) => api.update({ padding }),
|
|
1454
|
-
setGap: (gap) => api.update({ gap }),
|
|
1455
|
-
setPageFrame: (pageFrame) => api.update({ pageFrame }),
|
|
1456
|
-
setFitAlign: (fitAlign) => api.update({ fitAlign }),
|
|
1457
|
-
setArrivalAlign: (arrivalAlign) => api.update({ arrivalAlign }),
|
|
1458
|
-
setZoomAlign: (zoomAlign) => api.update({ zoomAlign }),
|
|
1459
|
-
setAnchorAlign: (anchorAlign) => api.update({ anchorAlign }),
|
|
1460
|
-
setDirection: (direction) => api.update({ direction }),
|
|
1461
|
-
setViewRotation: (viewRotation) => api.update({ viewRotation }),
|
|
1462
|
-
rotateView: (delta) => api.setViewRotation((0, _embedpdf_core_geometry.addRotations)(ctx.getState().viewRotation, delta === 90 ? 90 : 270)),
|
|
1463
|
-
setScrollBehavior: (behavior) => api.update({ scrollBehavior: behavior }),
|
|
1464
|
-
applyViewState: (view) => {
|
|
1465
|
-
cancelAnim();
|
|
1466
|
-
base = mergeSettings(base, pickSettings(view));
|
|
1467
|
-
const { effective, active } = resolveResponsive(base, rules, boxOf(vp()));
|
|
1468
|
-
lastEffective = effective;
|
|
1469
|
-
publishActive(active);
|
|
1470
|
-
ctx.dispatch({
|
|
1471
|
-
type: "PATCH",
|
|
1472
|
-
patch: effective
|
|
1473
|
-
});
|
|
1474
|
-
ctx.dispatch({
|
|
1475
|
-
type: "CURSOR",
|
|
1476
|
-
cursor: view.cursor ?? 0
|
|
1477
|
-
});
|
|
1478
|
-
sceneCache = null;
|
|
1479
|
-
applyAnchor(view.anchor);
|
|
1480
|
-
},
|
|
1481
|
-
provideInitialView: (priority, fn) => {
|
|
1482
|
-
initialViewProviders.push({
|
|
1483
|
-
priority,
|
|
1484
|
-
fn
|
|
1485
|
-
});
|
|
1486
|
-
},
|
|
1487
|
-
placeInitial: () => {
|
|
1488
|
-
placementStarted = true;
|
|
1489
|
-
const sorted = [...initialViewProviders].sort((a, b) => b.priority - a.priority);
|
|
1490
|
-
for (const p of sorted) {
|
|
1491
|
-
const view = p.fn();
|
|
1492
|
-
if (view) {
|
|
1493
|
-
api.applyViewState(view);
|
|
1494
|
-
ctx.dispatch({ type: "PLACED" });
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
api.resetView();
|
|
1499
|
-
ctx.dispatch({ type: "PLACED" });
|
|
1500
|
-
},
|
|
1501
|
-
resetView: () => goToTarget(0, { behavior: "instant" })
|
|
1486
|
+
}
|
|
1502
1487
|
};
|
|
1503
|
-
return api;
|
|
1504
1488
|
}
|
|
1505
1489
|
//#endregion
|
|
1506
|
-
//#region src/
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
const stage = ctx.get(token);
|
|
1534
|
-
return {
|
|
1535
|
-
currentPon: stage.pages()[state.cursor]?.pon ?? null,
|
|
1536
|
-
visiblePons: stage.visiblePages().map((page) => page.pon),
|
|
1537
|
-
cause: state.motionCause
|
|
1538
|
-
};
|
|
1539
|
-
};
|
|
1540
|
-
ctx.watch(() => {
|
|
1541
|
-
const s = snapshot();
|
|
1542
|
-
return s === null ? "unplaced" : `${s.currentPon ?? -1}|${[...s.visiblePons].sort((a, b) => a - b).join(",")}`;
|
|
1543
|
-
}, () => {
|
|
1544
|
-
const s = snapshot();
|
|
1545
|
-
if (s === null) return;
|
|
1546
|
-
actions.reportPageState({
|
|
1547
|
-
currentPon: s.currentPon,
|
|
1548
|
-
visiblePons: s.visiblePons,
|
|
1549
|
-
placed: true,
|
|
1550
|
-
cause: s.cause
|
|
1490
|
+
//#region src/services/events.ts
|
|
1491
|
+
function createEvents(ctx) {
|
|
1492
|
+
const pageChanged = ctx.events.source();
|
|
1493
|
+
const zoomChanged = ctx.events.source();
|
|
1494
|
+
const cameraChanged = ctx.events.source();
|
|
1495
|
+
const motionEnded = ctx.events.source();
|
|
1496
|
+
const settingsChanged = ctx.events.source();
|
|
1497
|
+
const viewportChanged = ctx.events.source();
|
|
1498
|
+
ctx.state.onChange(({ previous, next }) => {
|
|
1499
|
+
if (previous.camera !== next.camera) {
|
|
1500
|
+
cameraChanged.emit({ camera: next.camera });
|
|
1501
|
+
if (previous.camera.zoom !== next.camera.zoom) zoomChanged.emit({
|
|
1502
|
+
level: next.camera.zoom,
|
|
1503
|
+
previousLevel: previous.camera.zoom,
|
|
1504
|
+
mode: "mode" in next.zoom ? next.zoom.mode : "custom"
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
if (previous.cursor !== next.cursor) pageChanged.emit({
|
|
1508
|
+
page: ctx.document()?.pages[next.cursor]?.ref ?? null,
|
|
1509
|
+
pageIndex: next.cursor,
|
|
1510
|
+
previousPageIndex: previous.cursor
|
|
1511
|
+
});
|
|
1512
|
+
if (previous.viewport !== next.viewport) viewportChanged.emit({ size: next.viewport });
|
|
1513
|
+
const changed = SETTING_KEYS.filter((key) => !eqSetting(previous[key], next[key]));
|
|
1514
|
+
if (changed.length) settingsChanged.emit({
|
|
1515
|
+
settings: pickSettings(next),
|
|
1516
|
+
changed
|
|
1551
1517
|
});
|
|
1552
1518
|
});
|
|
1553
|
-
}
|
|
1554
|
-
//#endregion
|
|
1555
|
-
//#region src/reducer.ts
|
|
1556
|
-
const initialStageState = (config) => {
|
|
1557
|
-
const { scheduler: _scheduler, responsive: _responsive, ...overrides } = config;
|
|
1558
1519
|
return {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
cameraResting: true,
|
|
1566
|
-
vp: {
|
|
1567
|
-
width: 0,
|
|
1568
|
-
height: 0
|
|
1569
|
-
},
|
|
1570
|
-
dpr: 1,
|
|
1571
|
-
cursor: 0,
|
|
1572
|
-
motionCause: "user",
|
|
1573
|
-
activeRules: [],
|
|
1574
|
-
...DEFAULT_SETTINGS,
|
|
1575
|
-
...overrides
|
|
1520
|
+
pageChanged,
|
|
1521
|
+
zoomChanged,
|
|
1522
|
+
cameraChanged,
|
|
1523
|
+
motionEnded,
|
|
1524
|
+
settingsChanged,
|
|
1525
|
+
viewportChanged
|
|
1576
1526
|
};
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
const next = { ...state };
|
|
1581
|
-
let key;
|
|
1582
|
-
for (key in patch) {
|
|
1583
|
-
const value = patch[key];
|
|
1584
|
-
if (value !== void 0) Object.assign(next, { [key]: value });
|
|
1585
|
-
}
|
|
1586
|
-
return next;
|
|
1587
|
-
};
|
|
1527
|
+
}
|
|
1528
|
+
//#endregion
|
|
1529
|
+
//#region src/services/scene.ts
|
|
1588
1530
|
/**
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1531
|
+
* The scene model: the document's item grouping, the layout, the memoized
|
|
1532
|
+
* scene (continuous flow lays out the whole document; paged flow a one-item
|
|
1533
|
+
* slice at the origin), and the pure geometry reads every area builds on:
|
|
1534
|
+
* fit, alignment points, clamp bounds and anchors. Reads only; nothing here
|
|
1535
|
+
* changes state.
|
|
1592
1536
|
*/
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1537
|
+
function createScene(ctx) {
|
|
1538
|
+
const state = () => ctx.state.get();
|
|
1539
|
+
const camera = () => state().camera;
|
|
1540
|
+
const viewport = () => state().viewport;
|
|
1541
|
+
const dpr = () => state().dpr;
|
|
1542
|
+
const padding = () => state().padding;
|
|
1543
|
+
const paged = () => state().flow === "paged";
|
|
1544
|
+
const isFitAll = () => {
|
|
1545
|
+
const zoom = state().zoom;
|
|
1546
|
+
return "mode" in zoom && zoom.mode === _embedpdf_core_stage.ZoomMode.FitAll;
|
|
1547
|
+
};
|
|
1548
|
+
const grouping = (0, _embedpdf_core.memo)(() => [ctx.document()?.pageCount ?? 0, state().spread], (pageCount, spread) => {
|
|
1549
|
+
const groups = (0, _embedpdf_core_stage.groupPages)(pageCount, spread);
|
|
1550
|
+
return {
|
|
1551
|
+
grouping: groups,
|
|
1552
|
+
firstPages: groups.map((item) => item[0])
|
|
1598
1553
|
};
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1554
|
+
});
|
|
1555
|
+
const itemCountFull = () => grouping().grouping.length;
|
|
1556
|
+
const itemIndexOfPage = (pageIndex) => {
|
|
1557
|
+
const firstPages = grouping().firstPages;
|
|
1558
|
+
let low = 0;
|
|
1559
|
+
let high = firstPages.length;
|
|
1560
|
+
while (low < high) {
|
|
1561
|
+
const middle = low + high >> 1;
|
|
1562
|
+
if (firstPages[middle] <= pageIndex) low = middle + 1;
|
|
1563
|
+
else high = middle;
|
|
1564
|
+
}
|
|
1565
|
+
return Math.max(0, low - 1);
|
|
1566
|
+
};
|
|
1567
|
+
const effectiveZoom = () => {
|
|
1568
|
+
const zoom = state().zoom;
|
|
1569
|
+
return "level" in zoom ? zoom.level : Math.max(state().camera.zoom, 1e-4);
|
|
1570
|
+
};
|
|
1571
|
+
const wrapLineWidth = () => Math.max(1, (viewport().width - 2 * padding()) / effectiveZoom());
|
|
1572
|
+
const worldPageFrame = () => {
|
|
1573
|
+
const frame = state().pageFrame;
|
|
1574
|
+
if (!frame.top && !frame.right && !frame.bottom && !frame.left) return frame;
|
|
1575
|
+
const zoom = effectiveZoom();
|
|
1576
|
+
return {
|
|
1577
|
+
top: frame.top / zoom,
|
|
1578
|
+
right: frame.right / zoom,
|
|
1579
|
+
bottom: frame.bottom / zoom,
|
|
1580
|
+
left: frame.left / zoom
|
|
1602
1581
|
};
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1582
|
+
};
|
|
1583
|
+
const frameKey = () => {
|
|
1584
|
+
const frame = state().pageFrame;
|
|
1585
|
+
if (!frame.top && !frame.right && !frame.bottom && !frame.left) return "-";
|
|
1586
|
+
const world = worldPageFrame();
|
|
1587
|
+
return `${Math.round(world.top)},${Math.round(world.right)},${Math.round(world.bottom)},${Math.round(world.left)}`;
|
|
1588
|
+
};
|
|
1589
|
+
const worldGap = () => {
|
|
1590
|
+
const gap = state().gap;
|
|
1591
|
+
return typeof gap === "number" ? gap : gap.px ? gap.px / effectiveZoom() : 0;
|
|
1592
|
+
};
|
|
1593
|
+
const gapKey = () => {
|
|
1594
|
+
const gap = state().gap;
|
|
1595
|
+
return typeof gap === "number" ? String(gap) : `px:${Math.round(worldGap())}`;
|
|
1596
|
+
};
|
|
1597
|
+
const layoutFor = (groups) => {
|
|
1598
|
+
const settings = state();
|
|
1599
|
+
const registryPages = ctx.document()?.pages ?? [];
|
|
1600
|
+
const viewRotation = settings.viewRotation;
|
|
1601
|
+
const pages = viewRotation === 0 ? registryPages : registryPages.map((page) => ({
|
|
1602
|
+
...page,
|
|
1603
|
+
rotation: (0, _embedpdf_core_geometry.addRotations)(page.rotation, viewRotation)
|
|
1604
|
+
}));
|
|
1605
|
+
const pageFrame = worldPageFrame();
|
|
1606
|
+
const gap = worldGap();
|
|
1607
|
+
const viewUnitsPerPoint = settings.viewUnitsPerPoint;
|
|
1608
|
+
if (settings.layout === "grid") return (0, _embedpdf_core_stage.gridLayout)(pages, groups, {
|
|
1609
|
+
gap,
|
|
1610
|
+
sizing: settings.sizing,
|
|
1611
|
+
direction: settings.direction,
|
|
1612
|
+
pageFrame,
|
|
1613
|
+
viewUnitsPerPoint,
|
|
1614
|
+
columns: typeof settings.columns === "number" ? settings.columns : void 0,
|
|
1615
|
+
lineWidth: settings.columns === "auto" ? wrapLineWidth() : void 0
|
|
1616
|
+
});
|
|
1617
|
+
return (0, _embedpdf_core_stage.linearLayout)(pages, groups, {
|
|
1618
|
+
axis: settings.layout === "horizontal" ? "x" : "y",
|
|
1619
|
+
gap,
|
|
1620
|
+
sizing: settings.sizing,
|
|
1621
|
+
direction: settings.direction,
|
|
1622
|
+
pageFrame,
|
|
1623
|
+
viewUnitsPerPoint
|
|
1624
|
+
});
|
|
1625
|
+
};
|
|
1626
|
+
const columnsKey = () => {
|
|
1627
|
+
const settings = state();
|
|
1628
|
+
if (settings.layout !== "grid") return "-";
|
|
1629
|
+
return settings.columns === "auto" ? `auto:${Math.round(wrapLineWidth())}` : String(settings.columns);
|
|
1630
|
+
};
|
|
1631
|
+
const SCENE_KEY_FUNCTIONS = {
|
|
1632
|
+
columns: columnsKey,
|
|
1633
|
+
gap: gapKey,
|
|
1634
|
+
pageFrame: frameKey
|
|
1635
|
+
};
|
|
1636
|
+
const SCENE_KEYS = SETTING_KEYS.filter((key) => SETTINGS_EFFECT[key] === "scene");
|
|
1637
|
+
const settingsKey = () => SCENE_KEYS.map((key) => {
|
|
1638
|
+
const keyFunction = SCENE_KEY_FUNCTIONS[key];
|
|
1639
|
+
if (keyFunction) return keyFunction();
|
|
1640
|
+
const value = state()[key];
|
|
1641
|
+
return typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
1642
|
+
}).join("|");
|
|
1643
|
+
let sceneCache = null;
|
|
1644
|
+
const documentKey = () => {
|
|
1645
|
+
const document = ctx.document();
|
|
1646
|
+
return document ? `${document.pageCount}.${document.revision}` : "0.0";
|
|
1647
|
+
};
|
|
1648
|
+
const buildScene = () => {
|
|
1649
|
+
const { grouping: groups } = grouping();
|
|
1650
|
+
if (state().flow === "paged") {
|
|
1651
|
+
const index = groups.length ? Math.min(itemIndexOfPage(state().cursor), groups.length - 1) : 0;
|
|
1652
|
+
const key = `paged|${documentKey()}|${settingsKey()}|${index}`;
|
|
1653
|
+
if (sceneCache && sceneCache.key === key) return sceneCache.scene;
|
|
1654
|
+
const scene = layoutFor(groups.length ? [groups[index]] : []);
|
|
1655
|
+
sceneCache = {
|
|
1656
|
+
key,
|
|
1657
|
+
scene
|
|
1658
|
+
};
|
|
1659
|
+
return scene;
|
|
1660
|
+
}
|
|
1661
|
+
const key = `continuous|${documentKey()}|${settingsKey()}`;
|
|
1662
|
+
if (sceneCache && sceneCache.key === key) return sceneCache.scene;
|
|
1663
|
+
const scene = layoutFor(groups);
|
|
1664
|
+
sceneCache = {
|
|
1665
|
+
key,
|
|
1666
|
+
scene
|
|
1606
1667
|
};
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1668
|
+
return scene;
|
|
1669
|
+
};
|
|
1670
|
+
/** Drop the memoized scene (a 'scene' or 'reflow' setting changed, or a view was restored). */
|
|
1671
|
+
const invalidate = () => {
|
|
1672
|
+
sceneCache = null;
|
|
1673
|
+
};
|
|
1674
|
+
/** The current scene's cache key, part of the visible-pages signature. */
|
|
1675
|
+
const cacheKey = () => sceneCache.key;
|
|
1676
|
+
const sceneRect = () => {
|
|
1677
|
+
const { width, height } = buildScene().size;
|
|
1678
|
+
return {
|
|
1679
|
+
x: 0,
|
|
1680
|
+
y: 0,
|
|
1681
|
+
width,
|
|
1682
|
+
height
|
|
1610
1683
|
};
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1684
|
+
};
|
|
1685
|
+
const itemRect = (item) => ({
|
|
1686
|
+
x: item.x,
|
|
1687
|
+
y: item.y,
|
|
1688
|
+
width: item.width,
|
|
1689
|
+
height: item.height
|
|
1690
|
+
});
|
|
1691
|
+
const pageRectOf = (item, pageIndex) => {
|
|
1692
|
+
const box = item.pages.find((page) => page.pageIndex === pageIndex) ?? item.pages[0];
|
|
1693
|
+
return {
|
|
1694
|
+
x: box.x,
|
|
1695
|
+
y: box.y,
|
|
1696
|
+
width: box.width,
|
|
1697
|
+
height: box.height
|
|
1614
1698
|
};
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1699
|
+
};
|
|
1700
|
+
/** The item shown for the cursor: the slice's only item (paged) or the full-scene item. */
|
|
1701
|
+
const cursorItem = () => {
|
|
1702
|
+
const scene = buildScene();
|
|
1703
|
+
return paged() ? scene.items[0] : scene.items[itemIndexOfPage(state().cursor)];
|
|
1704
|
+
};
|
|
1705
|
+
/**
|
|
1706
|
+
* A page-space rect on a page → a world rect: the same quarter-turn
|
|
1707
|
+
* matrix `pageRectToViewport` uses, minus the camera, so a positioned
|
|
1708
|
+
* reveal and the rendered overlay never disagree about where a rect is.
|
|
1709
|
+
*/
|
|
1710
|
+
const worldRectForContent = (item, pageIndex, rect) => {
|
|
1711
|
+
const box = item.pages.find((page) => page.pageIndex === pageIndex) ?? item.pages[0];
|
|
1712
|
+
const content = (0, _embedpdf_core_geometry.displaySize)({
|
|
1713
|
+
width: box.width,
|
|
1714
|
+
height: box.height
|
|
1715
|
+
}, box.rotation);
|
|
1716
|
+
const world = (0, _embedpdf_core_geometry.applyRect)((0, _embedpdf_core_geometry.rotateScaleMatrix)(box.contentScale, content.width, content.height, box.rotation), rect);
|
|
1717
|
+
return {
|
|
1718
|
+
x: box.x + world.x,
|
|
1719
|
+
y: box.y + world.y,
|
|
1720
|
+
width: world.width,
|
|
1721
|
+
height: world.height
|
|
1618
1722
|
};
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1723
|
+
};
|
|
1724
|
+
const fits = (rect, zoom) => {
|
|
1725
|
+
const size = viewport();
|
|
1726
|
+
const inset = padding();
|
|
1727
|
+
const tolerance = .5;
|
|
1728
|
+
return rect.width * zoom <= size.width - 2 * inset + tolerance && rect.height * zoom <= size.height - 2 * inset + tolerance;
|
|
1729
|
+
};
|
|
1730
|
+
const alignFraction = (value) => value === "start" ? 0 : value === "center" ? .5 : value === "end" ? 1 : Math.min(1, Math.max(0, value));
|
|
1731
|
+
const alignPoint = (alignment, size) => {
|
|
1732
|
+
const rtl = state().direction === "rtl";
|
|
1733
|
+
const alignX = rtl && alignment.x === "start" ? "end" : rtl && alignment.x === "end" ? "start" : alignment.x;
|
|
1734
|
+
const inset = padding();
|
|
1735
|
+
return {
|
|
1736
|
+
x: inset + (size.width - 2 * inset) * alignFraction(alignX),
|
|
1737
|
+
y: inset + (size.height - 2 * inset) * alignFraction(alignment.y)
|
|
1622
1738
|
};
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1739
|
+
};
|
|
1740
|
+
const anchorPoint = () => alignPoint(state().anchorAlign, viewport());
|
|
1741
|
+
/** The box the zoom intent fits: the whole scene (fit-all), the current
|
|
1742
|
+
* item (paged: per-page fit), or the document maximum (continuous: a
|
|
1743
|
+
* document-stable zoom). */
|
|
1744
|
+
const fitBox = (item) => {
|
|
1745
|
+
if (isFitAll()) return buildScene().size;
|
|
1746
|
+
return paged() ? {
|
|
1747
|
+
width: item.width,
|
|
1748
|
+
height: item.height
|
|
1749
|
+
} : buildScene().maxItemSize;
|
|
1750
|
+
};
|
|
1751
|
+
/** The clamp rect for a camera write targeting this item (the scene in continuous flow). */
|
|
1752
|
+
const boundsFor = (item) => paged() ? itemRect(item) : sceneRect();
|
|
1753
|
+
/** The clamp rect for a write that stays put (pan, zoom): the slice item (paged) or the scene. */
|
|
1754
|
+
const stayBounds = () => paged() ? itemRect(buildScene().items[0]) : sceneRect();
|
|
1755
|
+
const constraint = () => ({
|
|
1756
|
+
bounded: state().bounded,
|
|
1757
|
+
padding: padding(),
|
|
1758
|
+
fitAlign: state().fitAlign,
|
|
1759
|
+
direction: state().direction
|
|
1760
|
+
});
|
|
1761
|
+
const anchorAt = (at) => {
|
|
1762
|
+
const scene = buildScene();
|
|
1763
|
+
return scene.itemCount ? (0, _embedpdf_core_stage.anchorFromCamera)(camera(), scene, viewport(), at) : {
|
|
1764
|
+
pageIndex: state().cursor,
|
|
1765
|
+
fx: .5,
|
|
1766
|
+
fy: 0
|
|
1627
1767
|
};
|
|
1628
|
-
|
|
1629
|
-
|
|
1768
|
+
};
|
|
1769
|
+
const currentAnchor = () => anchorAt(anchorPoint());
|
|
1770
|
+
const indexOfPage = (page) => ctx.getPage(page)?.index ?? -1;
|
|
1771
|
+
/**
|
|
1772
|
+
* A page argument (a `PageRef` or a display index) as a display index, or
|
|
1773
|
+
* null when it names no page: a ref not in the document, or a number that
|
|
1774
|
+
* isn't a whole number. An index past either end is kept; arrivals clamp it.
|
|
1775
|
+
*/
|
|
1776
|
+
const indexOfTarget = (page) => {
|
|
1777
|
+
if (typeof page === "number") return Number.isInteger(page) ? page : null;
|
|
1778
|
+
const index = indexOfPage(page);
|
|
1779
|
+
return index >= 0 ? index : null;
|
|
1780
|
+
};
|
|
1781
|
+
return {
|
|
1782
|
+
camera,
|
|
1783
|
+
viewport,
|
|
1784
|
+
dpr,
|
|
1785
|
+
padding,
|
|
1786
|
+
paged,
|
|
1787
|
+
isFitAll,
|
|
1788
|
+
grouping,
|
|
1789
|
+
itemCountFull,
|
|
1790
|
+
itemIndexOfPage,
|
|
1791
|
+
worldPageFrame,
|
|
1792
|
+
buildScene,
|
|
1793
|
+
invalidate,
|
|
1794
|
+
cacheKey,
|
|
1795
|
+
sceneRect,
|
|
1796
|
+
itemRect,
|
|
1797
|
+
pageRectOf,
|
|
1798
|
+
cursorItem,
|
|
1799
|
+
worldRectForContent,
|
|
1800
|
+
fits,
|
|
1801
|
+
alignPoint,
|
|
1802
|
+
anchorPoint,
|
|
1803
|
+
fitBox,
|
|
1804
|
+
boundsFor,
|
|
1805
|
+
stayBounds,
|
|
1806
|
+
constraint,
|
|
1807
|
+
anchorAt,
|
|
1808
|
+
currentAnchor,
|
|
1809
|
+
indexOfPage,
|
|
1810
|
+
indexOfTarget
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
//#endregion
|
|
1814
|
+
//#region src/services/index.ts
|
|
1815
|
+
function createServices(ctx) {
|
|
1816
|
+
return {
|
|
1817
|
+
events: createEvents(ctx),
|
|
1818
|
+
clock: ctx.clock,
|
|
1819
|
+
placement: createPlacementLatch(),
|
|
1820
|
+
scene: createScene(ctx)
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
//#endregion
|
|
1824
|
+
//#region src/settings/settings.ts
|
|
1825
|
+
/**
|
|
1826
|
+
* The settings bag and its responsive resolution. The base (config plus
|
|
1827
|
+
* `updateSettings()` changes) merged with the matching rules' patches is the effective
|
|
1828
|
+
* settings the state holds. `applySettings` is the one reactive applier: the
|
|
1829
|
+
* strongest effect among the touched settings wins ('reflow' ⊃ 'scene'/'refit'
|
|
1830
|
+
* ⊃ 'reclamp' ⊃ 'none'), so a breakpoint crossing gets exactly the invariant a
|
|
1831
|
+
* hand-written update would.
|
|
1832
|
+
*/
|
|
1833
|
+
const EFFECT_RANK = {
|
|
1834
|
+
none: 0,
|
|
1835
|
+
reclamp: 1,
|
|
1836
|
+
refit: 2,
|
|
1837
|
+
scene: 3,
|
|
1838
|
+
reflow: 4
|
|
1630
1839
|
};
|
|
1840
|
+
function createSettings(ctx, { scene }, config, { writeCamera }, { cancelAnimation }, { applyAnchor, reapply, goToTarget }) {
|
|
1841
|
+
const { camera, viewport, alignPoint, anchorAt } = scene;
|
|
1842
|
+
const state = () => ctx.state.get();
|
|
1843
|
+
const snapshotSettings = (0, _embedpdf_core.memo)(() => SETTING_KEYS.map((key) => state()[key]), () => pickSettings(state()));
|
|
1844
|
+
/**
|
|
1845
|
+
* React to a settings change per the registry: the strongest effect among
|
|
1846
|
+
* the touched settings wins. Both the public `updateSettings()` and the
|
|
1847
|
+
* responsive driver land here.
|
|
1848
|
+
*/
|
|
1849
|
+
const applySettings = (patch) => {
|
|
1850
|
+
cancelAnimation();
|
|
1851
|
+
const touched = (effect) => SETTING_KEYS.some((key) => patch[key] !== void 0 && SETTINGS_EFFECT[key] === effect);
|
|
1852
|
+
const align = touched("refit") && !touched("scene") && !touched("reflow") ? state().zoomAlign : state().anchorAlign;
|
|
1853
|
+
const anchor = anchorAt(alignPoint(align, viewport()));
|
|
1854
|
+
ctx.state.update(patchSettings, patch);
|
|
1855
|
+
if (touched("scene")) scene.invalidate();
|
|
1856
|
+
if (touched("reflow")) goToTarget(state().cursor, { behavior: "instant" });
|
|
1857
|
+
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
1858
|
+
else if (touched("reclamp")) writeCamera(camera());
|
|
1859
|
+
};
|
|
1860
|
+
let base = snapshotSettings();
|
|
1861
|
+
const initialBase = base;
|
|
1862
|
+
let rules = config.responsive ?? DEFAULT_RESPONSIVE;
|
|
1863
|
+
let lastEffective = base;
|
|
1864
|
+
const publishActive = (active) => {
|
|
1865
|
+
const current = state().activeRules;
|
|
1866
|
+
if (current.length !== active.length || active.some((name, index) => name !== current[index])) ctx.state.update(setActiveRules, active);
|
|
1867
|
+
};
|
|
1868
|
+
/**
|
|
1869
|
+
* Re-resolve and apply what changed since the last resolution. `react:
|
|
1870
|
+
* true` routes the patch through `applySettings` (the full
|
|
1871
|
+
* anchor-preserving update); `react: false` (inside `setViewportSize`,
|
|
1872
|
+
* whose caller runs its own reframe) only lands the patch and the cache
|
|
1873
|
+
* invalidation and reports the strongest effect, so the caller can
|
|
1874
|
+
* escalate a rule-driven flow flip.
|
|
1875
|
+
*/
|
|
1876
|
+
const syncResponsive = (react) => {
|
|
1877
|
+
const { effective, active } = resolveResponsive(base, rules, boxOf(viewport()));
|
|
1878
|
+
const patch = {};
|
|
1879
|
+
for (const key of SETTING_KEYS) if (!eqSetting(effective[key], lastEffective[key])) Object.assign(patch, { [key]: effective[key] });
|
|
1880
|
+
lastEffective = effective;
|
|
1881
|
+
publishActive(active);
|
|
1882
|
+
const keys = Object.keys(patch);
|
|
1883
|
+
if (keys.length === 0) return "none";
|
|
1884
|
+
if (react) {
|
|
1885
|
+
applySettings(patch);
|
|
1886
|
+
return "none";
|
|
1887
|
+
}
|
|
1888
|
+
ctx.state.update(patchSettings, patch);
|
|
1889
|
+
let strongest = "none";
|
|
1890
|
+
for (const key of keys) if (EFFECT_RANK[SETTINGS_EFFECT[key]] > EFFECT_RANK[strongest]) strongest = SETTINGS_EFFECT[key];
|
|
1891
|
+
if (strongest === "scene" || strongest === "reflow") scene.invalidate();
|
|
1892
|
+
return strongest;
|
|
1893
|
+
};
|
|
1894
|
+
const updateSettings = (changes) => {
|
|
1895
|
+
base = mergeSettings(base, changes);
|
|
1896
|
+
syncResponsive(true);
|
|
1897
|
+
};
|
|
1898
|
+
const applyViewState = (view) => {
|
|
1899
|
+
cancelAnimation();
|
|
1900
|
+
base = mergeSettings(base, pickSettings(view));
|
|
1901
|
+
const { effective, active } = resolveResponsive(base, rules, boxOf(viewport()));
|
|
1902
|
+
lastEffective = effective;
|
|
1903
|
+
publishActive(active);
|
|
1904
|
+
ctx.state.update(patchSettings, effective);
|
|
1905
|
+
ctx.state.update(setCursor, view.cursor ?? 0);
|
|
1906
|
+
scene.invalidate();
|
|
1907
|
+
applyAnchor(view.anchor);
|
|
1908
|
+
};
|
|
1909
|
+
return {
|
|
1910
|
+
snapshotSettings,
|
|
1911
|
+
syncResponsive,
|
|
1912
|
+
updateSettings,
|
|
1913
|
+
applyViewState,
|
|
1914
|
+
api: {
|
|
1915
|
+
getSettings: snapshotSettings,
|
|
1916
|
+
resetSettings: () => {
|
|
1917
|
+
base = mergeSettings(initialBase, {});
|
|
1918
|
+
syncResponsive(true);
|
|
1919
|
+
},
|
|
1920
|
+
updateSettings,
|
|
1921
|
+
setResponsiveRules: (next) => {
|
|
1922
|
+
rules = next;
|
|
1923
|
+
syncResponsive(true);
|
|
1924
|
+
},
|
|
1925
|
+
matchesRule: (name) => state().activeRules.includes(name),
|
|
1926
|
+
listActiveRules: () => state().activeRules,
|
|
1927
|
+
applyViewState
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
//#endregion
|
|
1932
|
+
//#region src/view/lifecycle.ts
|
|
1933
|
+
function createViewLifecycle(ctx, { scene, placement }, { cancelAnimation }, arrive, settings) {
|
|
1934
|
+
const { dpr, currentAnchor } = scene;
|
|
1935
|
+
const { goToTarget, reapply } = arrive;
|
|
1936
|
+
const { snapshotSettings, syncResponsive } = settings;
|
|
1937
|
+
const state = () => ctx.state.get();
|
|
1938
|
+
const initialViewProviders = [];
|
|
1939
|
+
const placeInitial = () => {
|
|
1940
|
+
placement.started = true;
|
|
1941
|
+
const byPriority = [...initialViewProviders].sort((left, right) => right.priority - left.priority);
|
|
1942
|
+
let view = null;
|
|
1943
|
+
for (const provider of byPriority) {
|
|
1944
|
+
view = provider.provide();
|
|
1945
|
+
if (view) break;
|
|
1946
|
+
}
|
|
1947
|
+
if (view) settings.applyViewState(view);
|
|
1948
|
+
else arrive.resetView();
|
|
1949
|
+
const pending = placement.pending;
|
|
1950
|
+
placement.pending = null;
|
|
1951
|
+
pending?.();
|
|
1952
|
+
ctx.state.update(markPlaced);
|
|
1953
|
+
};
|
|
1954
|
+
return { api: {
|
|
1955
|
+
setViewportSize: (size) => {
|
|
1956
|
+
if (!placement.started) {
|
|
1957
|
+
ctx.state.update(setViewport, size);
|
|
1958
|
+
syncResponsive(false);
|
|
1959
|
+
if (size.width > 0 && size.height > 0 && (ctx.document()?.pageCount ?? 0) > 0) placeInitial();
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (size.width <= 0 || size.height <= 0) return;
|
|
1963
|
+
const current = state().viewport;
|
|
1964
|
+
if (size.width === current.width && size.height === current.height) return;
|
|
1965
|
+
cancelAnimation();
|
|
1966
|
+
const anchor = currentAnchor();
|
|
1967
|
+
ctx.state.update(setViewport, size);
|
|
1968
|
+
if (syncResponsive(false) === "reflow") goToTarget(state().cursor, { behavior: "instant" });
|
|
1969
|
+
else reapply(anchor);
|
|
1970
|
+
},
|
|
1971
|
+
setDevicePixelRatio: (ratio) => {
|
|
1972
|
+
if (ratio > 0 && ratio !== dpr()) ctx.state.update(setDpr, ratio);
|
|
1973
|
+
},
|
|
1974
|
+
refit: () => {
|
|
1975
|
+
if (!placement.started) return;
|
|
1976
|
+
cancelAnimation();
|
|
1977
|
+
reapply(currentAnchor());
|
|
1978
|
+
},
|
|
1979
|
+
getViewState: () => ({
|
|
1980
|
+
...snapshotSettings(),
|
|
1981
|
+
cursor: state().cursor,
|
|
1982
|
+
anchor: currentAnchor()
|
|
1983
|
+
}),
|
|
1984
|
+
getLensId: () => ctx.id,
|
|
1985
|
+
provideInitialView: (priority, provide) => {
|
|
1986
|
+
const entry = {
|
|
1987
|
+
priority,
|
|
1988
|
+
provide
|
|
1989
|
+
};
|
|
1990
|
+
initialViewProviders.push(entry);
|
|
1991
|
+
return () => {
|
|
1992
|
+
const index = initialViewProviders.indexOf(entry);
|
|
1993
|
+
if (index >= 0) initialViewProviders.splice(index, 1);
|
|
1994
|
+
};
|
|
1995
|
+
},
|
|
1996
|
+
placeInitial
|
|
1997
|
+
} };
|
|
1998
|
+
}
|
|
1999
|
+
//#endregion
|
|
2000
|
+
//#region src/controller.ts
|
|
2001
|
+
/**
|
|
2002
|
+
* The stage controller: the composition root. It builds the plugin's services
|
|
2003
|
+
* once (the events, the timing seam, the scene model), wires each area with
|
|
2004
|
+
* the services it declares, and assembles the host capability from the areas'
|
|
2005
|
+
* API slices. stage-core is the pure spatial math; the areas are the impure
|
|
2006
|
+
* shell that writes state, caches and drives motion, and time enters only
|
|
2007
|
+
* through the host's clock (`ctx.clock`).
|
|
2008
|
+
*
|
|
2009
|
+
* The camera model: every camera move is defined by what it holds fixed.
|
|
2010
|
+
* Gestures (pan, pinch, wheel) hold the page-point under the pointer;
|
|
2011
|
+
* navigation lands its target at the arrivalAlign point; positioned reveals
|
|
2012
|
+
* and restored viewpoints hold what the call specifies; pointer-less zooms
|
|
2013
|
+
* hold the zoomAlign point; reframes (resize, rotation, layout changes) hold
|
|
2014
|
+
* the anchorAlign point; and an axis the camera cannot travel rests at fitAlign.
|
|
2015
|
+
*/
|
|
2016
|
+
function createStageController(ctx, config = {}) {
|
|
2017
|
+
const services = createServices(ctx);
|
|
2018
|
+
const { events } = services;
|
|
2019
|
+
const reads = createPageReads(ctx, services);
|
|
2020
|
+
const write = createCameraWrite(ctx, services);
|
|
2021
|
+
const animation = createAnimation(services, write);
|
|
2022
|
+
const gestures = createGestures(ctx, services, write, animation, reads);
|
|
2023
|
+
const arrive = createArrival(ctx, services, write, animation);
|
|
2024
|
+
const reveal = createReveal(ctx, services, write, animation, arrive);
|
|
2025
|
+
const settings = createSettings(ctx, services, config, write, animation, arrive);
|
|
2026
|
+
const zoom = createZoom(ctx, services, write, animation, gestures.api, settings);
|
|
2027
|
+
const view = createViewLifecycle(ctx, services, animation, arrive, settings);
|
|
2028
|
+
const api = (0, _embedpdf_core.composeApi)("stage", [
|
|
2029
|
+
reads.api,
|
|
2030
|
+
animation.api,
|
|
2031
|
+
gestures.api,
|
|
2032
|
+
arrive.api,
|
|
2033
|
+
reveal.api,
|
|
2034
|
+
settings.api,
|
|
2035
|
+
zoom.api,
|
|
2036
|
+
view.api,
|
|
2037
|
+
{
|
|
2038
|
+
onPageChanged: events.pageChanged.on,
|
|
2039
|
+
onZoomChanged: events.zoomChanged.on,
|
|
2040
|
+
onCameraChanged: events.cameraChanged.on,
|
|
2041
|
+
onMotionEnded: events.motionEnded.on,
|
|
2042
|
+
onSettingsChanged: events.settingsChanged.on,
|
|
2043
|
+
onViewportChanged: events.viewportChanged.on
|
|
2044
|
+
}
|
|
2045
|
+
]);
|
|
2046
|
+
return {
|
|
2047
|
+
api,
|
|
2048
|
+
connect: () => connectStage(ctx, api)
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
1631
2051
|
//#endregion
|
|
1632
2052
|
//#region src/stage.plugin.ts
|
|
1633
2053
|
/**
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
2054
|
+
* The stage plugin: one lens's camera, layout, zoom and navigation over a
|
|
2055
|
+
* document. Document-scoped. The actions plugin is optional: with it, the
|
|
2056
|
+
* main lens feeds page state to it and interprets GoTo and Named page
|
|
2057
|
+
* actions. Pointer input is opted into by the surface binding
|
|
2058
|
+
* (`<Stage interaction>` / `createStageSurface`), which both forwards pointer
|
|
2059
|
+
* samples and registers this lens's scroll handler.
|
|
1637
2060
|
*/
|
|
1638
2061
|
const stagePlugin = (options = {}) => {
|
|
1639
|
-
const { id =
|
|
2062
|
+
const { id = DEFAULT_LENS_ID, token = require_contract.StageToken, ...config } = options;
|
|
1640
2063
|
return (0, _embedpdf_core.definePlugin)({
|
|
1641
2064
|
id,
|
|
1642
|
-
token,
|
|
2065
|
+
token: (0, _embedpdf_core.createHostToken)(token),
|
|
1643
2066
|
scope: "document",
|
|
1644
2067
|
optional: [_embedpdf_plugin_actions_contract.ActionsToken],
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
capability: (ctx) => createStageCapability(ctx, config),
|
|
1648
|
-
effects: (ctx) => registerStageEffects(ctx, token, id === "stage"),
|
|
1649
|
-
init: (ctx) => {
|
|
1650
|
-
if (id !== "stage") return;
|
|
1651
|
-
const actions = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
|
|
1652
|
-
if (!actions) return;
|
|
1653
|
-
ctx.cleanup(actions.registerExecutor("goto", (node) => {
|
|
1654
|
-
if (node.type !== "goto") return {
|
|
1655
|
-
status: "inert",
|
|
1656
|
-
reason: "not a goto node"
|
|
1657
|
-
};
|
|
1658
|
-
const stage = ctx.tryGet(token);
|
|
1659
|
-
const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === node.destination.pageObjectNumber);
|
|
1660
|
-
if (!stage || !layout) return {
|
|
1661
|
-
status: "failed",
|
|
1662
|
-
error: "no stage or destination page available"
|
|
1663
|
-
};
|
|
1664
|
-
const { pageIndex, options: reveal } = require_destination.destinationToReveal(node.destination, layout);
|
|
1665
|
-
stage.reveal(pageIndex, {
|
|
1666
|
-
...reveal,
|
|
1667
|
-
behavior: "smooth"
|
|
1668
|
-
});
|
|
1669
|
-
return { status: "executed" };
|
|
1670
|
-
}));
|
|
1671
|
-
ctx.cleanup(actions.registerExecutor("named", (node) => {
|
|
1672
|
-
if (node.type !== "named") return {
|
|
1673
|
-
status: "inert",
|
|
1674
|
-
reason: "not a named node"
|
|
1675
|
-
};
|
|
1676
|
-
const stage = ctx.tryGet(token);
|
|
1677
|
-
if (!stage) return {
|
|
1678
|
-
status: "failed",
|
|
1679
|
-
error: "no stage available"
|
|
1680
|
-
};
|
|
1681
|
-
switch (node.name) {
|
|
1682
|
-
case "NextPage":
|
|
1683
|
-
stage.next({ behavior: "smooth" });
|
|
1684
|
-
return { status: "executed" };
|
|
1685
|
-
case "PrevPage":
|
|
1686
|
-
stage.prev({ behavior: "smooth" });
|
|
1687
|
-
return { status: "executed" };
|
|
1688
|
-
case "FirstPage":
|
|
1689
|
-
stage.goToPage(0, { behavior: "smooth" });
|
|
1690
|
-
return { status: "executed" };
|
|
1691
|
-
case "LastPage":
|
|
1692
|
-
stage.goToPage(Math.max(0, stage.pageCount() - 1), { behavior: "smooth" });
|
|
1693
|
-
return { status: "executed" };
|
|
1694
|
-
default: return {
|
|
1695
|
-
status: "inert",
|
|
1696
|
-
reason: `unknown named action '${node.name}'`
|
|
1697
|
-
};
|
|
1698
|
-
}
|
|
1699
|
-
}));
|
|
1700
|
-
}
|
|
2068
|
+
state: () => initialStageState(config),
|
|
2069
|
+
create: (ctx) => createStageController(ctx, config)
|
|
1701
2070
|
});
|
|
1702
2071
|
};
|
|
1703
2072
|
//#endregion
|
|
2073
|
+
//#region src/state.ts
|
|
2074
|
+
/**
|
|
2075
|
+
* The Stage page's State table as code: what `useStageState()` returns, and
|
|
2076
|
+
* the same fields in every other framework. A second view registered with its
|
|
2077
|
+
* own token reads the same fields through its own capability.
|
|
2078
|
+
*/
|
|
2079
|
+
const stageState = (0, _embedpdf_core.defineState)(require_contract.StageToken, {
|
|
2080
|
+
read: (stage) => ({
|
|
2081
|
+
zoomLevel: stage.getZoomLevel(),
|
|
2082
|
+
zoomMode: stage.getZoomMode(),
|
|
2083
|
+
currentPageIndex: stage.getCurrentPageIndex(),
|
|
2084
|
+
currentPage: stage.getCurrentPage(),
|
|
2085
|
+
pageCount: stage.getPageCount(),
|
|
2086
|
+
viewRotation: stage.getViewRotation(),
|
|
2087
|
+
activeRules: stage.listActiveRules()
|
|
2088
|
+
}),
|
|
2089
|
+
empty: {
|
|
2090
|
+
zoomLevel: 1,
|
|
2091
|
+
zoomMode: _embedpdf_core_stage.ZoomMode.Automatic,
|
|
2092
|
+
currentPageIndex: 0,
|
|
2093
|
+
currentPage: null,
|
|
2094
|
+
pageCount: 0,
|
|
2095
|
+
viewRotation: 0,
|
|
2096
|
+
activeRules: []
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
//#endregion
|
|
1704
2100
|
//#region src/scroll-handler.ts
|
|
1705
2101
|
/**
|
|
1706
2102
|
* Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
|
|
1707
|
-
* (the `scroll` tag) and
|
|
1708
|
-
* captures
|
|
1709
|
-
* claims them first at higher priority). It uses the sample's viewport delta,
|
|
1710
|
-
* it pans over the whole viewport (pages and gaps).
|
|
2103
|
+
* (the `scroll` tag) and, with `panFallback`, under every tool, where it then
|
|
2104
|
+
* captures only page-gap drags (on-page downs belong to the active tool, which
|
|
2105
|
+
* claims them first at higher priority). It uses the sample's viewport delta,
|
|
2106
|
+
* so it pans over the whole viewport (pages and gaps).
|
|
1711
2107
|
*
|
|
1712
|
-
*
|
|
2108
|
+
* Cursors: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
|
|
1713
2109
|
* hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
|
|
1714
2110
|
* from its `Tool.cursor`, so hovering it needs no claim here.
|
|
1715
2111
|
*/
|
|
@@ -1719,24 +2115,24 @@ function createScrollHandler(stage, interaction, options = {}) {
|
|
|
1719
2115
|
x: 0,
|
|
1720
2116
|
y: 0
|
|
1721
2117
|
};
|
|
1722
|
-
const isPanTool = () => interaction.
|
|
2118
|
+
const isPanTool = () => interaction.getActiveTool().enables.has("scroll");
|
|
1723
2119
|
return {
|
|
1724
2120
|
id: "stage-scroll",
|
|
1725
2121
|
priority: 10,
|
|
1726
2122
|
enabledFor: (tool) => tool.enables.has("scroll") || panFallback,
|
|
1727
|
-
onDown: (
|
|
1728
|
-
if (!isPanTool() &&
|
|
1729
|
-
last =
|
|
1730
|
-
interaction.
|
|
2123
|
+
onDown: (sample) => {
|
|
2124
|
+
if (!isPanTool() && sample.page) return false;
|
|
2125
|
+
last = sample.viewport;
|
|
2126
|
+
interaction.claimCursor("stage-grab", "grabbing", 40);
|
|
1731
2127
|
return true;
|
|
1732
2128
|
},
|
|
1733
|
-
onMove: (
|
|
1734
|
-
stage.panBy(
|
|
1735
|
-
last =
|
|
2129
|
+
onMove: (sample) => {
|
|
2130
|
+
stage.panBy(sample.viewport.x - last.x, sample.viewport.y - last.y);
|
|
2131
|
+
last = sample.viewport;
|
|
1736
2132
|
},
|
|
1737
|
-
onUp: () => interaction.
|
|
1738
|
-
onHover: (
|
|
1739
|
-
if (panFallback && !isPanTool()) interaction.
|
|
2133
|
+
onUp: () => interaction.claimCursor("stage-grab", null),
|
|
2134
|
+
onHover: (sample) => {
|
|
2135
|
+
if (panFallback && !isPanTool()) interaction.claimCursor("stage-pan-fallback", sample.page ? null : "grab", 5);
|
|
1740
2136
|
}
|
|
1741
2137
|
};
|
|
1742
2138
|
}
|
|
@@ -1752,10 +2148,10 @@ Object.defineProperty(exports, "ZoomMode", {
|
|
|
1752
2148
|
});
|
|
1753
2149
|
exports.boxOf = boxOf;
|
|
1754
2150
|
exports.createScrollHandler = createScrollHandler;
|
|
1755
|
-
exports.destinationToReveal = require_destination.destinationToReveal;
|
|
1756
2151
|
exports.matchesQuery = matchesQuery;
|
|
1757
2152
|
exports.resolveResponsive = resolveResponsive;
|
|
1758
2153
|
exports.settingsEqual = settingsEqual;
|
|
1759
2154
|
exports.stagePlugin = stagePlugin;
|
|
2155
|
+
exports.stageState = stageState;
|
|
1760
2156
|
|
|
1761
2157
|
//# sourceMappingURL=index.cjs.map
|