@embedpdf/plugin-stage 3.0.0-next.0 → 3.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +594 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +201 -49
- package/dist/index.d.ts +201 -49
- package/dist/index.js +590 -92
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -22,10 +22,153 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
24
|
let _embedpdf_core = require("@embedpdf/core");
|
|
25
|
-
let _embedpdf_plugin_interaction = require("@embedpdf/plugin-interaction");
|
|
26
25
|
let _embedpdf_core_stage = require("@embedpdf/core-stage");
|
|
27
26
|
_embedpdf_core_stage = __toESM(_embedpdf_core_stage, 1);
|
|
28
27
|
let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
28
|
+
//#region src/motion.ts
|
|
29
|
+
/**
|
|
30
|
+
* The camera's MOTION vocabulary — every formula, pure.
|
|
31
|
+
*
|
|
32
|
+
* The capability owns the impure drivers (scheduler frames, dispatch, gesture
|
|
33
|
+
* state); the LAWS those drivers integrate live here, parameterized by dt/t
|
|
34
|
+
* with no state, no scheduler, no dispatch — unit-testable in isolation, and
|
|
35
|
+
* with zero plugin dependencies, liftable into stage-core if the native port
|
|
36
|
+
* ever wants them.
|
|
37
|
+
*
|
|
38
|
+
* Two families:
|
|
39
|
+
* • ballistic — one per-axis law for every free motion: `glideStep` (the
|
|
40
|
+
* touch fling, decaying on UIScrollView's curve) and `springStep` (a
|
|
41
|
+
* critically-damped return to the clamp). A glide that reaches a content
|
|
42
|
+
* edge converts to a spring with its velocity carried in, so the bounce
|
|
43
|
+
* grows out of physics rather than a scripted overshoot.
|
|
44
|
+
* • resistance — the iOS rubber-band curve `rubberOut` and its exact
|
|
45
|
+
* inverse `rubberIn`: dragging `d` px past an edge displays an
|
|
46
|
+
* asymptotically-bounded stretch, and re-entering a gesture over a
|
|
47
|
+
* stretched camera (the mid-bounce catch) reconstructs the
|
|
48
|
+
* finger-integrated position the curve came from.
|
|
49
|
+
* Plus the tween interpolators: `easeOutCubic` for coordinate tweens and
|
|
50
|
+
* `zoomLerp` — GEOMETRIC zoom interpolation (zoom is multiplicative; equal
|
|
51
|
+
* ratios per unit time, not equal deltas — the linear lerp's visible "dip").
|
|
52
|
+
*/
|
|
53
|
+
/** Resistance coefficient of the rubber-band curve (the iOS feel). */
|
|
54
|
+
const RUBBER = .55;
|
|
55
|
+
/** Glide decay per ms — UIScrollView's normal deceleration rate. */
|
|
56
|
+
const FLING_DECAY = .998;
|
|
57
|
+
/** rad/ms — critically damped, ~400ms to settle. */
|
|
58
|
+
const SPRING_OMEGA = .015;
|
|
59
|
+
/**
|
|
60
|
+
* Overshoot distance → displayed stretch: asymptotic to the viewport
|
|
61
|
+
* dimension `dim`, so the content never leaves the screen however far the
|
|
62
|
+
* finger travels. `rubberOut(0) === 0` — inside the bounds the curve is
|
|
63
|
+
* invisible.
|
|
64
|
+
*/
|
|
65
|
+
const rubberOut = (d, dim) => (1 - 1 / (d * RUBBER / dim + 1)) * dim;
|
|
66
|
+
/** Exact inverse of `rubberOut` (defined for `out < dim`). */
|
|
67
|
+
const rubberIn = (out, dim) => out * dim / (RUBBER * (dim - out));
|
|
68
|
+
/**
|
|
69
|
+
* One frame of free glide: exponential velocity decay, position advanced by
|
|
70
|
+
* the midpoint integral of the decay over `dt` (exact for the exponential —
|
|
71
|
+
* frame-rate independent).
|
|
72
|
+
*/
|
|
73
|
+
const glideStep = (p, v, dt) => {
|
|
74
|
+
const k = Math.pow(FLING_DECAY, dt);
|
|
75
|
+
const np = p + v * (dt * (1 + k) / 2);
|
|
76
|
+
const nv = v * k;
|
|
77
|
+
return Math.abs(nv) < .02 ? {
|
|
78
|
+
p: np,
|
|
79
|
+
v: 0,
|
|
80
|
+
done: true
|
|
81
|
+
} : {
|
|
82
|
+
p: np,
|
|
83
|
+
v: nv,
|
|
84
|
+
done: false
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* One frame of critically-damped spring toward `edge` — semi-implicit Euler
|
|
89
|
+
* on x'' = −ω²(x−e) − 2ωx'. Lands exactly ON the edge (position snapped,
|
|
90
|
+
* velocity zeroed) once inside half a pixel at negligible speed.
|
|
91
|
+
*/
|
|
92
|
+
const springStep = (p, v, edge, dt) => {
|
|
93
|
+
const nv = v + (-.015 * SPRING_OMEGA * (p - edge) - 2 * SPRING_OMEGA * v) * dt;
|
|
94
|
+
const np = p + nv * dt;
|
|
95
|
+
if (Math.abs(np - edge) < .5 && Math.abs(nv) < .01) return {
|
|
96
|
+
p: edge,
|
|
97
|
+
v: 0,
|
|
98
|
+
done: true
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
p: np,
|
|
102
|
+
v: nv,
|
|
103
|
+
done: false
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
|
|
107
|
+
/**
|
|
108
|
+
* Geometric zoom interpolation: `z0·(z1/z0)^k`. Zoom is multiplicative, so a
|
|
109
|
+
* constant-RATE zoom moves equal ratios per unit time; interpolating the
|
|
110
|
+
* level linearly instead makes the start rush and the end crawl (and, under
|
|
111
|
+
* a focal anchor, swings the anchored point along a curved path).
|
|
112
|
+
*/
|
|
113
|
+
const zoomLerp = (z0, z1, k) => z0 * Math.pow(z1 / z0, k);
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/responsive.ts
|
|
116
|
+
/**
|
|
117
|
+
* Container queries for the settings bag — the pure half. The capability owns
|
|
118
|
+
* the driver (when to re-resolve, how to react); the LAWS live here: what a
|
|
119
|
+
* box is, when a query matches, and what the effective settings are. No state,
|
|
120
|
+
* no dispatch — property-testable in isolation (the `wheel.ts` convention).
|
|
121
|
+
*
|
|
122
|
+
* The query vocabulary is deliberately just the box: a headless, DOM-free
|
|
123
|
+
* stage knows exactly one environmental fact — the viewport it was told.
|
|
124
|
+
* Anything else (pointer coarseness, platform, app state) is either per-event
|
|
125
|
+
* (`PointerSample.pointerType`) or the app's own business via `update()`.
|
|
126
|
+
*/
|
|
127
|
+
/** The reported viewport as a queryable box. A square box is 'portrait' (CSS). */
|
|
128
|
+
const boxOf = (vp) => ({
|
|
129
|
+
width: vp.width,
|
|
130
|
+
height: vp.height,
|
|
131
|
+
orientation: vp.height >= vp.width ? "portrait" : "landscape"
|
|
132
|
+
});
|
|
133
|
+
/** All bounds inclusive, all fields optional, conditions AND-ed (CSS ranges). */
|
|
134
|
+
const matchesQuery = (q, box) => (q.minWidth === void 0 || box.width >= q.minWidth) && (q.maxWidth === void 0 || box.width <= q.maxWidth) && (q.minHeight === void 0 || box.height >= q.minHeight) && (q.maxHeight === void 0 || box.height <= q.maxHeight) && (q.orientation === void 0 || box.orientation === q.orientation);
|
|
135
|
+
const ruleMatches = (rule, box) => typeof rule.when === "function" ? !!rule.when(box) : matchesQuery(rule.when, box);
|
|
136
|
+
/** Merge a patch over settings, skipping undefined values (Partial semantics). */
|
|
137
|
+
const mergeSettings = (into, patch) => {
|
|
138
|
+
const out = { ...into };
|
|
139
|
+
let key;
|
|
140
|
+
for (key in patch) {
|
|
141
|
+
const value = patch[key];
|
|
142
|
+
if (value !== void 0) Object.assign(out, { [key]: value });
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Setting-value equality: primitives by identity, the flat objects the
|
|
148
|
+
* settings vocabulary uses (`{ px }`, `{ x, y }`, page frames, zoom specs) by
|
|
149
|
+
* one level of own-key comparison. Settings values are never nested deeper.
|
|
150
|
+
*/
|
|
151
|
+
const eqSetting = (a, b) => {
|
|
152
|
+
if (a === b) return true;
|
|
153
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
154
|
+
const ka = Object.keys(a);
|
|
155
|
+
const kb = Object.keys(b);
|
|
156
|
+
return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
|
|
157
|
+
};
|
|
158
|
+
const resolveResponsive = (base, rules, box) => {
|
|
159
|
+
let effective = base;
|
|
160
|
+
const active = [];
|
|
161
|
+
for (const rule of rules) {
|
|
162
|
+
if (!ruleMatches(rule, box)) continue;
|
|
163
|
+
if (rule.name !== void 0) active.push(rule.name);
|
|
164
|
+
if (rule.settings) effective = mergeSettings(effective, rule.settings);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
effective,
|
|
168
|
+
active
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
29
172
|
//#region src/settings.ts
|
|
30
173
|
/**
|
|
31
174
|
* Out-of-the-box defaults — a sensible document-reading feel. They are JUST
|
|
@@ -71,6 +214,18 @@ const DEFAULT_SETTINGS = {
|
|
|
71
214
|
scrollBehavior: "smooth",
|
|
72
215
|
viewUnitsPerPoint: 96 / 72
|
|
73
216
|
};
|
|
217
|
+
/**
|
|
218
|
+
* Out-of-the-box responsive rules — the platform feel with zero configuration:
|
|
219
|
+
* a compact container gets the thin gutter phones use (space, not device — an
|
|
220
|
+
* embedded 500px pane on a desktop is compact too). Override the whole list
|
|
221
|
+
* with `stagePlugin({ responsive: [...] })`; `responsive: []` opts out. The
|
|
222
|
+
* 'compact' name is queryable app-side via `stage.matches('compact')`.
|
|
223
|
+
*/
|
|
224
|
+
const DEFAULT_RESPONSIVE = [{
|
|
225
|
+
name: "compact",
|
|
226
|
+
when: { maxWidth: 600 },
|
|
227
|
+
settings: { padding: 4 }
|
|
228
|
+
}];
|
|
74
229
|
const SETTINGS_EFFECT = {
|
|
75
230
|
flow: "reflow",
|
|
76
231
|
layout: "scene",
|
|
@@ -109,8 +264,9 @@ const settingsEqual = (a, b) => SETTING_KEYS.every((k) => valueEq(a[k], b[k]));
|
|
|
109
264
|
* Layering, stated honestly:
|
|
110
265
|
* • stage-core — PURE spatial math (no DOM, no time). Ports to Rust later.
|
|
111
266
|
* • this capability — the IMPURE platform shell. It dispatches, caches, and owns
|
|
112
|
-
* the camera tween. It is NOT pure;
|
|
113
|
-
*
|
|
267
|
+
* the camera tween. It is NOT pure; its one host dependency (frame timing)
|
|
268
|
+
* enters through the Scheduler seam — injectable, defaulting to the host's
|
|
269
|
+
* own frame clock when one exists (see the seam below).
|
|
114
270
|
*
|
|
115
271
|
* The model: EVERY camera move is defined by what it holds fixed.
|
|
116
272
|
* • gestures (pan/pinch/wheel) — the content under the pointer. Physics, no setting.
|
|
@@ -396,12 +552,100 @@ function createStageCapability(ctx, config = {}) {
|
|
|
396
552
|
fitAlign: ctx.getState().fitAlign,
|
|
397
553
|
direction: ctx.getState().direction
|
|
398
554
|
});
|
|
555
|
+
let placementStarted = false;
|
|
556
|
+
let gestureDepth = 0;
|
|
557
|
+
let gestureZoomed = false;
|
|
558
|
+
let gestureElastic = false;
|
|
559
|
+
let gestureRaw = null;
|
|
560
|
+
const REST_MS = 150;
|
|
561
|
+
let restRaf = 0;
|
|
562
|
+
const armRest = () => {
|
|
563
|
+
if (!canAnimate || !placementStarted) return;
|
|
564
|
+
if (ctx.getState().cameraResting) ctx.dispatch({
|
|
565
|
+
type: "CAMERA_REST",
|
|
566
|
+
resting: false
|
|
567
|
+
});
|
|
568
|
+
if (restRaf) scheduler.caf(restRaf);
|
|
569
|
+
let t0 = 0;
|
|
570
|
+
const tick = (ts) => {
|
|
571
|
+
restRaf = 0;
|
|
572
|
+
if (!t0) t0 = ts;
|
|
573
|
+
if (ts - t0 >= REST_MS) {
|
|
574
|
+
ctx.dispatch({
|
|
575
|
+
type: "CAMERA_REST",
|
|
576
|
+
resting: true
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
restRaf = scheduler.raf(tick);
|
|
581
|
+
};
|
|
582
|
+
restRaf = scheduler.raf(tick);
|
|
583
|
+
};
|
|
399
584
|
const setCam = (next, bounds = stayBounds()) => {
|
|
585
|
+
const clamped = _embedpdf_core_stage.clampCamera(next, bounds, vp(), constraint());
|
|
586
|
+
if (clamped.zoom !== cam().zoom) if (gestureDepth > 0) {
|
|
587
|
+
gestureZoomed = true;
|
|
588
|
+
if (ctx.getState().cameraResting) ctx.dispatch({
|
|
589
|
+
type: "CAMERA_REST",
|
|
590
|
+
resting: false
|
|
591
|
+
});
|
|
592
|
+
if (restRaf) {
|
|
593
|
+
scheduler.caf(restRaf);
|
|
594
|
+
restRaf = 0;
|
|
595
|
+
}
|
|
596
|
+
} else armRest();
|
|
400
597
|
ctx.dispatch({
|
|
401
598
|
type: "CAMERA",
|
|
402
|
-
camera:
|
|
599
|
+
camera: clamped
|
|
403
600
|
});
|
|
404
601
|
};
|
|
602
|
+
const axisTravels = (origin, content, view, zoom) => !_embedpdf_core_stage.travelRange(origin, content, view, zoom, constraint().padding).fits;
|
|
603
|
+
/** Unclamped finger-integrated camera → the DISPLAYED camera: clamp, then
|
|
604
|
+
* re-apply the overshoot through the resistance curve, per travelling axis.
|
|
605
|
+
* Inside the bounds this is exactly the clamp (rubber of zero is zero);
|
|
606
|
+
* on a fitting axis it is exactly the clamp ALWAYS. */
|
|
607
|
+
const rubberize = (raw) => {
|
|
608
|
+
const clamped = _embedpdf_core_stage.clampCamera(raw, stayBounds(), vp(), constraint());
|
|
609
|
+
const v = vp();
|
|
610
|
+
const b = stayBounds();
|
|
611
|
+
const axis = (rawA, clampedA, dim, travels) => {
|
|
612
|
+
if (!travels) return clampedA;
|
|
613
|
+
const dWorld = rawA - clampedA;
|
|
614
|
+
if (dWorld === 0) return clampedA;
|
|
615
|
+
const out = rubberOut(Math.abs(dWorld) * raw.zoom, Math.max(1, dim));
|
|
616
|
+
return clampedA + Math.sign(dWorld) * out / raw.zoom;
|
|
617
|
+
};
|
|
618
|
+
return {
|
|
619
|
+
zoom: raw.zoom,
|
|
620
|
+
x: axis(raw.x, clamped.x, v.width, axisTravels(b.x, b.width, v.width, raw.zoom)),
|
|
621
|
+
y: axis(raw.y, clamped.y, v.height, axisTravels(b.y, b.height, v.height, raw.zoom))
|
|
622
|
+
};
|
|
623
|
+
};
|
|
624
|
+
/** Displayed camera → the raw position `rubberize` would have produced it
|
|
625
|
+
* from. Overshoot is capped just under the asymptote so the inverse stays
|
|
626
|
+
* finite whatever state a catch finds the camera in. */
|
|
627
|
+
const unrubberize = (displayed) => {
|
|
628
|
+
const clamped = _embedpdf_core_stage.clampCamera(displayed, stayBounds(), vp(), constraint());
|
|
629
|
+
const v = vp();
|
|
630
|
+
const b = stayBounds();
|
|
631
|
+
const axis = (dispA, clampedA, dim, travels) => {
|
|
632
|
+
if (!travels) return clampedA;
|
|
633
|
+
const dWorld = dispA - clampedA;
|
|
634
|
+
if (dWorld === 0) return clampedA;
|
|
635
|
+
const d = Math.max(1, dim);
|
|
636
|
+
const out = Math.min(Math.abs(dWorld) * displayed.zoom, d - 1);
|
|
637
|
+
return clampedA + Math.sign(dWorld) * rubberIn(out, d) / displayed.zoom;
|
|
638
|
+
};
|
|
639
|
+
return {
|
|
640
|
+
zoom: displayed.zoom,
|
|
641
|
+
x: axis(displayed.x, clamped.x, v.width, axisTravels(b.x, b.width, v.width, displayed.zoom)),
|
|
642
|
+
y: axis(displayed.y, clamped.y, v.height, axisTravels(b.y, b.height, v.height, displayed.zoom))
|
|
643
|
+
};
|
|
644
|
+
};
|
|
645
|
+
const setCamRaw = (c) => ctx.dispatch({
|
|
646
|
+
type: "CAMERA",
|
|
647
|
+
camera: c
|
|
648
|
+
});
|
|
405
649
|
/**
|
|
406
650
|
* Cursor reconciliation, one direction per interaction:
|
|
407
651
|
* navigation → cursor is INTENT, set explicitly; the camera honors it as far
|
|
@@ -435,7 +679,6 @@ function createStageCapability(ctx, config = {}) {
|
|
|
435
679
|
}
|
|
436
680
|
cancelAnim();
|
|
437
681
|
const from = cam();
|
|
438
|
-
const ease = (t) => 1 - Math.pow(1 - t, 3);
|
|
439
682
|
let started = false;
|
|
440
683
|
let t0 = 0;
|
|
441
684
|
const tick = (now) => {
|
|
@@ -443,7 +686,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
443
686
|
started = true;
|
|
444
687
|
t0 = now;
|
|
445
688
|
}
|
|
446
|
-
const k =
|
|
689
|
+
const k = easeOutCubic(Math.min(1, (now - t0) / ms));
|
|
447
690
|
setCam({
|
|
448
691
|
x: lerp(from.x, target.x, k),
|
|
449
692
|
y: lerp(from.y, target.y, k),
|
|
@@ -457,6 +700,116 @@ function createStageCapability(ctx, config = {}) {
|
|
|
457
700
|
};
|
|
458
701
|
raf = scheduler.raf(tick);
|
|
459
702
|
};
|
|
703
|
+
/**
|
|
704
|
+
* Zoom-anchored tween — animates THE INVARIANT, not the coordinates. A
|
|
705
|
+
* camera move is defined by what it holds fixed; for an anchored zoom that
|
|
706
|
+
* is "the focal page-point stays at `pt` while scale changes". Lerping
|
|
707
|
+
* x/y/zoom independently (the plain tween) breaks that mid-flight: holding
|
|
708
|
+
* a screen point requires `x(t) = focal.x − pt.x / zoom(t)` — hyperbolic in
|
|
709
|
+
* the zoom — so linear coordinates swing the tapped point along a curved
|
|
710
|
+
* path (the visible "dip"). Here the zoom interpolates GEOMETRICALLY
|
|
711
|
+
* (constant rate — zoom is multiplicative) and each frame's camera derives
|
|
712
|
+
* from the anchor at that zoom, then clamps: the focal point is stationary
|
|
713
|
+
* by construction, and a zoom-out rails into its fit progressively as the
|
|
714
|
+
* shrinking travel range lets the clamp take over.
|
|
715
|
+
*/
|
|
716
|
+
const animateZoomAnchored = (focal, pt, targetZoom, ms = 240, then) => {
|
|
717
|
+
if (!canAnimate) {
|
|
718
|
+
const sc = buildScene();
|
|
719
|
+
if (sc.itemCount) setCam(_embedpdf_core_stage.cameraForAnchorAtScreen(focal, sc, pt, targetZoom));
|
|
720
|
+
then?.();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
cancelAnim();
|
|
724
|
+
const z0 = cam().zoom;
|
|
725
|
+
let started = false;
|
|
726
|
+
let t0 = 0;
|
|
727
|
+
const tick = (now) => {
|
|
728
|
+
if (!started) {
|
|
729
|
+
started = true;
|
|
730
|
+
t0 = now;
|
|
731
|
+
}
|
|
732
|
+
const k = easeOutCubic(Math.min(1, (now - t0) / ms));
|
|
733
|
+
const z = zoomLerp(z0, targetZoom, k);
|
|
734
|
+
const sc = buildScene();
|
|
735
|
+
if (sc.itemCount) setCam(_embedpdf_core_stage.cameraForAnchorAtScreen(focal, sc, pt, z));
|
|
736
|
+
if (k < 1) raf = scheduler.raf(tick);
|
|
737
|
+
else {
|
|
738
|
+
raf = 0;
|
|
739
|
+
then?.();
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
raf = scheduler.raf(tick);
|
|
743
|
+
};
|
|
744
|
+
const startCameraPhysics = (vxFinger, vyFinger) => {
|
|
745
|
+
if (!canAnimate) {
|
|
746
|
+
setCam(cam());
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
cancelAnim();
|
|
750
|
+
const zoom = cam().zoom;
|
|
751
|
+
const c0 = cam();
|
|
752
|
+
const cl0 = _embedpdf_core_stage.clampCamera(c0, stayBounds(), vp(), constraint());
|
|
753
|
+
const b0 = stayBounds();
|
|
754
|
+
const v0 = vp();
|
|
755
|
+
const mk = (camA, clampedA, fingerV) => ({
|
|
756
|
+
p: camA * zoom,
|
|
757
|
+
v: -fingerV / 1e3,
|
|
758
|
+
springing: camA !== clampedA,
|
|
759
|
+
done: false
|
|
760
|
+
});
|
|
761
|
+
const ax = mk(c0.x, cl0.x, axisTravels(b0.x, b0.width, v0.width, zoom) ? vxFinger : 0);
|
|
762
|
+
const ay = mk(c0.y, cl0.y, axisTravels(b0.y, b0.height, v0.height, zoom) ? vyFinger : 0);
|
|
763
|
+
if (!ax.springing && !ay.springing && Math.hypot(ax.v, ay.v) < .02) return;
|
|
764
|
+
let started = false;
|
|
765
|
+
let last = 0;
|
|
766
|
+
const tick = (now) => {
|
|
767
|
+
raf = 0;
|
|
768
|
+
const dt = started ? Math.min(64, Math.max(1, now - last)) : 16;
|
|
769
|
+
started = true;
|
|
770
|
+
last = now;
|
|
771
|
+
const cl = _embedpdf_core_stage.clampCamera({
|
|
772
|
+
x: ax.p / zoom,
|
|
773
|
+
y: ay.p / zoom,
|
|
774
|
+
zoom
|
|
775
|
+
}, stayBounds(), vp(), constraint());
|
|
776
|
+
const step = (a, edgeWorld) => {
|
|
777
|
+
if (a.done) return;
|
|
778
|
+
const r = a.springing ? springStep(a.p, a.v, edgeWorld * zoom, dt) : glideStep(a.p, a.v, dt);
|
|
779
|
+
a.p = r.p;
|
|
780
|
+
a.v = r.v;
|
|
781
|
+
a.done = r.done;
|
|
782
|
+
};
|
|
783
|
+
step(ax, cl.x);
|
|
784
|
+
step(ay, cl.y);
|
|
785
|
+
const ncl = _embedpdf_core_stage.clampCamera({
|
|
786
|
+
x: ax.p / zoom,
|
|
787
|
+
y: ay.p / zoom,
|
|
788
|
+
zoom
|
|
789
|
+
}, stayBounds(), vp(), constraint());
|
|
790
|
+
const convert = (a, clampedA) => {
|
|
791
|
+
if (!a.springing && Math.abs(clampedA * zoom - a.p) > 1e-6) {
|
|
792
|
+
a.p = clampedA * zoom;
|
|
793
|
+
a.springing = true;
|
|
794
|
+
a.done = false;
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
convert(ax, ncl.x);
|
|
798
|
+
convert(ay, ncl.y);
|
|
799
|
+
setCamRaw({
|
|
800
|
+
x: ax.p / zoom,
|
|
801
|
+
y: ay.p / zoom,
|
|
802
|
+
zoom
|
|
803
|
+
});
|
|
804
|
+
syncCursorFromCamera();
|
|
805
|
+
if (ax.done && ay.done) {
|
|
806
|
+
setCam(cam());
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
raf = scheduler.raf(tick);
|
|
810
|
+
};
|
|
811
|
+
raf = scheduler.raf(tick);
|
|
812
|
+
};
|
|
460
813
|
const anchorAt = (at) => {
|
|
461
814
|
const sc = buildScene();
|
|
462
815
|
return sc.itemCount ? _embedpdf_core_stage.anchorFromCamera(cam(), sc, vp(), at) : {
|
|
@@ -584,8 +937,11 @@ function createStageCapability(ctx, config = {}) {
|
|
|
584
937
|
} : unrotatedPoints(box);
|
|
585
938
|
const c = cam();
|
|
586
939
|
const ratio = dpr();
|
|
587
|
-
const
|
|
588
|
-
const
|
|
940
|
+
const rawX = (box.x - c.x) * c.zoom;
|
|
941
|
+
const rawY = (box.y - c.y) * c.zoom;
|
|
942
|
+
const resting = ctx.getState().cameraResting;
|
|
943
|
+
const screenX = resting ? (0, _embedpdf_core_geometry.snapToDevice)(rawX, ratio) : rawX;
|
|
944
|
+
const screenY = resting ? (0, _embedpdf_core_geometry.snapToDevice)(rawY, ratio) : rawY;
|
|
589
945
|
const transform = (0, _embedpdf_core_geometry.pageTransform)({
|
|
590
946
|
pageSize,
|
|
591
947
|
rotation: box.rotation,
|
|
@@ -605,7 +961,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
605
961
|
width: transform.viewWidth,
|
|
606
962
|
height: transform.viewHeight
|
|
607
963
|
});
|
|
608
|
-
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.
|
|
964
|
+
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.viewToContentRect(onScreen) : {
|
|
609
965
|
x: 0,
|
|
610
966
|
y: 0,
|
|
611
967
|
width: 0,
|
|
@@ -620,8 +976,19 @@ function createStageCapability(ctx, config = {}) {
|
|
|
620
976
|
visibleRect
|
|
621
977
|
};
|
|
622
978
|
};
|
|
979
|
+
const EMPTY_SCROLL_METRICS = {
|
|
980
|
+
scrollLeft: 0,
|
|
981
|
+
scrollTop: 0,
|
|
982
|
+
scrollWidth: 0,
|
|
983
|
+
scrollHeight: 0,
|
|
984
|
+
clientWidth: 0,
|
|
985
|
+
clientHeight: 0,
|
|
986
|
+
scrollableX: false,
|
|
987
|
+
scrollableY: false
|
|
988
|
+
};
|
|
623
989
|
let scrollMemo = null;
|
|
624
990
|
const scrollMetricsNow = () => {
|
|
991
|
+
if (!ctx.getState().placed) return EMPTY_SCROLL_METRICS;
|
|
625
992
|
const sc = buildScene();
|
|
626
993
|
const m = _embedpdf_core_stage.scrollMetrics(cam(), sc.itemCount ? stayBounds() : {
|
|
627
994
|
x: 0,
|
|
@@ -634,14 +1001,16 @@ function createStageCapability(ctx, config = {}) {
|
|
|
634
1001
|
scrollMemo = m;
|
|
635
1002
|
return m;
|
|
636
1003
|
};
|
|
1004
|
+
const EMPTY_VISIBLE_PAGES = [];
|
|
637
1005
|
let visSig = "";
|
|
638
1006
|
let vis = [];
|
|
639
1007
|
const visiblePages = () => {
|
|
1008
|
+
if (!ctx.getState().placed) return EMPTY_VISIBLE_PAGES;
|
|
640
1009
|
const c = cam();
|
|
641
1010
|
const v = vp();
|
|
642
1011
|
const sc = buildScene();
|
|
643
1012
|
const items = paged() ? sc.items.slice(0, 1) : sc.query(_embedpdf_core_stage.cameraWorldRect(c, v));
|
|
644
|
-
const sig = `${sceneCache.key}|${ctx.getState().flow}|${c.x},${c.y},${c.zoom}|${v.width}x${v.height}|${dpr()}`;
|
|
1013
|
+
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}`;
|
|
645
1014
|
if (sig === visSig) return vis;
|
|
646
1015
|
visSig = sig;
|
|
647
1016
|
vis = items.flatMap((it) => it.pages).map(withTransform);
|
|
@@ -653,8 +1022,73 @@ function createStageCapability(ctx, config = {}) {
|
|
|
653
1022
|
return out;
|
|
654
1023
|
};
|
|
655
1024
|
const snapshotSettings = () => pickSettings(ctx.getState());
|
|
1025
|
+
/**
|
|
1026
|
+
* Settings change, reacted per the registry — the strongest effect among the
|
|
1027
|
+
* touched settings wins: 'reflow' ⊃ 'scene'/'refit' ⊃ 'reclamp' ⊃ 'none'.
|
|
1028
|
+
* The ONE reactive applier: the public `update()` and the responsive driver
|
|
1029
|
+
* both land here, so a breakpoint crossing gets exactly the invariant a
|
|
1030
|
+
* hand-written update would.
|
|
1031
|
+
*/
|
|
1032
|
+
const applySettings = (patch) => {
|
|
1033
|
+
cancelAnim();
|
|
1034
|
+
const touched = (effect) => SETTING_KEYS.some((k) => patch[k] !== void 0 && SETTINGS_EFFECT[k] === effect);
|
|
1035
|
+
const align = touched("refit") && !touched("scene") && !touched("reflow") ? ctx.getState().zoomAlign : ctx.getState().anchorAlign;
|
|
1036
|
+
const anchor = anchorAt(alignPoint(align, vp()));
|
|
1037
|
+
ctx.dispatch({
|
|
1038
|
+
type: "PATCH",
|
|
1039
|
+
patch
|
|
1040
|
+
});
|
|
1041
|
+
if (touched("scene")) sceneCache = null;
|
|
1042
|
+
if (touched("reflow")) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
1043
|
+
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
1044
|
+
else if (touched("reclamp")) setCam(cam());
|
|
1045
|
+
};
|
|
1046
|
+
let base = snapshotSettings();
|
|
1047
|
+
let rules = config.responsive ?? DEFAULT_RESPONSIVE;
|
|
1048
|
+
let lastEffective = base;
|
|
1049
|
+
const publishActive = (active) => {
|
|
1050
|
+
const prev = ctx.getState().activeRules;
|
|
1051
|
+
if (prev.length !== active.length || active.some((n, i) => n !== prev[i])) ctx.dispatch({
|
|
1052
|
+
type: "RESPONSIVE",
|
|
1053
|
+
active
|
|
1054
|
+
});
|
|
1055
|
+
};
|
|
1056
|
+
/**
|
|
1057
|
+
* Re-resolve and apply what CHANGED since the last resolution. `react: true`
|
|
1058
|
+
* routes the patch through `applySettings` (full anchor-preserving update);
|
|
1059
|
+
* `react: false` (inside `setViewport`, whose caller runs its own reframe)
|
|
1060
|
+
* just lands the patch + cache invalidation and reports the strongest
|
|
1061
|
+
* effect so the caller can escalate a rule-driven flow flip.
|
|
1062
|
+
*/
|
|
1063
|
+
const syncResponsive = (react) => {
|
|
1064
|
+
const { effective, active } = resolveResponsive(base, rules, boxOf(vp()));
|
|
1065
|
+
const patch = {};
|
|
1066
|
+
for (const k of SETTING_KEYS) if (!eqSetting(effective[k], lastEffective[k])) Object.assign(patch, { [k]: effective[k] });
|
|
1067
|
+
lastEffective = effective;
|
|
1068
|
+
publishActive(active);
|
|
1069
|
+
const keys = Object.keys(patch);
|
|
1070
|
+
if (keys.length === 0) return "none";
|
|
1071
|
+
if (react) {
|
|
1072
|
+
applySettings(patch);
|
|
1073
|
+
return "none";
|
|
1074
|
+
}
|
|
1075
|
+
ctx.dispatch({
|
|
1076
|
+
type: "PATCH",
|
|
1077
|
+
patch
|
|
1078
|
+
});
|
|
1079
|
+
const rank = {
|
|
1080
|
+
none: 0,
|
|
1081
|
+
reclamp: 1,
|
|
1082
|
+
refit: 2,
|
|
1083
|
+
scene: 3,
|
|
1084
|
+
reflow: 4
|
|
1085
|
+
};
|
|
1086
|
+
let strongest = "none";
|
|
1087
|
+
for (const k of keys) if (rank[SETTINGS_EFFECT[k]] > rank[strongest]) strongest = SETTINGS_EFFECT[k];
|
|
1088
|
+
if (strongest === "scene" || strongest === "reflow") sceneCache = null;
|
|
1089
|
+
return strongest;
|
|
1090
|
+
};
|
|
656
1091
|
const initialViewProviders = [];
|
|
657
|
-
let hasPlaced = false;
|
|
658
1092
|
const api = {
|
|
659
1093
|
camera: cam,
|
|
660
1094
|
viewport: vp,
|
|
@@ -672,6 +1106,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
672
1106
|
label: p.label ?? null
|
|
673
1107
|
})),
|
|
674
1108
|
pageRect: (pon) => {
|
|
1109
|
+
if (!ctx.getState().placed) return null;
|
|
675
1110
|
const meta = ctx.document();
|
|
676
1111
|
const index = meta ? meta.pages.findIndex((p) => p.pageObjectNumber === pon) : -1;
|
|
677
1112
|
if (index < 0) return null;
|
|
@@ -686,7 +1121,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
686
1121
|
const ly = screen.y - p.screenY;
|
|
687
1122
|
if (lx >= 0 && ly >= 0 && lx <= p.transform.viewWidth && ly <= p.transform.viewHeight) return {
|
|
688
1123
|
pon: p.pon,
|
|
689
|
-
point: p.transform.
|
|
1124
|
+
point: p.transform.viewToContent({
|
|
690
1125
|
x: lx,
|
|
691
1126
|
y: ly
|
|
692
1127
|
}),
|
|
@@ -700,7 +1135,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
700
1135
|
pointOnPage: (pon, screen) => {
|
|
701
1136
|
const p = visiblePages().find((v) => v.pon === pon);
|
|
702
1137
|
if (!p) return null;
|
|
703
|
-
return p.transform.
|
|
1138
|
+
return p.transform.viewToContent({
|
|
704
1139
|
x: screen.x - p.screenX,
|
|
705
1140
|
y: screen.y - p.screenY
|
|
706
1141
|
});
|
|
@@ -772,11 +1207,12 @@ function createStageCapability(ctx, config = {}) {
|
|
|
772
1207
|
anchor: currentAnchor()
|
|
773
1208
|
}),
|
|
774
1209
|
setViewport: (v) => {
|
|
775
|
-
if (!
|
|
1210
|
+
if (!placementStarted) {
|
|
776
1211
|
ctx.dispatch({
|
|
777
1212
|
type: "VP",
|
|
778
1213
|
vp: v
|
|
779
1214
|
});
|
|
1215
|
+
syncResponsive(false);
|
|
780
1216
|
if (v.width > 0 && v.height > 0 && (ctx.document()?.pageCount ?? 0) > 0) api.placeInitial();
|
|
781
1217
|
return;
|
|
782
1218
|
}
|
|
@@ -786,7 +1222,8 @@ function createStageCapability(ctx, config = {}) {
|
|
|
786
1222
|
type: "VP",
|
|
787
1223
|
vp: v
|
|
788
1224
|
});
|
|
789
|
-
|
|
1225
|
+
if (syncResponsive(false) === "reflow") goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
1226
|
+
else reapply(anchor);
|
|
790
1227
|
},
|
|
791
1228
|
setDevicePixelRatio: (ratio) => {
|
|
792
1229
|
if (ratio > 0 && ratio !== dpr()) ctx.dispatch({
|
|
@@ -801,7 +1238,10 @@ function createStageCapability(ctx, config = {}) {
|
|
|
801
1238
|
},
|
|
802
1239
|
panBy: (dx, dy) => {
|
|
803
1240
|
cancelAnim();
|
|
804
|
-
|
|
1241
|
+
if (gestureDepth > 0 && gestureElastic) {
|
|
1242
|
+
gestureRaw = _embedpdf_core_stage.panByScreen(gestureRaw ?? unrubberize(cam()), dx, dy);
|
|
1243
|
+
setCamRaw(rubberize(gestureRaw));
|
|
1244
|
+
} else setCam(_embedpdf_core_stage.panByScreen(cam(), dx, dy));
|
|
805
1245
|
syncCursorFromCamera();
|
|
806
1246
|
},
|
|
807
1247
|
scrollTo: (opts) => {
|
|
@@ -827,14 +1267,72 @@ function createStageCapability(ctx, config = {}) {
|
|
|
827
1267
|
const before = buildScene();
|
|
828
1268
|
const focal = before.itemCount ? _embedpdf_core_stage.anchorAtPoint(before, _embedpdf_core_stage.toWorld(cam(), pt)) : null;
|
|
829
1269
|
setCam(_embedpdf_core_stage.zoomAround(cam(), pt, factor));
|
|
830
|
-
ctx.dispatch({
|
|
1270
|
+
if (gestureDepth === 0) ctx.dispatch({
|
|
831
1271
|
type: "PATCH",
|
|
832
1272
|
patch: { zoom: { level: cam().zoom } }
|
|
833
1273
|
});
|
|
834
1274
|
const after = buildScene();
|
|
835
1275
|
if (after !== before && focal && after.itemCount) setCam(_embedpdf_core_stage.cameraForAnchorAtScreen(focal, after, pt, cam().zoom));
|
|
1276
|
+
if (gestureDepth > 0 && gestureElastic) gestureRaw = unrubberize(cam());
|
|
836
1277
|
syncCursorFromCamera();
|
|
837
1278
|
},
|
|
1279
|
+
beginGesture: (options) => {
|
|
1280
|
+
gestureDepth++;
|
|
1281
|
+
if (gestureDepth === 1) {
|
|
1282
|
+
cancelAnim();
|
|
1283
|
+
gestureZoomed = false;
|
|
1284
|
+
gestureElastic = options?.elastic === true;
|
|
1285
|
+
gestureRaw = null;
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
endGesture: () => {
|
|
1289
|
+
if (gestureDepth === 0) return;
|
|
1290
|
+
gestureDepth--;
|
|
1291
|
+
if (gestureDepth > 0) return;
|
|
1292
|
+
gestureRaw = null;
|
|
1293
|
+
const wasElastic = gestureElastic;
|
|
1294
|
+
gestureElastic = false;
|
|
1295
|
+
if (gestureZoomed) {
|
|
1296
|
+
gestureZoomed = false;
|
|
1297
|
+
ctx.dispatch({
|
|
1298
|
+
type: "PATCH",
|
|
1299
|
+
patch: { zoom: { level: cam().zoom } }
|
|
1300
|
+
});
|
|
1301
|
+
armRest();
|
|
1302
|
+
}
|
|
1303
|
+
syncCursorFromCamera();
|
|
1304
|
+
if (wasElastic) {
|
|
1305
|
+
const c = cam();
|
|
1306
|
+
const cl = _embedpdf_core_stage.clampCamera(c, stayBounds(), vp(), constraint());
|
|
1307
|
+
if (cl.x !== c.x || cl.y !== c.y) startCameraPhysics(0, 0);
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
fling: (vx, vy) => startCameraPhysics(vx, vy),
|
|
1311
|
+
cameraInMotion: () => raf !== 0,
|
|
1312
|
+
doubleTapZoom: (pt) => {
|
|
1313
|
+
cancelAnim();
|
|
1314
|
+
const sc0 = buildScene();
|
|
1315
|
+
if (!sc0.itemCount) return;
|
|
1316
|
+
const item = paged() ? sc0.items[0] : sc0.items[itemIndexOfPage(ctx.getState().cursor)];
|
|
1317
|
+
const fit = fitBox(item);
|
|
1318
|
+
const auto = _embedpdf_core_stage.resolveZoom({ mode: _embedpdf_core_stage.ZoomMode.Automatic }, fit, vp(), pad());
|
|
1319
|
+
const stops = [
|
|
1320
|
+
auto,
|
|
1321
|
+
_embedpdf_core_stage.resolveZoom({ mode: _embedpdf_core_stage.ZoomMode.FitWidth }, fit, vp(), pad()),
|
|
1322
|
+
Math.min(auto * 2.5, _embedpdf_core_stage.ZOOM_MAX)
|
|
1323
|
+
].sort((a, b) => a - b).filter((z, i, all) => i === 0 || z > all[i - 1] * 1.1);
|
|
1324
|
+
const near = (z, stop) => z > stop / 1.1 && z < stop * 1.1;
|
|
1325
|
+
const onRung = stops.findIndex((s) => near(cam().zoom, s));
|
|
1326
|
+
const target = onRung >= 0 ? stops[(onRung + 1) % stops.length] : stops[0];
|
|
1327
|
+
ctx.dispatch({
|
|
1328
|
+
type: "PATCH",
|
|
1329
|
+
patch: { zoom: { level: target } }
|
|
1330
|
+
});
|
|
1331
|
+
const sc = buildScene();
|
|
1332
|
+
if (!sc.itemCount) return;
|
|
1333
|
+
const focal = _embedpdf_core_stage.anchorAtPoint(sc, _embedpdf_core_stage.toWorld(cam(), pt));
|
|
1334
|
+
animateZoomAnchored(focal, pt, target, 240, syncCursorFromCamera);
|
|
1335
|
+
},
|
|
838
1336
|
zoomIn: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1.2),
|
|
839
1337
|
zoomOut: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1 / 1.2),
|
|
840
1338
|
zoomTo: (spec) => api.update({ zoom: spec }),
|
|
@@ -843,7 +1341,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
843
1341
|
fitAll: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.FitAll } }),
|
|
844
1342
|
automatic: () => api.update({ zoom: { mode: _embedpdf_core_stage.ZoomMode.Automatic } }),
|
|
845
1343
|
refit: () => {
|
|
846
|
-
if (!
|
|
1344
|
+
if (!placementStarted) return;
|
|
847
1345
|
cancelAnim();
|
|
848
1346
|
reapply(currentAnchor());
|
|
849
1347
|
},
|
|
@@ -852,7 +1350,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
852
1350
|
const doc = ctx.document();
|
|
853
1351
|
if (!doc || doc.pageCount === 0) return;
|
|
854
1352
|
const target = Math.max(0, Math.min(pageIndex, doc.pageCount - 1));
|
|
855
|
-
if (!(!!opts && (opts.rect
|
|
1353
|
+
if (!(!!opts && (opts.rect != null || opts.anchor !== void 0 || opts.zoom !== void 0 && opts.zoom !== "keep"))) {
|
|
856
1354
|
if (paged()) {
|
|
857
1355
|
goToTarget(target, opts);
|
|
858
1356
|
return;
|
|
@@ -918,20 +1416,17 @@ function createStageCapability(ctx, config = {}) {
|
|
|
918
1416
|
},
|
|
919
1417
|
next: (opts) => step(1, opts),
|
|
920
1418
|
prev: (opts) => step(-1, opts),
|
|
1419
|
+
lensId: () => ctx.id,
|
|
921
1420
|
update: (patch) => {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
patch
|
|
929
|
-
});
|
|
930
|
-
if (touched("scene")) sceneCache = null;
|
|
931
|
-
if (touched("reflow")) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
932
|
-
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
933
|
-
else if (touched("reclamp")) setCam(cam());
|
|
1421
|
+
base = mergeSettings(base, patch);
|
|
1422
|
+
syncResponsive(true);
|
|
1423
|
+
},
|
|
1424
|
+
setResponsive: (next) => {
|
|
1425
|
+
rules = next;
|
|
1426
|
+
syncResponsive(true);
|
|
934
1427
|
},
|
|
1428
|
+
matches: (name) => ctx.getState().activeRules.includes(name),
|
|
1429
|
+
activeRules: () => ctx.getState().activeRules,
|
|
935
1430
|
setFlow: (flow) => api.update({ flow }),
|
|
936
1431
|
setLayout: (layout) => api.update({ layout }),
|
|
937
1432
|
setSpread: (spread) => api.update({ spread }),
|
|
@@ -951,9 +1446,13 @@ function createStageCapability(ctx, config = {}) {
|
|
|
951
1446
|
setScrollBehavior: (behavior) => api.update({ scrollBehavior: behavior }),
|
|
952
1447
|
applyViewState: (view) => {
|
|
953
1448
|
cancelAnim();
|
|
1449
|
+
base = mergeSettings(base, pickSettings(view));
|
|
1450
|
+
const { effective, active } = resolveResponsive(base, rules, boxOf(vp()));
|
|
1451
|
+
lastEffective = effective;
|
|
1452
|
+
publishActive(active);
|
|
954
1453
|
ctx.dispatch({
|
|
955
1454
|
type: "PATCH",
|
|
956
|
-
patch:
|
|
1455
|
+
patch: effective
|
|
957
1456
|
});
|
|
958
1457
|
ctx.dispatch({
|
|
959
1458
|
type: "CURSOR",
|
|
@@ -969,62 +1468,24 @@ function createStageCapability(ctx, config = {}) {
|
|
|
969
1468
|
});
|
|
970
1469
|
},
|
|
971
1470
|
placeInitial: () => {
|
|
972
|
-
|
|
1471
|
+
placementStarted = true;
|
|
973
1472
|
const sorted = [...initialViewProviders].sort((a, b) => b.priority - a.priority);
|
|
974
1473
|
for (const p of sorted) {
|
|
975
1474
|
const view = p.fn();
|
|
976
1475
|
if (view) {
|
|
977
1476
|
api.applyViewState(view);
|
|
1477
|
+
ctx.dispatch({ type: "PLACED" });
|
|
978
1478
|
return;
|
|
979
1479
|
}
|
|
980
1480
|
}
|
|
981
1481
|
api.resetView();
|
|
1482
|
+
ctx.dispatch({ type: "PLACED" });
|
|
982
1483
|
},
|
|
983
1484
|
resetView: () => goToTarget(0, { behavior: "instant" })
|
|
984
1485
|
};
|
|
985
1486
|
return api;
|
|
986
1487
|
}
|
|
987
1488
|
//#endregion
|
|
988
|
-
//#region src/scroll-handler.ts
|
|
989
|
-
/**
|
|
990
|
-
* Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
|
|
991
|
-
* (the `scroll` tag) and — with `panFallback` — under every tool, where it then
|
|
992
|
-
* captures ONLY page-gap drags (on-page downs belong to the active tool, which
|
|
993
|
-
* claims them first at higher priority). It uses the sample's viewport delta, so
|
|
994
|
-
* it pans over the whole viewport (pages and gaps).
|
|
995
|
-
*
|
|
996
|
-
* Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
|
|
997
|
-
* hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
|
|
998
|
-
* from its `Tool.cursor`, so hovering it needs no claim here.
|
|
999
|
-
*/
|
|
1000
|
-
function createScrollHandler(stage, interaction, options = {}) {
|
|
1001
|
-
const panFallback = options.panFallback ?? true;
|
|
1002
|
-
let last = {
|
|
1003
|
-
x: 0,
|
|
1004
|
-
y: 0
|
|
1005
|
-
};
|
|
1006
|
-
const isPanTool = () => interaction.activeTool().enables.has("scroll");
|
|
1007
|
-
return {
|
|
1008
|
-
id: "stage-scroll",
|
|
1009
|
-
priority: 10,
|
|
1010
|
-
enabledFor: (tool) => tool.enables.has("scroll") || panFallback,
|
|
1011
|
-
onDown: (s) => {
|
|
1012
|
-
if (!isPanTool() && s.page) return false;
|
|
1013
|
-
last = s.viewport;
|
|
1014
|
-
interaction.setCursor("stage-grab", "grabbing", 40);
|
|
1015
|
-
return true;
|
|
1016
|
-
},
|
|
1017
|
-
onMove: (s) => {
|
|
1018
|
-
stage.panBy(s.viewport.x - last.x, s.viewport.y - last.y);
|
|
1019
|
-
last = s.viewport;
|
|
1020
|
-
},
|
|
1021
|
-
onUp: () => interaction.setCursor("stage-grab", null),
|
|
1022
|
-
onHover: (s) => {
|
|
1023
|
-
if (panFallback && !isPanTool()) interaction.setCursor("stage-pan-fallback", s.page ? null : "grab", 5);
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
//#endregion
|
|
1028
1489
|
//#region src/effects.ts
|
|
1029
1490
|
/**
|
|
1030
1491
|
* The one effect a stage lens needs: when the document's page registry changes
|
|
@@ -1047,19 +1508,22 @@ function registerStageEffects(ctx, token) {
|
|
|
1047
1508
|
//#endregion
|
|
1048
1509
|
//#region src/reducer.ts
|
|
1049
1510
|
const initialStageState = (config) => {
|
|
1050
|
-
const { scheduler: _scheduler, ...overrides } = config;
|
|
1511
|
+
const { scheduler: _scheduler, responsive: _responsive, ...overrides } = config;
|
|
1051
1512
|
return {
|
|
1052
1513
|
camera: {
|
|
1053
1514
|
x: 0,
|
|
1054
1515
|
y: 0,
|
|
1055
1516
|
zoom: 1
|
|
1056
1517
|
},
|
|
1518
|
+
placed: false,
|
|
1519
|
+
cameraResting: true,
|
|
1057
1520
|
vp: {
|
|
1058
1521
|
width: 0,
|
|
1059
1522
|
height: 0
|
|
1060
1523
|
},
|
|
1061
1524
|
dpr: 1,
|
|
1062
1525
|
cursor: 0,
|
|
1526
|
+
activeRules: [],
|
|
1063
1527
|
...DEFAULT_SETTINGS,
|
|
1064
1528
|
...overrides
|
|
1065
1529
|
};
|
|
@@ -1085,6 +1549,14 @@ const stageReducer = (state, a) => {
|
|
|
1085
1549
|
...state,
|
|
1086
1550
|
camera: a.camera
|
|
1087
1551
|
};
|
|
1552
|
+
case "CAMERA_REST": return state.cameraResting === a.resting ? state : {
|
|
1553
|
+
...state,
|
|
1554
|
+
cameraResting: a.resting
|
|
1555
|
+
};
|
|
1556
|
+
case "PLACED": return state.placed ? state : {
|
|
1557
|
+
...state,
|
|
1558
|
+
placed: true
|
|
1559
|
+
};
|
|
1088
1560
|
case "VP": return {
|
|
1089
1561
|
...state,
|
|
1090
1562
|
vp: a.vp
|
|
@@ -1098,6 +1570,10 @@ const stageReducer = (state, a) => {
|
|
|
1098
1570
|
cursor: a.cursor
|
|
1099
1571
|
};
|
|
1100
1572
|
case "PATCH": return applyPatch(state, a.patch);
|
|
1573
|
+
case "RESPONSIVE": return {
|
|
1574
|
+
...state,
|
|
1575
|
+
activeRules: a.active
|
|
1576
|
+
};
|
|
1101
1577
|
default: return state;
|
|
1102
1578
|
}
|
|
1103
1579
|
};
|
|
@@ -1112,19 +1588,14 @@ const StageToken = (0, _embedpdf_core.createCapabilityToken)("stage");
|
|
|
1112
1588
|
* sibling files.
|
|
1113
1589
|
*/
|
|
1114
1590
|
const stagePlugin = (options = {}) => {
|
|
1115
|
-
const { id = "stage", token = StageToken,
|
|
1591
|
+
const { id = "stage", token = StageToken, ...config } = options;
|
|
1116
1592
|
return (0, _embedpdf_core.definePlugin)({
|
|
1117
1593
|
id,
|
|
1118
1594
|
token,
|
|
1119
1595
|
scope: "document",
|
|
1120
|
-
optional: interaction ? [_embedpdf_plugin_interaction.InteractionToken] : void 0,
|
|
1121
1596
|
initialState: () => initialStageState(config),
|
|
1122
1597
|
reduce: stageReducer,
|
|
1123
1598
|
capability: (ctx) => createStageCapability(ctx, config),
|
|
1124
|
-
init: interaction ? (ctx) => {
|
|
1125
|
-
const ix = ctx.tryGet(_embedpdf_plugin_interaction.InteractionToken);
|
|
1126
|
-
if (ix) ix.registerHandler(createScrollHandler(ctx.get(token), ix, { panFallback }));
|
|
1127
|
-
} : void 0,
|
|
1128
1599
|
effects: (ctx) => registerStageEffects(ctx, token)
|
|
1129
1600
|
});
|
|
1130
1601
|
};
|
|
@@ -1271,19 +1742,47 @@ function destinationToReveal(dest, layout, bbox) {
|
|
|
1271
1742
|
};
|
|
1272
1743
|
}
|
|
1273
1744
|
//#endregion
|
|
1274
|
-
//#region src/
|
|
1275
|
-
/**
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1745
|
+
//#region src/scroll-handler.ts
|
|
1746
|
+
/**
|
|
1747
|
+
* Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
|
|
1748
|
+
* (the `scroll` tag) and — with `panFallback` — under every tool, where it then
|
|
1749
|
+
* captures ONLY page-gap drags (on-page downs belong to the active tool, which
|
|
1750
|
+
* claims them first at higher priority). It uses the sample's viewport delta, so
|
|
1751
|
+
* it pans over the whole viewport (pages and gaps).
|
|
1752
|
+
*
|
|
1753
|
+
* Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
|
|
1754
|
+
* hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
|
|
1755
|
+
* from its `Tool.cursor`, so hovering it needs no claim here.
|
|
1756
|
+
*/
|
|
1757
|
+
function createScrollHandler(stage, interaction, options = {}) {
|
|
1758
|
+
const panFallback = options.panFallback ?? true;
|
|
1759
|
+
let last = {
|
|
1760
|
+
x: 0,
|
|
1761
|
+
y: 0
|
|
1762
|
+
};
|
|
1763
|
+
const isPanTool = () => interaction.activeTool().enables.has("scroll");
|
|
1764
|
+
return {
|
|
1765
|
+
id: "stage-scroll",
|
|
1766
|
+
priority: 10,
|
|
1767
|
+
enabledFor: (tool) => tool.enables.has("scroll") || panFallback,
|
|
1768
|
+
onDown: (s) => {
|
|
1769
|
+
if (!isPanTool() && s.page) return false;
|
|
1770
|
+
last = s.viewport;
|
|
1771
|
+
interaction.setCursor("stage-grab", "grabbing", 40);
|
|
1772
|
+
return true;
|
|
1773
|
+
},
|
|
1774
|
+
onMove: (s) => {
|
|
1775
|
+
stage.panBy(s.viewport.x - last.x, s.viewport.y - last.y);
|
|
1776
|
+
last = s.viewport;
|
|
1777
|
+
},
|
|
1778
|
+
onUp: () => interaction.setCursor("stage-grab", null),
|
|
1779
|
+
onHover: (s) => {
|
|
1780
|
+
if (panFallback && !isPanTool()) interaction.setCursor("stage-pan-fallback", s.page ? null : "grab", 5);
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1285
1783
|
}
|
|
1286
1784
|
//#endregion
|
|
1785
|
+
exports.DEFAULT_RESPONSIVE = DEFAULT_RESPONSIVE;
|
|
1287
1786
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
1288
1787
|
exports.StageToken = StageToken;
|
|
1289
1788
|
Object.defineProperty(exports, "ZoomMode", {
|
|
@@ -1292,9 +1791,12 @@ Object.defineProperty(exports, "ZoomMode", {
|
|
|
1292
1791
|
return _embedpdf_core_stage.ZoomMode;
|
|
1293
1792
|
}
|
|
1294
1793
|
});
|
|
1794
|
+
exports.boxOf = boxOf;
|
|
1795
|
+
exports.createScrollHandler = createScrollHandler;
|
|
1295
1796
|
exports.destinationToReveal = destinationToReveal;
|
|
1797
|
+
exports.matchesQuery = matchesQuery;
|
|
1798
|
+
exports.resolveResponsive = resolveResponsive;
|
|
1296
1799
|
exports.settingsEqual = settingsEqual;
|
|
1297
1800
|
exports.stagePlugin = stagePlugin;
|
|
1298
|
-
exports.wheelZoomFactor = wheelZoomFactor;
|
|
1299
1801
|
|
|
1300
1802
|
//# sourceMappingURL=index.cjs.map
|